Merge branch 'feat/signFlow-gen'

This commit is contained in:
Developer 02 2025-05-09 17:00:12 +02:00
commit 3b82467133
75 changed files with 2192 additions and 253 deletions

View File

@ -0,0 +1,18 @@
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///
/// </summary>
public interface IDocumentExecutor
{
/// <summary>
///
/// </summary>
/// <param name="base64"></param>
/// <param name="envelope_uuid"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
Task<EnvelopeDocument> CreateDocumentAsync(string base64, string envelope_uuid, CancellationToken cancellation = default);
}

View File

@ -0,0 +1,21 @@
using Dapper;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///
/// </summary>
public interface IEnvelopeExecutor : ISQLExecutor
{
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <param name="title"></param>
/// <param name="message"></param>
/// <param name="tfaEnabled"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
Task<Envelope> CreateEnvelopeAsync(int userId, string title = "", string message = "", bool tfaEnabled = false, CancellationToken cancellation = default);
}

View File

@ -0,0 +1,20 @@
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///
/// </summary>
public interface IEnvelopeReceiverExecutor
{
/// <summary>
///
/// </summary>
/// <param name="envelope_uuid"></param>
/// <param name="emailAddress"></param>
/// <param name="salutation"></param>
/// <param name="phone"></param>
/// <param name="cancellation"></param>
/// <returns></returns>
Task<EnvelopeReceiver?> AddEnvelopeReceiverAsync(string envelope_uuid, string emailAddress, string? salutation = null, string? phone = null, CancellationToken cancellation = default);
}

View File

@ -0,0 +1,70 @@
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TEntity">The type of the entity being queried.</typeparam>
public interface IQuery<TEntity>
{
/// <summary>
/// Asynchronously retrieves the first entity or a default value if no entity is found.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains the entity or a default value.</returns>
public Task<TEntity?> FirstOrDefaultAsync();
/// <summary>
/// Asynchronously retrieves a single entity or a default value if no entity is found.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains the entity or a default value.</returns>
public Task<TEntity?> SingleOrDefaultAsync();
/// <summary>
/// Asynchronously retrieves a list of entities.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains the list of entities.</returns>
public Task<IEnumerable<TEntity>> ToListAsync();
/// <summary>
/// Asynchronously retrieves the first entity. Throws an exception if no entity is found.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains the first entity.</returns>
public Task<TEntity> FirstAsync();
/// <summary>
/// Asynchronously retrieves a single entity. Throws an exception if no entity is found.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains the single entity.</returns>
public Task<TEntity> SingleAsync();
/// <summary>
/// Synchronously retrieves the first entity or a default value if no entity is found.
/// </summary>
/// <returns>The first entity or a default value.</returns>
public TEntity? FirstOrDefault();
/// <summary>
/// Synchronously retrieves a single entity or a default value if no entity is found.
/// </summary>
/// <returns>The single entity or a default value.</returns>
public TEntity? SingleOrDefault();
/// <summary>
/// Synchronously retrieves a list of entities.
/// </summary>
/// <returns>The list of entities.</returns>
public IEnumerable<TEntity> ToList();
/// <summary>
/// Synchronously retrieves the first entity. Throws an exception if no entity is found.
/// </summary>
/// <returns>The first entity.</returns>
public TEntity First();
/// <summary>
/// Synchronously retrieves a single entity. Throws an exception if no entity is found.
/// </summary>
/// <returns>The single entity.</returns>
public TEntity Single();
}

View File

@ -0,0 +1,20 @@
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
/// Represents a raw SQL query contract.
/// </summary>
public interface ISQL
{
/// <summary>
/// Gets the raw SQL query string.
/// </summary>
string Raw { get; }
}
/// <summary>
/// Represents a typed SQL query contract for a specific entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity associated with the SQL query.</typeparam>
public interface ISQL<TEntity> : ISQL
{
}

View File

@ -0,0 +1,27 @@
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
/// 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 <typeparamref name="TEntity"/> objects.
/// </summary>
/// <typeparam name="TEntity">The entity type to which the SQL query results will be mapped.</typeparam>
public interface ISQLExecutor<TEntity>: ISQLExecutor
{
/// <summary>
/// Executes a raw SQL query and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result.
/// </summary>
/// <param name="sql">The raw SQL query to execute.</param>
/// <param name="cancellation">Optional cancellation token for the operation.</param>
/// <param name="parameters">Optional parameters for the SQL query.</param>
/// <returns>An <see cref="IQuery{TEntity}"/> instance for further query operations on the result.</returns>
IQuery<TEntity> Execute(string sql, CancellationToken cancellation = default, params object[] parameters);
/// <summary>
/// Executes a custom SQL query defined by a class that implements <see cref="ISQL{TEntity}"/> and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result.
/// </summary>
/// <typeparam name="TSQL">The type of the custom SQL query class implementing <see cref="ISQL{TEntity}"/>.</typeparam>
/// <param name="cancellation">Optional cancellation token for the operation.</param>
/// <param name="parameters">Optional parameters for the SQL query.</param>
/// <returns>An <see cref="IQuery{TEntity}"/> instance for further query operations on the result.</returns>
IQuery<TEntity> Execute<TSQL>(CancellationToken cancellation = default, params object[] parameters) where TSQL : ISQL<TEntity>;
}

View File

@ -0,0 +1,29 @@
using Dapper;
namespace EnvelopeGenerator.Application.Contracts.SQLExecutor;
/// <summary>
///
/// </summary>
public interface ISQLExecutor
{
/// <summary>
/// Executes a raw SQL query and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result.
/// </summary>
/// <typeparam name="TEntity">The entity type to which the SQL query results will be mapped.</typeparam>
/// <param name="sql">The raw SQL query to execute.</param>
/// <param name="parameters">Parameters for the SQL query.</param>
/// <param name="cancellation">Optional cancellation token for the operation.</param>
/// <returns>An <see cref="IQuery{TEntity}"/> instance for further query operations on the result.</returns>
Task<IEnumerable<TEntity>> Execute<TEntity>(string sql, DynamicParameters parameters, CancellationToken cancellation = default);
/// <summary>
/// Executes a custom SQL query defined by a class that implements <see cref="ISQL{TEntity}"/> and returns an <see cref="IQuery{TEntity}"/> for further querying operations on the result.
/// </summary>
/// <typeparam name="TEntity">The entity type to which the SQL query results will be mapped.</typeparam>
/// <typeparam name="TSQL">The type of the custom SQL query class implementing <see cref="ISQL{TEntity}"/>.</typeparam>
/// <param name="parameters">Parameters for the SQL query.</param>
/// <param name="cancellation">Optional cancellation token for the operation.</param>
/// <returns>An <see cref="IQuery{TEntity}"/> instance for further query operations on the result.</returns>
Task<IEnumerable<TEntity>> Execute<TEntity, TSQL>(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL;
}

View File

@ -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<EnvelopeReceiverDt
Task<DataResult<bool>> IsExisting(string envelopeReceiverId);
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses);
Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, EnvelopeQuery? envelopeQuery = null, ReadReceiverQuery? receiverQuery = null, params int[] ignore_statuses);
Task<DataResult<string?>> ReadLastUsedReceiverNameByMail(string mail);

View File

@ -3,10 +3,30 @@ using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.DTOs
{
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EmailTemplateDto(
int Id,
string Name,
string Body,
string Subject) : IUnique<int>;
public record EmailTemplateDto : IUnique<int>
{
/// <summary>
///
/// </summary>
public int Id{ get; init; }
/// <summary>
///
/// </summary>
public required string Name { get; init; }
/// <summary>
///
/// </summary>
public required string Body { get; set; }
/// <summary>
///
/// </summary>
public required string Subject { get; set; }
};
}

View File

@ -66,6 +66,9 @@ namespace EnvelopeGenerator.Application.DTOs
public string? StatusTranslated { get; set; }
public string? ContractTypeTranslated { get; set; }
public byte[]? DocResult { get; init; }
public IEnumerable<EnvelopeDocumentDto>? Documents { get; set; }
}
}

View File

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

View File

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

View File

@ -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 <see cref="EmailTemplateQuery"/> und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage.
/// </summary>
/// <param name="Id">Die optionale ID der E-Mail-Vorlage, die zurückgesetzt werden soll.</param>
/// <param name="Type">Der Typ der E-Mail-Vorlage, z. B. <see cref="Constants.EmailTemplateType"/> (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.
/// </param>
public record ResetEnvelopeTemplateCommand(int? Id, Constants.EmailTemplateType? Type) : EmailTemplateQuery(Id, Type);
public record ResetEmailTemplateCommand : EmailTemplateQuery, IRequest
{
/// <summary>
///
/// </summary>
/// <param name="orginal"></param>
public ResetEmailTemplateCommand(EmailTemplateQuery? orginal = null) : base(orginal ?? new())
{
}
/// <summary>
///
/// </summary>
/// <param name="Id">Die optionale ID der E-Mail-Vorlage, die zurückgesetzt werden soll.</param>
/// <param name="Type">Der Typ der E-Mail-Vorlage, z. B. <see cref="Constants.EmailTemplateType"/> (optional).
public ResetEmailTemplateCommand(int? Id = null, Constants.EmailTemplateType? Type = null) : base(Id, Type)
{
}
};

View File

@ -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;
/// <summary>
///
/// </summary>
public class ResetEmailTemplateCommandHandler : IRequestHandler<ResetEmailTemplateCommand>
{
private readonly IRepository<EmailTemplate> _repository;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public ResetEmailTemplateCommandHandler(IRepository<EmailTemplate> repository)
{
_repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task Handle(ResetEmailTemplateCommand request, CancellationToken cancel)
{
var temps = request.Id is not null
? await _repository.ReadAllAsync<EmailTemplateDto>(t => t.Id == request.Id, cancel)
: request.Type is not null
? await _repository.ReadAllAsync<EmailTemplateDto>(t => t.Name == request.Type.ToString(), cancel)
: await _repository.ReadAllAsync<EmailTemplateDto>(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);
}
}
/// <summary>
///
/// </summary>
public static readonly IEnumerable<EmailTemplateDto> Defaults = new List<EmailTemplateDto>()
{
new(){
Id = 1,
Name = "DocumentReceived",
Body = "Guten Tag [NAME_RECEIVER],<br />\r\n<br /><B><I>\r\n[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet.<br />\r\n<br />\r\nÜber den folgenden Link können Sie das Dokument einsehen und elektronisch unterschreiben: <a href=\"[LINK_TO_DOCUMENT]\">[LINK_TO_DOCUMENT_TEXT]</a><br />\r\n<br />\r\n[MESSAGE]<br />\r\n<br />\r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "Dokument erhalten: '[DOCUMENT_TITLE]'"
},
new(){
Id = 2,
Name = "DocumentDeleted",
Body = "Guten Tag [NAME_RECEIVER],<br />\r\n<br /><B><I>\r\n[NAME_SENDER]</I></B> hat den Umschlag <B><I>'[DOCUMENT_TITLE]'</I></B> gelöscht/zurückgezogen.<br /><p>\rBegründung: <br /> <I>[REASON]</I> <p>\r\n<br />\r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "Umschlag zurückgezogen: '[DOCUMENT_TITLE]'"
},
new(){
Id = 3,
Name = "DocumentSigned",
Body = "Guten Tag [NAME_RECEIVER],<br />\r\n<br />\r\nhiermit bestätigen wir Ihnen die erfolgreiche Signatur für den Vorgang <B><I>'[DOCUMENT_TITLE]'</I></B>.<br />\r\nWenn alle Vertragspartner unterzeichnet haben, erhalten Sie ebenfalls per email ein unterschriebenes Exemplar mit dem Signierungszertifikat!\r\n<br />\r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "Dokument unterschrieben: '[DOCUMENT_TITLE]'"
},
new(){
Id = 4,
Name = "DocumentCompleted",
Body = "Guten Tag [NAME_RECEIVER],<br />\r\n<br />\r\nDer Signaturvorgang <B><I>'[DOCUMENT_TITLE]'</I></B> wurde erfolgreich abgeschlossen.<br />\r\n<br />\r\nSie erhalten das Dokument mit einem detaillierten Ergebnisbericht als Anhang zu dieser Email.<br />\r\n<br />\r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "Umschlag abgeschlossen: '[DOCUMENT_TITLE]'"
},
new(){
Id = 5,
Name = "DocumentAccessCodeReceived",
Body = "Guten Tag [NAME_RECEIVER],<br />\r\n<br /><B><I>\r\n[NAME_SENDER]</I></B> hat Ihnen ein Dokument zum [SIGNATURE_TYPE] gesendet. <br />\r\n<br />\r\nVerwenden Sie den folgenden Zugriffscode, um das Dokument einzusehen:<br />\r\n<br />\r\n[DOCUMENT_ACCESS_CODE]<br />\r\n<br />\r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "Zugriffscode für Dokument erhalten: '[DOCUMENT_TITLE]'"
},
new(){
Id = 6,
Name = "DocumentRejected_ADM",
Body = "Guten Tag [NAME_SENDER],<p><B><I>[NAME_RECEIVER]</I></B> hat den Umschlag <B><I>'[DOCUMENT_TITLE]'</I></B> mit folgendem Grund abgelehnt: <p>\r\n[REASON] \r\n<p>Der Umschlag wurde auf den Status Rejected gesetzt. <p> \r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "'[DOCUMENT_TITLE]' - Unterzeichnungsvorgang zurückgezogen"
},
new(){
Id = 9,
Name = "DocumentRejected_REC",
Body = "Guten Tag [NAME_RECEIVER],\r\n<p>Hiermit bestätigen wir Ihnen die Ablehnung des Unterzeichnungsvorganges <B><I>'[DOCUMENT_TITLE]'</I></B>!<p>Der Vertragsinhaber <B><I>[NAME_SENDER]</I></B> wurde über die Ablehnung informiert. <p> \r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "'[DOCUMENT_TITLE]' - Bestätigung Ablehnung"
},
new(){
Id = 10,
Name = "DocumentRejected_REC_2",
Body = "Guten Tag [NAME_RECEIVER],\r\n<p>Der Unterzeichnungsvorganges <B><I>'[DOCUMENT_TITLE]'</I></B> wurde durch einen anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.<p> Der Vertragsinhaber <B><I>[NAME_SENDER]</I></B> wird sich bei Bedarf mit Ihnen in Verbindung setzen. <p> \r\nMit freundlichen Grüßen<br />\r\n<br />\r\n[NAME_PORTAL]",
Subject = "'[DOCUMENT_TITLE]' - Unterzeichnungsvorgang abgelehnt."
},
new(){
Id = 11,
Name = "DocumentShared",
Body = "Guten Tag,<br /> <br /><B><I> [NAME_RECEIVER]</I></B> hat Ihnen ein Dokument zum Ansehen gesendet.<br /> <br /> Über den folgenden Link können Sie das Dokument einsehen: <a href=\"[LINK_TO_DOCUMENT]\">[LINK_TO_DOCUMENT_TEXT]</a><br /> <br /> <br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]",
Subject = "Dokument geteilt: '[DOCUMENT_TITLE]'"
},
new(){
Id = 12,
Name = "TotpSecret",
Body = "Guten Tag,<br /> <br />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.<br /> <br /> <img src=\"data:image/png;base64,[TFA_QR_CODE]\" style=\"width: 13rem; height: 13rem;\"><br /> <br />\r\n<br /> Mit freundlichen Grüßen<br /> <br /> [NAME_PORTAL]",
Subject = "2-Faktor-Verifizierung QR-Code"
}
};
}

View File

@ -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;
/// <param name="Subject">
/// (Optional) Der neue Betreff der E-Mail. Wenn null, bleibt der vorhandene Betreff unverändert.
/// </param>
public record UpdateEmailTemplateCommand(string? Body = null, string? Subject = null)
public record UpdateEmailTemplateCommand(string? Body = null, string? Subject = null) : IRequest
{
/// <param>
/// Die Abfrage, die die E-Mail-Vorlage darstellt, die aktualisiert werden soll.
/// </param>
[JsonIgnore]
public EmailTemplateQuery? EmailTemplateQuery { get; set; }
/// <summary>
///
/// </summary>
[JsonIgnore]
public DateTime ChangedWhen { get; init; } = DateTime.Now;
}

View File

@ -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;
/// <summary>
///
/// </summary>
public class UpdateEmailTemplateCommandHandler : IRequestHandler<UpdateEmailTemplateCommand>
{
private readonly IRepository<EmailTemplate> _repository;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
public UpdateEmailTemplateCommandHandler(IRepository<EmailTemplate> repository)
{
_repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="NotFoundException"></exception>
public async Task Handle(UpdateEmailTemplateCommand request, CancellationToken cancel)
{
EmailTemplateDto? temp;
if (request.EmailTemplateQuery?.Id is int id)
{
temp = await _repository.ReadOrDefaultAsync<EmailTemplateDto>(t => t.Id == id, single: false, cancel);
}
else if (request!.EmailTemplateQuery!.Type is Common.Constants.EmailTemplateType type)
{
temp = await _repository.ReadOrDefaultAsync<EmailTemplateDto>(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);
}
}

View File

@ -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;
/// <summary>
///
/// </summary>
public class ReadEmailTemplateMappingProfile : Profile
{
/// <summary>
///
/// </summary>
public ReadEmailTemplateMappingProfile()
{
CreateMap<EmailTemplate, ReadEmailTemplateResponse>();
}
}

View File

@ -1,10 +1,12 @@
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
using MediatR;
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
/// <summary>
/// Stellt eine Abfrage dar, um eine E-Mail-Vorlage zu lesen.
/// Diese Klasse erbt von <see cref="EmailTemplateQuery"/>.
/// </summary>
public record ReadEmailTemplateQuery : EmailTemplateQuery
public record ReadEmailTemplateQuery : EmailTemplateQuery, IRequest<ReadEmailTemplateResponse?>
{
}

View File

@ -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;
/// <summary>
///
/// </summary>
public class ReadEmailTemplateQueryHandler : IRequestHandler<ReadEmailTemplateQuery, ReadEmailTemplateResponse?>
{
private readonly IMapper _mapper;
private readonly IEmailTemplateRepository _repository;
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </summary>
/// <param name="mapper">
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
public ReadEmailTemplateQueryHandler(IMapper mapper, IEmailTemplateRepository repository)
{
_mapper = mapper;
_repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<ReadEmailTemplateResponse?> 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<ReadEmailTemplateResponse>(temp);
return res;
}
}

View File

@ -3,18 +3,35 @@
/// <summary>
/// Stellt die Antwort für eine Abfrage von E-Mail-Vorlagen bereit.
/// </summary>
/// <param name="Id">Die eindeutige Kennung der E-Mail-Vorlage.</param>
/// <param name="Type">Der Typ der E-Mail-Vorlage.</param>
/// <param name="AddedWhen">Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde.</param>
/// <param name="Body">Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein.</param>
/// <param name="Subject">Der Betreff der E-Mail-Vorlage. Kann null sein.</param>
/// <param name="ChangedWhen">Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.</param>
public record ReadEmailTemplateResponse(
int Id,
int Type,
DateTime AddedWhen,
string? Body = null,
string? Subject = null,
DateTime? ChangedWhen = null)
public class ReadEmailTemplateResponse
{
/// <summary>
/// Die eindeutige Kennung der E-Mail-Vorlage.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Name des Typs
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde.
/// </summary>
public DateTime AddedWhen { get; set; }
/// <summary>
/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Body { get; set; }
/// <summary>
/// Der Betreff der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Subject { get; set; }
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.
/// </summary>
public DateTime? ChangedWhen { get; set; }
}

View File

@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.2.1" />
<PackageReference Include="DigitalData.Core.Client" Version="2.0.3" />
@ -20,6 +21,7 @@
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.0.0" />
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
<PackageReference Include="QRCoder" Version="1.6.0" />

View File

@ -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<ReceiverGetOrCreateCommand> Receivers,
bool TFAEnabled = false
) : IRequest;
#region DTOs
/// <summary>
/// Signaturposition auf einem Dokument.
/// </summary>
/// <param name="X">X-Position</param>
/// <param name="Y">Y-Position</param>
/// <param name="Page">Seite, auf der sie sich befindet</param>
public record Signature([Required] int X, [Required] int Y, [Required] int Page);
/// <summary>
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
/// Wenn nicht, wird sie erstellt und mit einer Signatur versehen.
/// </summary>
/// <param name="Signatures">Unterschriften auf Dokumenten.</param>
/// <param name="Salution">Der Name, mit dem der Empfänger angesprochen werden soll. Bei Null oder keinem Wert wird der zuletzt verwendete Name verwendet.</param>
/// <param name="PhoneNumber">Sollte mit Vorwahl geschrieben werden</param>
public record ReceiverGetOrCreateCommand([Required] IEnumerable<Signature> Signatures, string? Salution = null, string? PhoneNumber = null)
{
private string _emailAddress = string.Empty;
/// <summary>
/// E-Mail-Adresse des Empfängers.
/// </summary>
[Required]
public required string EmailAddress { get => _emailAddress.ToLower(); init => _emailAddress = _emailAddress.ToLower(); }
};
/// <summary>
/// DTO zum Erstellen eines Dokuments.
/// </summary>
/// <param name="DataAsByte">
/// Die Dokumentdaten im Byte-Array-Format. Wird verwendet, wenn das Dokument als Roh-Binärdaten bereitgestellt wird.
/// </param>
/// <param name="DataAsBase64">
/// Die Dokumentdaten im Base64-String-Format. Wird verwendet, wenn das Dokument als Base64-codierter String bereitgestellt wird.
/// </param>
public record DocumentCreateCommand(byte[]? DataAsByte = null, string? DataAsBase64 = null);
#endregion
) : CreateEnvelopeCommand(Title, Message, TFAEnabled), IRequest<CreateEnvelopeReceiverResponse?>;

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
public class CreateEnvelopeReceiverCommandHandler : IRequestHandler<CreateEnvelopeReceiverCommand, CreateEnvelopeReceiverResponse>
{
private readonly IMapper _mapper;
private readonly IEnvelopeExecutor _envelopeExecutor;
private readonly IEnvelopeReceiverExecutor _erExecutor;
/// <summary>
///
/// </summary>
/// <param name="mapper"></param>
/// <param name="envelopeExecutor"></param>
/// <param name="erExecutor"></param>
public CreateEnvelopeReceiverCommandHandler(IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor)
{
_mapper = mapper;
_envelopeExecutor = envelopeExecutor;
_erExecutor = erExecutor;
}
/// <summary>
/// Handles the execution of the <see cref="CreateEnvelopeReceiverCommand"/>.
/// Responsible for validating input data, creating or retrieving recipients, associating signatures,
/// and storing the envelope and document details.
/// </summary>
/// <param name="request">The command containing all necessary information to create an envelope.</param>
/// <param name="cancel">Token to observe while waiting for the task to complete.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task<CreateEnvelopeReceiverResponse> 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<EnvelopeReceiver> sentRecipients = new();
List<ReceiverGetOrCreateCommand> 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<CreateEnvelopeReceiverResponse>(envelope);
res.UnsentReceivers = unsentRecipients;
res.SentReceiver = _mapper.Map<IEnumerable<ReceiverReadDto>>(sentRecipients);
return res;
}
}

View File

@ -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
/// <summary>
/// Signaturposition auf einem Dokument.
/// </summary>
/// <param name="X">X-Position</param>
/// <param name="Y">Y-Position</param>
/// <param name="Page">Seite, auf der sie sich befindet</param>
public record Signature([Required] double X, [Required] double Y, [Required] int Page);
/// <summary>
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
/// Wenn nicht, wird sie erstellt und mit einer Signatur versehen.
/// </summary>
/// <param name="Signatures">Unterschriften auf Dokumenten.</param>
/// <param name="Salution">Der Name, mit dem der Empfänger angesprochen werden soll. Bei Null oder keinem Wert wird der zuletzt verwendete Name verwendet.</param>
/// <param name="PhoneNumber">Sollte mit Vorwahl geschrieben werden</param>
public record ReceiverGetOrCreateCommand([Required] IEnumerable<Signature> Signatures, string? Salution = null, string? PhoneNumber = null)
{
private string _emailAddress = string.Empty;
/// <summary>
/// E-Mail-Adresse des Empfängers.
/// </summary>
[Required]
public string EmailAddress { get => _emailAddress.ToLower(); init => _emailAddress = value.ToLower(); }
};
/// <summary>
/// DTO zum Erstellen eines Dokuments.
/// </summary>
public record DocumentCreateCommand()
{
/// <summary>
/// Die Dokumentdaten im Base64-String-Format. Wird verwendet, wenn das Dokument als Base64-codierter String bereitgestellt wird.
/// </summary>
[Required]
public required string DataAsBase64 { get; init; }
};
#endregion

View File

@ -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;
/// <summary>
///
/// </summary>
public class CreateEnvelopeReceiverMappingProfile : Profile
{
/// <summary>
///
/// </summary>
public CreateEnvelopeReceiverMappingProfile()
{
CreateMap<Envelope, CreateEnvelopeResponse>();
CreateMap<Receiver, ReceiverReadDto>();
}
}

View File

@ -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;
/// <summary>
///
/// </summary>
public record CreateEnvelopeReceiverResponse : CreateEnvelopeResponse
{
/// <summary>
///
/// </summary>
/// <param name="Id"></param>
/// <param name="UserId"></param>
/// <param name="Status"></param>
/// <param name="Uuid"></param>
/// <param name="Message"></param>
/// <param name="AddedWhen"></param>
/// <param name="ChangedWhen"></param>
/// <param name="Title"></param>
/// <param name="Language"></param>
/// <param name="TFAEnabled"></param>
/// <param name="User"></param>
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)
{
}
/// <summary>
///
/// </summary>
public IEnumerable<ReceiverReadDto> SentReceiver { get; set; } = new List<ReceiverReadDto>();
/// <summary>
///
/// </summary>
public IEnumerable<ReceiverGetOrCreateCommand> UnsentReceivers { get; set; } = new List<ReceiverGetOrCreateCommand>();
}

View File

@ -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;
/// <summary>
/// Befehl zur Erstellung eines Umschlags.
/// </summary>
/// <param name="Title">Der Titel des Umschlags. Dies ist ein Pflichtfeld.</param>
/// <param name="Message">Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.</param>
/// <param name="TFAEnabled">Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.</param>
public record CreateEnvelopeCommand(
[Required] string Title,
[Required] string Message,
bool TFAEnabled = false
) : IRequest<CreateEnvelopeResponse?>
{
/// <summary>
/// Id of receiver
/// </summary>
[JsonIgnore]
[BindNever]
public int? UserId { get; set; }
};

View File

@ -0,0 +1,41 @@
using AutoMapper;
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using MediatR;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// <summary>
///
/// </summary>
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, CreateEnvelopeResponse?>
{
private readonly IEnvelopeExecutor _envelopeExecutor;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="envelopeExecutor"></param>
/// <param name="mapper"></param>
public CreateEnvelopeCommandHandler(IEnvelopeExecutor envelopeExecutor, IMapper mapper)
{
_envelopeExecutor = envelopeExecutor;
_mapper = mapper;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<CreateEnvelopeResponse?> 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<CreateEnvelopeResponse>(envelope);
}
}

View File

@ -0,0 +1,19 @@
using AutoMapper;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// <summary>
///
/// </summary>
public class CreateEnvelopeMappingProfile : Profile
{
/// <summary>
///
/// </summary>
public CreateEnvelopeMappingProfile()
{
CreateMap<Envelope, CreateEnvelopeReceiverResponse>();
}
}

View File

@ -0,0 +1,20 @@
using EnvelopeGenerator.Application.Envelopes.Queries.Read;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// <summary>
///
/// </summary>
/// <param name="Id"><inheritdoc/></param>
/// <param name="UserId"><inheritdoc/></param>
/// <param name="Status"><inheritdoc/></param>
/// <param name="Uuid"><inheritdoc/></param>
/// <param name="Message"><inheritdoc/></param>
/// <param name="AddedWhen"><inheritdoc/></param>
/// <param name="ChangedWhen"><inheritdoc/></param>
/// <param name="Title"><inheritdoc/></param>
/// <param name="Language"><inheritdoc/></param>
/// <param name="TFAEnabled"><inheritdoc/></param>
/// <param name="User"><inheritdoc/></param>
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);

View File

@ -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.
/// </summary>
/// <param name="Envelope">Der Umschlag, für den die Anrede des Empfängers ermittelt werden soll.</param>
/// <param name="OnlyLast">Gibt an, ob nur die zuletzt verwendete Anrede zurückgegeben werden soll.</param>
public record ReadReceiverNameQuery(EnvelopeQuery? Envelope = null, bool OnlyLast = true) : ReadReceiverQuery
public record ReadReceiverNameQuery() : ReadReceiverQuery
{
}

View File

@ -0,0 +1,22 @@
namespace EnvelopeGenerator.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when a bad request is encountered.
/// </summary>
public class BadRequestException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="BadRequestException"/> class.
/// </summary>
public BadRequestException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BadRequestException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public BadRequestException(string? message) : base(message)
{
}
}

View File

@ -0,0 +1,22 @@
namespace EnvelopeGenerator.Application.Exceptions;
/// <summary>
/// Represents an exception that is thrown when a requested resource is not found.
/// </summary>
public class NotFoundException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="NotFoundException"/> class.
/// </summary>
public NotFoundException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="NotFoundException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public NotFoundException(string? message) : base(message)
{
}
}

View File

@ -4,17 +4,14 @@ using EnvelopeGenerator.Common;
namespace EnvelopeGenerator.Application.Histories.Queries.Read;
//TODO: Add sender query
/// <summary>
/// Repräsentiert eine Abfrage für die Verlaufshistorie eines Umschlags.
/// </summary>
/// <param name="EnvelopeId">Die eindeutige Kennung des Umschlags.</param>
/// <param name="Envelope">Die Abfrage, die den Umschlag beschreibt.</param>
/// <param name="Receiver">Die Abfrage, die den Empfänger beschreibt.</param>
/// <param name="Related">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.</param>
/// <param name="OnlyLast">Abfrage zur Steuerung, ob nur der aktuelle Status oder der gesamte Datensatz zurückgegeben wird.</param>
public record ReadHistoryQuery(
int EnvelopeId,
ReadEnvelopeQuery? Envelope = null,
ReadReceiverQuery? Receiver = null,
int? EnvelopeId = null,
Constants.ReferenceType? Related = null,
bool? OnlyLast = true);

View File

@ -0,0 +1,42 @@
using Dapper;
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.SQL;
/// <summary>
///
/// </summary>
public class DocumentCreateReadSQL : ISQL<EnvelopeDocument>
{
/// <summary>
/// Base64, OUT_UID
/// </summary>
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
";
/// <summary>
///
/// </summary>
/// <param name="base64"></param>
/// <returns></returns>
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;
}
}

View File

@ -0,0 +1,49 @@
using Dapper;
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using EnvelopeGenerator.Domain.Entities;
using System.Data;
namespace EnvelopeGenerator.Application.SQL;
/// <summary>
///
/// </summary>
public class EnvelopeCreateReadSQL : ISQL<Envelope>
{
/// <summary>
/// USER_ID, TITLE, TFAEnabled, MESSAGE
/// </summary>
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;
";
/// <summary>
///
/// </summary>
/// <param name="userId"></param>
/// <param name="title"></param>
/// <param name="message"></param>
/// <param name="tfaEnabled"></param>
/// <returns></returns>
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;
}
}

View File

@ -0,0 +1,47 @@
using Dapper;
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.SQL;
/// <summary>
///
/// </summary>
public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
{
/// <summary>
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
/// </summary>
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;
";
/// <summary>
///
/// </summary>
/// <param name="envelope_uuid"></param>
/// <param name="emailAddress"></param>
/// <param name="salutation"></param>
/// <param name="phone"></param>
/// <returns></returns>
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;
}
}

View File

@ -0,0 +1,30 @@
using System.Globalization;
namespace EnvelopeGenerator.Application.SQL;
/// <summary>
/// Extension method for converting objects to SQL parameter strings.
/// </summary>
public static class ParamsExtensions
{
/// <summary>
/// Converts a .NET object to its corresponding SQL-safe parameter string.
/// </summary>
/// <param name="obj">The object to convert.</param>
/// <returns>A string representing the SQL parameter.</returns>
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.");
}
}

View File

@ -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<IEnvelopeReceiverReposit
: Result.Success(code);
}
public async Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> ReadByUsernameAsync(string username, int? min_status = null, int? max_status = null, params int[] ignore_statuses)
public async Task<DataResult<IEnumerable<EnvelopeReceiverDto>>> 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<IEnumerable<EnvelopeReceiverDto>>(er_list);
return Result.Success(dto_list);
}

View File

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

View File

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

View File

@ -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; }
/// <summary>
/// The sender of envelope
/// </summary>

View File

@ -13,6 +13,7 @@
<ItemGroup>
<PackageReference Include="HtmlSanitizer" Version="8.0.865" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="7.0.19" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Otp.NET" Version="1.4.0" />

View File

@ -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<string, int> GetEnumAsDictionary<TEnum>(this IMemoryCache memoryCache)
where TEnum : Enum
=> memoryCache.GetOrCreate(BaseId + typeof(TEnum).FullName, _ =>
Enum.GetValues(typeof(TEnum))
.Cast<TEnum>()
.ToDictionary(e => e.ToString(), e => Convert.ToInt32(e)))
?? throw new InvalidOperationException($"Failed to cache or retrieve enum dictionary for type '{typeof(TEnum).FullName}'.");
}

View File

@ -117,18 +117,10 @@ public partial class AuthController : ControllerBase
[Authorize]
[HttpPost("logout")]
public async Task<IActionResult> 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);
}
}
/// <summary>
/// Prüft, ob der Benutzer ein autorisiertes Token hat.

View File

@ -3,22 +3,60 @@ using System.Security.Claims;
namespace EnvelopeGenerator.GeneratorAPI.Controllers
{
/// <summary>
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
/// </summary>
public static class ControllerExtensions
{
public static int? GetId(this ClaimsPrincipal user)
=> int.TryParse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value, out int result)
/// <summary>
/// Attempts to retrieve the user's ID from the claims. Returns null if the ID is not found or invalid.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The user's ID as an integer, or null if not found or invalid.</returns>
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)
/// <summary>
/// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The user's ID as an integer.</returns>
/// <exception cref="InvalidOperationException">Thrown if the user ID claim is missing or invalid.</exception>
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.");
/// <summary>
/// Retrieves the username from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The username as a string, or null if not found.</returns>
public static string? GetUsernameOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Name)?.Value;
public static string? GetName(this ClaimsPrincipal user)
/// <summary>
/// Retrieves the user's surname (last name) from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The surname as a string, or null if not found.</returns>
public static string? GetNameOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Surname)?.Value;
public static string? GetPrename(this ClaimsPrincipal user)
/// <summary>
/// Retrieves the user's given name (first name) from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The given name as a string, or null if not found.</returns>
public static string? GetPrenameOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.GivenName)?.Value;
public static string? GetEmail(this ClaimsPrincipal user)
/// <summary>
/// Retrieves the user's email address from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The email address as a string, or null if not found.</returns>
public static string? GetEmailOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Email)?.Value;
}
}

View File

@ -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;
/// <summary>
/// Controller for managing email templates.
/// Controller for managing temp templates.
/// Steuerung zur Verwaltung von E-Mail-Vorlagen.
/// </summary>
[Route("api/[controller]")]
@ -17,17 +23,27 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
[Authorize]
public class EmailTemplateController : ControllerBase
{
private readonly ILogger<EmailTemplateController> _logger;
private readonly IMapper _mapper;
private readonly IEmailTemplateRepository _repository;
private readonly IMediator _mediator;
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </summary>
/// <param name="mapper">
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
public EmailTemplateController(IMapper mapper)
public EmailTemplateController(IMapper mapper, IEmailTemplateRepository repository, ILogger<EmailTemplateController> logger, IMediator mediator)
{
_mapper = mapper;
_repository = repository;
_logger = logger;
_mediator = mediator;
}
/// <summary>
@ -45,17 +61,25 @@ public class EmailTemplateController : ControllerBase
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
[HttpGet]
public IActionResult Get([FromQuery] ReadEmailTemplateQuery? emailTemplate = null)
public async Task<IActionResult> 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<IEnumerable<EmailTemplateDto>>(temps));
}
else
{
var temp = await _mediator.Send(emailTemplate);
return temp is null ? NotFound() : Ok(temp);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="email">Die E-Mail-Vorlagenabfrage.</param>
/// <param name="temp">Die E-Mail-Vorlagenabfrage.</param>
/// <param name="update">Der Aktualisierungsbefehl für die E-Mail-Vorlage.
/// Wird auf Standardwert aktualisiert, wenn die Anfrage ohne http-Body gesendet wird.
/// </param>
@ -73,19 +97,22 @@ public class EmailTemplateController : ControllerBase
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
[HttpPut]
public IActionResult Update([FromQuery] EmailTemplateQuery email, [FromBody] UpdateEmailTemplateCommand? update = null)
public async Task<IActionResult> Update([FromQuery] EmailTemplateQuery? temp = null, [FromBody] UpdateEmailTemplateCommand? update = null)
{
if (update is null)
{
var reset = _mapper.Map<ResetEnvelopeTemplateCommand>(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
}
return Ok(); // Placeholder for actual implementation
update.EmailTemplateQuery = temp;
await _mediator.Send(update);
return Ok();
}
}
}

View File

@ -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<EnvelopeController> _logger;
private readonly IEnvelopeService _envelopeService;
private readonly IMediator _mediator;
/// <summary>
/// Erstellt eine neue Instanz des EnvelopeControllers.
/// </summary>
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
/// <param name="envelopeService">Der Dienst, der für die Verarbeitung von Umschlägen zuständig ist.</param>
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService)
/// <param name="mediator"></param>
public EnvelopeController(ILogger<EnvelopeController> logger, IEnvelopeService envelopeService, IMediator mediator)
{
_logger = logger;
_envelopeService = envelopeService;
_mediator = mediator;
}
/// <summary>
@ -52,12 +60,22 @@ public class EnvelopeController : ControllerBase
[Authorize]
[HttpGet]
public async Task<IActionResult> 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: Ok,
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);
@ -69,10 +87,71 @@ public class EnvelopeController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
catch (Exception ex)
/// <summary>
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
/// </summary>
/// <param name="id">Die eindeutige ID des Umschlags.</param>
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
[HttpGet("doc-result")]
public async Task<IActionResult> GetDocResultAsync([FromQuery] int id, [FromQuery] bool view = false)
{
_logger.LogError(ex, "{Message}", ex.Message);
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);
}
}
/// <summary>
///
/// </summary>
/// <param name="envelope"></param>
/// <returns></returns>
[NonAction]
[Authorize]
[HttpPost]
public async Task<IActionResult> 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);
}
}

View File

@ -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;
/// <summary>
/// Konstruktor für den EnvelopeReceiverController.
/// </summary>
/// <param name="logger">Logger-Instanz zur Protokollierung von Informationen und Fehlern.</param>
/// <param name="envelopeReceiverService">Service zur Verwaltung von Umschlagempfängern.</param>
/// <param name="mediator">Mediator-Instanz zur Verarbeitung von Befehlen und Abfragen.</param>
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator)
/// <param name="mapper"></param>
/// <param name="envelopeExecutor"></param>
/// <param name="erExecutor"></param>
/// <param name="documentExecutor"></param>
/// <param name="csOpt"></param>
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
{
_logger = logger;
_erService = envelopeReceiverService;
_mediator = mediator;
_mapper = mapper;
_envelopeExecutor = envelopeExecutor;
_erExecutor = erExecutor;
_documentExecutor = documentExecutor;
_cnnStr = csOpt.Value.Value;
}
/// <summary>
@ -55,18 +84,23 @@ public class EnvelopeReceiverController : ControllerBase
[HttpGet]
public async Task<IActionResult> 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());
User.GetId(), User.GetUsernameOrDefault(), User.GetNameOrDefault(), User.GetPrenameOrDefault(), User.GetEmailOrDefault());
return StatusCode(StatusCodes.Status500InternalServerError);
}
return await _erService.ReadByUsernameAsync(username: username).ThenAsync(
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<int>())
.ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
@ -74,12 +108,6 @@ public class EnvelopeReceiverController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, msg);
});
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred. {message}", ex.Message);
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
@ -97,8 +125,9 @@ public class EnvelopeReceiverController : ControllerBase
[HttpGet("salute")]
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiverName)
{
try
{
if (receiverName.EmailAddress is null)
return BadRequest();
return await _erService.ReadLastUsedReceiverNameByMail(receiverName.EmailAddress).ThenAsync(
Success: res => res is null ? Ok(string.Empty) : Ok(res),
Fail: IActionResult (msg, ntc) =>
@ -110,18 +139,11 @@ public class EnvelopeReceiverController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError);
});
}
catch (Exception ex)
{
_logger.LogError(ex, "{message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
/// </summary>
/// <param name="createEnvelopeQuery"></param>
/// <param name="cancellationToken">Token to cancel the operation</param>
/// <param name="request"></param>
/// <returns>HTTP-Antwort</returns>
/// <remarks>
/// Sample request:
@ -157,9 +179,133 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand createEnvelopeQuery, CancellationToken cancellationToken)
public async Task<IActionResult> 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<EnvelopeReceiver> sentReceivers = new();
List<ReceiverGetOrCreateCommand> 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<CreateEnvelopeReceiverResponse>(envelope);
res.UnsentReceivers = unsentReceivers;
res.SentReceiver = _mapper.Map<List<ReceiverReadDto>>(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<Signature>())
{
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);
}
/// <summary>
///
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static bool IsBase64String(string input)
{
if (string.IsNullOrWhiteSpace(input))
return false;
try
{
Convert.FromBase64String(input);
return true;
}
catch (FormatException)
{
return false;
}
}
}

View File

@ -20,8 +20,6 @@ public class EnvelopeTypeController : ControllerBase
[HttpGet]
public async Task<IActionResult> GetAllAsync()
{
try
{
return await _service.ReadAllAsync().ThenAsync(
Success: Ok,
@ -31,10 +29,4 @@ public class EnvelopeTypeController : ControllerBase
return ntc.HasFlag(Flag.NotFound) ? NotFound() : StatusCode(StatusCodes.Status500InternalServerError);
});
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}

View File

@ -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;
/// <summary>
/// Konstruktor für den HistoryController.
/// </summary>
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
/// <param name="service">Der Dienst, der für die Verarbeitung der Umschlaghistorie verantwortlich ist.</param>
public HistoryController(ILogger<HistoryController> logger, IEnvelopeHistoryService service)
public HistoryController(ILogger<HistoryController> logger, IEnvelopeHistoryService service, IMemoryCache memoryCache)
{
_logger = logger;
_service = service;
_memoryCache = memoryCache;
}
/// <summary>
@ -47,19 +52,9 @@ public class HistoryController : ControllerBase
/// <response code="200"></response>
[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<ReferenceType>()
.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<ReferenceType>()) : Ok(referenceType.ToString());
}
/// <summary>
@ -91,7 +86,7 @@ public class HistoryController : ControllerBase
/// 3004: MessageDeletionSent
/// 3005: MessageCompletionSent
/// </summary>
/// <param name="related">
/// <param name="status">
/// 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
/// <response code="200"></response>
[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<EnvelopeStatus>()
.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<EnvelopeStatus>()) : Ok(status.ToString());
}
/// <summary>
@ -131,6 +116,8 @@ public class HistoryController : ControllerBase
[Authorize]
public async Task<IActionResult> GetAllAsync([FromQuery] ReadHistoryQuery history)
{
bool withReceiver = false;
bool withSender = false;

View File

@ -38,25 +38,24 @@ public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverS
[HttpGet]
public async Task<IActionResult> 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
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) =>
{
_logger.LogNotice(ntc);
return StatusCode(StatusCodes.Status500InternalServerError);
return NotFound();
});
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
#region REMOVED ENDPOINTS
/// <summary>

View File

@ -10,9 +10,9 @@
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>EnvelopeGenerator.GeneratorAPI</Product>
<Version>1.2.0</Version>
<FileVersion>1.2.0</FileVersion>
<AssemblyVersion>1.2.0</AssemblyVersion>
<Version>1.2.2</Version>
<FileVersion>1.2.2</FileVersion>
<AssemblyVersion>1.2.2</AssemblyVersion>
<PackageOutputPath>Copyright © 2025 Digital Data GmbH. All rights reserved.</PackageOutputPath>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the request pipeline.</param>
/// <param name="logger">The logger instance for logging exceptions.</param>
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Invokes the middleware to handle the HTTP request.
/// </summary>
/// <param name="context">The HTTP context of the current request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context); // Continue down the pipeline
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex, _logger);
}
}
/// <summary>
/// Handles exceptions by logging them and writing an appropriate JSON response.
/// </summary>
/// <param name="context">The HTTP context of the current request.</param>
/// <param name="exception">The exception that occurred.</param>
/// <param name="logger">The logger instance for logging the exception.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
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
}));
}
}

View File

@ -0,0 +1,12 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
/// <summary>
///
/// </summary>
public class ConnectionString
{
/// <summary>
///
/// </summary>
public string Value { get; set; } = string.Empty;
}

View File

@ -6,8 +6,8 @@ namespace EnvelopeGenerator.GeneratorAPI.Models;
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.
/// </summary>
/// <param name="Password">Das erforderliche Passwort für das Login.</param>
/// <param name="Id">Die optionale ID des Benutzers.</param>
/// <param name="UserId">Die optionale ID des Benutzers.</param>
/// <param name="Username">Der optionale Benutzername.</param>
public record Login([Required] string Password, int? Id = null, string? Username = null)
public record Login([Required] string Password, int? UserId = null, string? Username = null)
{
}

View File

@ -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<ConnectionString>(cs => cs.Value = connStr);
builder.Services.AddDbContext<EGDbContext>(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<ExceptionHandlingMiddleware>();
app.MapOpenApi();
// Configure the HTTP request pipeline.

View File

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

View File

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

View File

@ -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.
/// </remarks>
public static IServiceCollection AddEnvelopeGeneratorRepositories(this IServiceCollection services, Action<DbContextOptionsBuilder>? dbContextOptions = null)
public static IServiceCollection AddEnvelopeGeneratorInfrastructureServices(this IServiceCollection services,
Action<DbContextOptionsBuilder>? dbContextOptions = null,
IConfiguration? sqlExecutorConfiguration = null,
Action<SQLExecutorParams>? sqlExecutorConfigureOptions = null)
{
if(dbContextOptions is not null)
services.AddDbContext<EGDbContext>(dbContextOptions);
@ -59,6 +69,92 @@ public static class DIExtensions
services.AddDbRepository<EGDbContext, UserReceiver>(context => context.UserReceivers).UseAutoMapper();
services.AddDbRepository<EGDbContext, EnvelopeReceiverReadOnly>(context => context.EnvelopeReceiverReadOnlys).UseAutoMapper();
services.AddSQLExecutor<Envelope>();
services.AddSQLExecutor<Receiver>();
services.AddSQLExecutor<EnvelopeDocument>();
services.AddSQLExecutor<DocumentReceiverElement>();
services.AddSQLExecutor<DocumentStatus>();
SetDapperTypeMap<Envelope>();
SetDapperTypeMap<User>();
SetDapperTypeMap<Receiver>();
SetDapperTypeMap<EnvelopeDocument>();
SetDapperTypeMap<DocumentReceiverElement>();
SetDapperTypeMap<DocumentStatus>();
services.AddScoped<IEnvelopeExecutor, EnvelopeExecutor>();
services.AddScoped<IEnvelopeReceiverExecutor, EnvelopeReceiverExecutor>();
services.AddScoped<IDocumentExecutor, DocumentExecutor>();
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<SQLExecutorParams>? 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<SQLExecutorParams>(configuration);
if(configureOptions is not null)
services.Configure(configureOptions);
return services.AddSingleton<ISQLExecutor, SQLExecutor>();
}
private static void SetDapperTypeMap<TModel>()
{
Dapper.SqlMapper.SetTypeMap(typeof(TModel), new CustomPropertyTypeMap(
typeof(TModel),
(type, columnName) =>
{
return type.GetProperties().FirstOrDefault(prop =>
{
var attr = prop.GetCustomAttribute<ColumnAttribute>();
return attr != null && string.Equals(attr.Name, columnName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(prop.Name, columnName, StringComparison.OrdinalIgnoreCase);
})!;
}
));
}
public static IServiceCollection AddSQLExecutor<T>(this IServiceCollection services, IConfiguration? configuration = null, Action<SQLExecutorParams>? 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<SQLExecutorParams>(configuration);
services.AddScoped<ISQLExecutor<T>, SQLExecutor<T>>();
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<Type>(); }
})
.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;
}
}

View File

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.0.4" />
<PackageReference Include="DigitalData.Core.Infrastructure.AutoMapper" Version="1.0.2" />

View File

@ -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<SQLExecutorParams> sqlExecutorParamsOptions) : base(provider, sqlExecutorParamsOptions)
{
}
public async Task<EnvelopeDocument> CreateDocumentAsync(string base64, string envelope_uuid, CancellationToken cancellation = default)
{
using var connection = new SqlConnection(Params.ConnectionString);
var sql = Provider.GetRequiredService<DocumentCreateReadSQL>();
var formattedSql = string.Format(sql.Raw, envelope_uuid.ToSqlParam());
var param = DocumentCreateReadSQL.CreateParmas(base64);
await connection.OpenAsync(cancellation);
var documents = await connection.QueryAsync<EnvelopeDocument>(formattedSql, param);
return documents.FirstOrDefault()
?? throw new InvalidOperationException($"Document creation failed. Parameters:" +
$"base64={base64}, envelope_uuid='{envelope_uuid}'.");
}
}

View File

@ -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<SQLExecutorParams> sqlExecutorParamsOptions, IUserRepository userRepository) : base(provider, sqlExecutorParamsOptions)
{
_userRepository = userRepository;
}
public async Task<Envelope> CreateEnvelopeAsync(int userId, string title = "", string message = "", bool tfaEnabled = false, CancellationToken cancellation = default)
{
using var connection = new SqlConnection(Params.ConnectionString);
var sql = Provider.GetRequiredService<EnvelopeCreateReadSQL>();
var formattedSql = string.Format(sql.Raw, userId.ToSqlParam(), title.ToSqlParam(), tfaEnabled.ToSqlParam(), message.ToSqlParam());
await connection.OpenAsync(cancellation);
var envelopes = await connection.QueryAsync<Envelope>(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;
}
}

View File

@ -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<SQLExecutorParams> sqlExecutorParamsOptions, IEnvelopeReceiverRepository erRepository) : base(provider, sqlExecutorParamsOptions)
{
_erRepository = erRepository;
}
public async Task<EnvelopeReceiver?> 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<EnvelopeReceiverAddReadSQL>();
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);
}
}

View File

@ -0,0 +1,41 @@
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Infrastructure.Executor;
public sealed record Query<TEntity> : IQuery<TEntity>
{
private readonly IQueryable<TEntity> _query;
internal Query(IQueryable<TEntity> queryable)
{
_query = queryable;
}
public TEntity First() => _query.First();
public Task<TEntity> FirstAsync() => _query.FirstAsync();
public TEntity? FirstOrDefault() => _query.FirstOrDefault();
public Task<TEntity?> FirstOrDefaultAsync() => _query.FirstOrDefaultAsync();
public TEntity Single() => _query.Single();
public Task<TEntity> SingleAsync() => _query.SingleAsync();
public TEntity? SingleOrDefault() => _query.SingleOrDefault();
public Task<TEntity?> SingleOrDefaultAsync() => _query.SingleOrDefaultAsync();
public IEnumerable<TEntity> ToList() => _query.ToList();
public async Task<IEnumerable<TEntity>> ToListAsync() => await _query.ToListAsync();
}

View File

@ -0,0 +1,9 @@
namespace EnvelopeGenerator.Infrastructure.Executor;
public static class QueryExtension
{
public static Query<TEntity> ToQuery<TEntity>(this IQueryable<TEntity> queryable) where TEntity : class
{
return new Query<TEntity>(queryable);
}
}

View File

@ -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<SQLExecutorParams> sqlExecutorParamsOptions)
{
Provider = provider;
Params = sqlExecutorParamsOptions.Value;
}
public async Task<IEnumerable<TEntity>> Execute<TEntity>(string sql, DynamicParameters parameters, CancellationToken cancellation = default)
{
using var connection = new SqlConnection(Params.ConnectionString);
await connection.OpenAsync(cancellation);
return await connection.QueryAsync<TEntity>(sql, parameters);
}
public Task<IEnumerable<TEntity>> Execute<TEntity, TSQL>(DynamicParameters parameters, CancellationToken cancellation = default) where TSQL : ISQL
{
var sql = Provider.GetRequiredService<TSQL>();
return Execute<TEntity>(sql.Raw, parameters, cancellation);
}
}

View File

@ -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<T> : SQLExecutor, ISQLExecutor<T> where T : class
{
private readonly EGDbContext _context;
private readonly IServiceProvider _provider;
public SQLExecutor(EGDbContext context, IServiceProvider provider, IOptions<SQLExecutorParams> options) : base(provider, options)
{
_context = context;
_provider = provider;
}
public IQuery<T> Execute(string sql, CancellationToken cancellation = default, params object[] parameters)
=> _context
.Set<T>()
.FromSqlRaw(sql, parameters)
.ToQuery();
public IQuery<T> Execute<TSQL>(CancellationToken cancellation = default, params object[] parameters) where TSQL : ISQL<T>
{
var sql = _provider.GetRequiredService<TSQL>();
return Execute(sql.Raw);
}
}

View File

@ -0,0 +1,6 @@
namespace EnvelopeGenerator.Infrastructure.Executor;
public class SQLExecutorParams
{
public string? ConnectionString { get; set; }
}

View File

@ -13,7 +13,7 @@ public class EnvelopeHistoryRepository : CRUDRepository<EnvelopeHistory, long, E
private IQueryable<EnvelopeHistory> 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);

View File

@ -48,9 +48,19 @@ public class EnvelopeReceiverRepository : CRUDRepository<EnvelopeReceiver, (int
public async Task<int> CountAsync(string uuid, string signature) => await ReadWhere(uuid: uuid, signature: signature).CountAsync();
private IQueryable<EnvelopeReceiver> ReadById(int envelopeId, int receiverId, bool readOnly = true)
private IQueryable<EnvelopeReceiver> 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);
}

View File

@ -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<CommandManager>()
.AddEnvelopeGeneratorRepositories()
.AddEnvelopeGeneratorInfrastructureServices()
.AddEnvelopeGeneratorServices(configuration)
.AddSingleton(sp =>
{

View File

@ -17,7 +17,7 @@ public class Mock
builder.Configuration.AddJsonFile(configPath, optional: true, reloadOnChange: true);
builder.Services
.AddEnvelopeGeneratorRepositories(opt =>
.AddEnvelopeGeneratorInfrastructureServices(opt =>
{
if (useRealDb)
{

View File

@ -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<AnnotationParams>();
builder.Services.AddUserManager<EGDbContext>();
var app = builder.Build();
// Configure the HTTP request pipeline.

View File

@ -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": {