Compare commits

...

53 Commits

Author SHA1 Message Date
Developer 02
2692fee6d2 Enhance envelope creation functionality and configuration
- Updated DependencyInjection to include new command handler.
- Modified CreateEnvelopeReceiverCommand for nullable responses.
- Altered SQL command in EnvelopeReceiverAddReadSQL.
- Refactored EnvelopeReceiverController with new dependencies and improved CreateAsync method.
- Expanded appsettings.json with additional configuration settings.
- Introduced new DTOs for signatures and receivers in CreateEnvelopeReceiverDtos.
2025-05-06 16:11:02 +02:00
Developer 02
cae00d9177 Refactor EnvelopeExecutor to use IUserRepository
Updated the EnvelopeExecutor class to replace the IEnvelopeRepository dependency with IUserRepository. Modified the constructor and adjusted the CreateEnvelopeAsync method to return the envelope with associated user information instead of retrieving it from the repository.
2025-05-06 14:53:23 +02:00
Developer 02
eaa1232490 Refactor envelope receiver command and response handling
- Updated `CreateEnvelopeReceiverCommand` to specify response type for `IRequest`.
- Enhanced `CreateEnvelopeReceiverCommandHandler` to use `AutoMapper` and return `CreateEnvelopeReceiverResponse`.
- Modified `Handle` method to map envelope data and populate recipient lists.
- Changed `CreateEnvelopeReceiverResponse` to inherit from `CreateEnvelopeResponse` and added new properties.
- Adjusted mapping profile to map `Envelope` to `CreateEnvelopeReceiverResponse`.
- Created new mapping profile for `Receiver` to `ReceiverReadDto`.
2025-05-06 12:32:54 +02:00
Developer 02
8c2550ff1d Refactor envelope and receiver methods for clarity
- Changed return type of `CreateEnvelopeAsync` to ensure a non-null `Envelope` is always returned.
- Updated `AddEnvelopeReceiverAsync` to allow nullable `salutation` parameter.
- Renamed cancellation token parameter in `CreateEnvelopeReceiverCommandHandler` for consistency.
- Enhanced error handling in `CreateEnvelopeAsync` to throw exceptions on failure with improved messages.
- Adjusted `CreateParameters` method to accept nullable `salutation`.
2025-05-06 12:05:24 +02:00
Developer 02
b5b9155bc0 Refactor envelope creation methods and parameter handling
- Updated `Extension.cs` to remove old methods and introduce a new parameter creation method.
- Modified `IEnvelopeExecutor.cs` to change `CreateEnvelopeAsync` signature for clarity.
- Added a consistent `CreateParmas` method in `EnvelopeCreateReadSQL.cs`.
- Changed service registration in `DependencyExtensions.cs` from singleton to scoped.
- Revised `CreateEnvelopeAsync` in `EnvelopeExecutor.cs` to utilize the new parameter structure and removed user addition logic.

These changes enhance code clarity, maintainability, and consistency in parameter handling.
2025-05-06 11:24:14 +02:00
Developer 02
3cc8e2b5db Add envelope executors to CreateEnvelopeReceiver handler
This commit introduces two new dependencies: `IEnvelopeExecutor` and `IEnvelopeReceiverExecutor`. The `using` directive for `IEnvelopeExecutor` has been added, and a new private field `_erExecutor` has been introduced in the `CreateEnvelopeReceiverCommandHandler` class. A constructor has also been added to initialize both `_envelopeExecutor` and `_erExecutor`, enabling the handler to effectively process envelope-related commands.
2025-05-06 11:06:47 +02:00
Developer 02
37032a48c9 Enhance envelope receiver functionality and documentation
Updated IEnvelopeReceiverExecutor with XML comments for
AddEnvelopeReceiverAsync method and added necessary using
directives. Introduced XML documentation for CreateParameters
in EnvelopeReceiverAddReadSQL. Modified
AddEnvelopeReceiverAsync in EnvelopeReceiverExecutor to
accept individual parameters, improving clarity and usability.
2025-05-06 10:45:54 +02:00
Developer 02
82fc7fa762 Refactor EnvelopeReceiverExecutor and Repository methods
- Updated `EnvelopeReceiverExecutor` to inherit from `SQLExecutor` and added `_erRepository` for enhanced data access.
- Introduced `AddEnvelopeReceiverAsync` method for retrieving envelope receivers.
- Modified `ReadById` in `EnvelopeReceiverRepository` to support flexible querying with new parameters.
- Updated `ReadByIdAsync` to align with changes in `ReadById`.
2025-05-06 10:33:17 +02:00
Developer 02
749366fff5 Refactor DI for Envelope services and add new executor
Updated DIExtensions to register IEnvelopeExecutor as a singleton and added IEnvelopeReceiverExecutor as a new singleton service. Introduced IEnvelopeReceiverExecutor interface and implemented it in the new EnvelopeReceiverExecutor class for future envelope processing functionality.
2025-05-06 09:59:18 +02:00
Developer 02
40dc0ecda3 Add CreateEnvelopeReceiverResponse record
Introduces the `CreateEnvelopeReceiverResponse` record in the
`EnvelopeGenerator.Application.EnvelopeReceivers.Commands.Create`
namespace. This record inherits from `ReadEnvelopeReceiverQuery`
and includes an optional `Status` parameter of type
`EnvelopeStatusQuery`. Also adds necessary using directives.
2025-05-06 09:56:35 +02:00
Developer 02
7b7a4b4f65 Add AddEnvelopeReceiver method and SQL class
Introduces the `AddEnvelopeReceiver` method in the `Extension` class to add envelope receivers with parameters for UUID, email, salutation, and optional phone.

Also adds the `EnvelopeReceiverAddReadSQL` class implementing `ISQL<Envelope>`, containing a SQL query to create a new receiver using the stored procedure `PRSIG_API_CREATE_RECEIVER`.

Includes XML documentation for improved code readability and maintainability.
2025-05-06 09:50:20 +02:00
Developer 02
b609253893 Revert "Refactor envelope handling to use ISQLExecutor"
This reverts commit 4cabaf3191.
2025-05-06 09:45:38 +02:00
Developer 02
b0eb1b6389 Revert "Add AddReceiver method and refactor SQL classes"
This reverts commit 1b515ea904.
2025-05-06 09:44:15 +02:00
Developer 02
1b515ea904 Add AddReceiver method and refactor SQL classes
Introduced a new `AddReceiver` method in the `Extension` class to facilitate adding a receiver to an envelope. This method includes parameters for `envelope_uuid`, `emailAdress`, `salutation`, and an optional `phone`, along with XML documentation for clarity.

Removed the `EnvelopeReceiverCreateReadSQL` class and added the `EnvelopeReceiverAddReadSQL` class, which defines the SQL command for adding a receiver. The new class also includes XML documentation comments for better understanding.
2025-05-06 01:41:42 +02:00
Developer 02
4cabaf3191 Refactor envelope handling to use ISQLExecutor
This commit removes the IEnvelopeExecutor interface and its implementation, replacing it with ISQLExecutor. The CreateEnvelopeAsync method in Extension.cs now directly creates DynamicParameters. The CreateEnvelopeCommandHandler has been updated to utilize ISQLExecutor. Additionally, the EnvelopeExecutor class has been removed, and a new SQL command class, EnvelopeReceiverCreateReadSQL, has been added for managing envelope receiver SQL operations.
2025-05-06 01:30:59 +02:00
Developer 02
8cfa28a863 Refactor envelope creation logic into Extension class
Added a new static class `Extension` in `Extension.cs` to encapsulate methods for creating parameters and asynchronous envelope creation. Removed previous implementations from `EnvelopeCreateReadSQL.cs` for better organization. Updated `using` directives in `CreateEnvelopeCommandHandler.cs` and `EnvelopeCreateReadSQL.cs` for consistency.
2025-05-05 16:36:16 +02:00
Developer 02
3955a3232d Refactor envelope creation SQL logic
Removed `CreateEnvelopeSQL` and introduced `EnvelopeCreateReadSQL` to handle envelope creation logic. Updated the `Extension` class methods and modified `CreateEnvelopeAsync` in `EnvelopeExecutor` to utilize the new SQL class.
2025-05-05 16:30:39 +02:00
Developer 02
b93ba6be17 Refactor envelope creation logic and add IEnvelopeExecutor
Updated CreateEnvelopeCommandHandler to use IEnvelopeExecutor for managing envelope creation. Introduced CreateParmas method for parameter handling in CreateEnvelopeSQL. Corrected Dapper type mapping method name and added service registration for IEnvelopeExecutor in DependencyExtensions. Refactored SQLExecutor to enhance encapsulation. Created EnvelopeExecutor class implementing IEnvelopeExecutor for dedicated envelope operations.
2025-05-05 16:11:29 +02:00
Developer 02
39ff4b8867 Refactor envelope creation logic and SQL execution
- Changed `_sqlExecutor` type in `CreateEnvelopeCommandHandler` to non-generic `ISQLExecutor`.
- Updated `Handle` method to use `CreateEnvelopeAsync` for simplified parameter handling.
- Restructured `CreateEnvelopeSQL` to implement `ISQL<Envelope>` with a raw SQL command for creating envelopes.
- Added `SetDappeTypeMap<TModel>` method in `DependencyExtensions` for Dapper type mappings.
- Improved overall code structure for better separation of concerns and maintainability.
2025-05-05 13:53:31 +02:00
Developer 02
7b7aba6efd Refactor SQL execution classes into new namespace
Restructure and refactor classes related to SQL execution within the `EnvelopeGenerator.Infrastructure` namespace. Key changes include:
- Added `EnvelopeGenerator.Infrastructure.Executor` namespace.
- Moved and redefined `Query`, `QueryExtension`, `SQLExecutor`, `SQLExecutorBaseEntity`, and `SQLExecutorParams` classes to the new namespace.
- Maintained existing functionality while improving code organization and clarity.
2025-05-05 10:54:09 +02:00
Developer 02
a42e4287ff Refactor envelope generator service dependencies
This commit replaces the `AddEnvelopeGeneratorRepositories` method with `AddEnvelopeGeneratorInfrastructureServices`, allowing for more flexible configuration through `IConfiguration` and `Action<SQLExecutorParams>`. The `SQLExecutor` class now utilizes `SQLExecutorParams` for its connection string, enhancing configurability. A new `SQLExecutorParams` class has been introduced to encapsulate connection string management. Various service registration calls have been updated to integrate the new infrastructure services, improving modularity and ease of database connection management.
2025-05-05 10:47:00 +02:00
Developer 02
a757749767 Refactor SQL execution and enhance envelope creation
- Updated `ISQLExecutor<TEntity>` to inherit from new `ISQLExecutor` interface for improved SQL execution flexibility.
- Added package references for `Dapper` and `DigitalData.Core` in project files.
- Modified `CreateEnvelopeCommand` to include `[BindNever]` on `UserId` for better model binding control.
- Refactored `CreateEnvelopeCommandHandler` to use `DynamicParameters` for SQL parameter handling.
- Updated `CreateEnvelopeSQL` to select only the top record for performance.
- Introduced `GetIdOrDefault` method in `ControllerExtensions` for user ID retrieval with fallback.
- Added `CreateAsync` method in `EnvelopeController` for envelope creation using `IMediator`.
- Ensured infrastructure project has necessary package references.
- Refactored `SQLExecutor` to implement new interface and simplified constructor.
- Introduced `SQLExecutorBaseEntity` for entity-specific SQL command execution.
2025-05-05 10:15:36 +02:00
Developer 02
5166f41941 Refactor CreateEnvelopeResponse to use record type
Changed CreateEnvelopeResponse from a class to a record type for improved immutability and conciseness. The new definition inherits from ReadEnvelopeResponse, reusing its functionality. Added XML documentation comments for better clarity on each parameter.
2025-05-05 02:06:33 +02:00
Developer 02
928f2a7780 Add AutoMapper profile for envelope mapping
Introduces `CreateEnvelopeMappingProfile` class to define
mapping between `Envelope` entity and `CreateEnvelopeResponse`
DTO using AutoMapper's `CreateMap` method.
2025-05-05 02:03:56 +02:00
Developer 02
d46aa6e2b8 Add envelope creation functionality and SQL integration
- Updated `EnvelopeGenerator.Application.csproj` to include `Microsoft.Data.SqlClient` package.
- Refactored `CreateEnvelopeReceiverCommand` to inherit from `CreateEnvelopeCommand`.
- Enhanced `CreateEnvelopeSQL` with a SQL script for envelope creation.
- Introduced `CreateEnvelopeCommand` to encapsulate envelope creation data.
- Added `CreateEnvelopeCommandHandler` to process commands and interact with the database.
- Created `CreateEnvelopeResponse` class for handling responses from envelope creation.
2025-05-05 02:01:01 +02:00
Developer 02
7cffc3f7bc fix: Change SQLExecutor service registration to scoped
Updated the `AddSQLExecutor<T>` method in the `DIExtensions` class to register `ISQLExecutor<T>` as a scoped service instead of a singleton. This ensures that a new instance of `SQLExecutor<T>` is created for each request within the same scope, improving resource management and request isolation.
2025-05-05 00:36:00 +02:00
Developer 02
841da3c261 Refactor DI extensions and add SQL executor support
Removed `DIExtensions.cs` and integrated its content into `DependencyExtensions.cs`, enhancing the dependency injection setup with repository and SQL executor registrations. Added a new `CreateEnvelopeSQL` class implementing the `ISQL<Envelope>` interface, currently with a placeholder implementation.
2025-05-05 00:31:58 +02:00
Developer 02
adbfd69418 feat(IQueryExecutor): IQuery umbenennen 2025-04-30 16:49:26 +02:00
Developer 02
1e54b775a2 refactor: Vereinfachung der SQLExecutor-Implementierung zur Rückgabe von IQueryExecutor
- Aktualisiert `SQLExecutor<T>` um `ISQLExecutor<T>` zu implementieren und `IQueryExecutor<T>` für die weitere Abfrageausführung zurückzugeben.
- Umstrukturierte Methoden zur Verwendung von `ToExecutor()` für die Konvertierung von rohen SQL-Abfragen in einen `IQueryExecutor<T>`.
- Geänderter Konstruktor, um `IServiceProvider` für die Injektion von Abhängigkeiten von benutzerdefinierten SQL-Abfrageklassen zu akzeptieren.
2025-04-29 17:14:38 +02:00
Developer 02
6e82b24578 feat: Implement QueryExecutor for executing queries using IQueryable
- Added a record `QueryExecutor<TEntity>` implementing `IQueryExecutor<TEntity>` for executing common query operations like First, Single, and ToList both synchronously and asynchronously.
- Methods leverage the IQueryable interface to perform database queries for entity types.
2025-04-29 16:56:13 +02:00
Developer 02
5331efe3c1 feat(sql): Hinzufügen generischer Überladungen zu SQLExecutor unter Verwendung von ISQL<T> über DI
Es wurden Überladungen für ExecuteFirstAsync, ExecuteSingleAsync und ExecuteAllAsync
eingeführt, die SQL-Definitionen aus dem Dependency Injection Container mittels ISQL<T> auflösen.
Außerdem wurde ein Fehler bei der Verwendung der Direktive von .cs zu standardmäßigem Schnittstellenimport korrigiert.
2025-04-29 16:39:18 +02:00
Developer 02
3b4ad2960a feat: SQLExecutor-Klasse für die Ausführung von Roh-SQL-Abfragen mit Entity Framework Core hinzufügen 2025-04-29 16:26:26 +02:00
Developer 02
33048e185b feat(CreateEnvelopeReceiverCommandHandler): initialized 2025-04-29 14:41:21 +02:00
Developer 02
3ae1b94eb7 refactor(Login): Id in UserId umbenennen 2025-04-29 11:39:21 +02:00
Developer 02
3d1966a715 feat(Jenkinsfile): Als Beispiel erstellt 2025-04-29 09:34:57 +02:00
Developer 02
c173814b8d refactor(AuthController): Login-Methoden aktualisieren, um NotImplementedException zu werfen, nur um Swagger-Dokumentation bereitzustellen 2025-04-29 09:21:05 +02:00
Developer 02
a85397a363 refactor(auth): Refactoring der Login-Methoden für OpenAPI-Kompatibilität
– Die Login-Methoden wurden überarbeitet, um NotImplementedException auszulösen und OpenAPI (Swagger) und Skalar zu konfigurieren.
– Unnötige using-Anweisungen wurden entfernt, um den Code zu optimieren.
2025-04-28 16:49:46 +02:00
Developer 02
2d3987b81e Add JWT Bearer authentication support
- Integrated JWT Bearer authentication for API security.
- Replaced previous CookieAuthenticationDefaults with JwtBearerDefaults as the default authentication scheme.
- Configured JWT token validation with issuer, audience, and signing key parameters.
- Added handling for token retrieval from cookies or query strings when missing in the header.
- Updated the authentication configuration to support both Cookie and JWT authentication schemes.
- Enhanced security by validating JWT tokens against provided public keys.
2025-04-28 16:18:31 +02:00
Developer 02
875ff95278 feat(AuthTokenKeys): Erstellt, um Parameter für Authentifizierungs-Tokens zu konfigurieren.
- AuthTokenKeys aus der Konfiguration in Program.cs lesen.
 - Upgrade von Core.Abstractions auf 3.5
2025-04-28 15:49:40 +02:00
Developer 02
dcdf0844cb feat(Program): DeferredServiceProvider-Instanz erstellen und Fabrik einstellen 2025-04-28 15:08:07 +02:00
Developer 02
27e9de4709 chore(Core.Abstraction): Aktualisiert auf 3.4.4 2025-04-28 15:02:49 +02:00
Developer 02
e2bdc73b76 chore: auth-hub-client hinzufügen und konfigurieren 2025-04-28 14:15:45 +02:00
Developer 02
7abb3a6c8a chore(deps): NuGet-Paketversionen aktualisieren und DigitalData.Auth.Client hinzufügen 2025-04-28 10:47:55 +02:00
Developer 02
9183ba4da5 Merge branch 'feat/terminal' 2025-04-28 09:19:52 +02:00
Developer 02
08e2e91e9a feat(program): Konfiguration aus appsettings.json laden
Die Anwendung lädt nun Konfigurationseinstellungen aus einer "appsettings.json"-Datei im Basisverzeichnis.
Dies ermöglicht eine externe Konfiguration ohne Codeänderungen und unterstützt das Neuladen der Einstellungen zur Laufzeit bei Änderungen der Datei.
2025-04-28 09:16:24 +02:00
Developer 02
2966d64455 feat(terminal): ReadDocument-Befehl um PDF-Speicheroptionen erweitert
Optionen `save`, `dir` und `fileName` zum `ReadDocument`-Befehl hinzugefügt.
Wenn `save` aktiviert ist, wird das PDF an dem angegebenen oder dem Standardpfad gespeichert.
Ermöglicht dem Benutzer mehr Kontrolle über Speicherort und Dateinamen.
2025-04-25 19:33:29 +02:00
Developer 02
41cb2c2d93 fix(PDFBurnerParams): Verschieben in das Verzeichnis /FinalizeDocument 2025-04-25 11:39:59 +02:00
Developer 02
edd54ab302 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2025-04-24 16:08:48 +02:00
Developer 02
51920089af feat(PDFBurner): PDFBurnerParams hinzufügen und Konfiguration binden 2025-04-24 16:08:05 +02:00
Developer01
5714c54385 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2025-04-24 16:00:29 +02:00
Developer01
09d2640345 V 2.9.0.0 WISAG 2025-04-24 16:00:16 +02:00
Developer 02
82cb50b7e1 feat(PDFBurnerParams): erstellt, um PDFBurner zu konfigurieren 2025-04-24 15:50:08 +02:00
Developer 02
ff36dc47c1 refactor(PDFBurner): Logik hinzufügen, um den Unterschied zwischen FormField-Annotationen zu verringern 2025-04-24 15:29:22 +02:00
76 changed files with 1666 additions and 344 deletions

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

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

@@ -3,23 +3,8 @@
/// <summary>
/// Represents the response for reading a document.
/// </summary>
public class ReadDocumentResponse
public class ReadDocumentResponse : ReadDocumentResponseBase
{
/// <summary>
/// The unique identifier of the document.
/// </summary>
public int Guid { get; init; }
/// <summary>
/// The identifier of the associated envelope.
/// </summary>
public int EnvelopeId { get; init; }
/// <summary>
/// The date and time when the document was added.
/// </summary>
public DateTime AddedWhen { get; init; }
/// <summary>
/// The binary data of the document, if available.
/// </summary>

View File

@@ -0,0 +1,22 @@
namespace EnvelopeGenerator.Application.Documents.Queries.Read;
/// <summary>
/// Represents the response for reading a document.
/// </summary>
public class ReadDocumentResponseBase
{
/// <summary>
/// The unique identifier of the document.
/// </summary>
public int Guid { get; init; }
/// <summary>
/// The identifier of the associated envelope.
/// </summary>
public int EnvelopeId { get; init; }
/// <summary>
/// The date and time when the document was added.
/// </summary>
public DateTime AddedWhen { get; init; }
}

View File

@@ -13,13 +13,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.3" />
<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" />
<PackageReference Include="DigitalData.Core.DTO" Version="2.0.1" />
<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" />
@@ -80,4 +82,8 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Procedures\" />
</ItemGroup>
</Project>

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.UnsentRecipients = unsentRecipients;
res.SentRecipients = _mapper.Map<IEnumerable<ReceiverReadDto>>(sentRecipients);
return res;
}
}

View File

@@ -0,0 +1,46 @@
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] 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 = "h.tek@digitaldata.works";
/// <summary>
/// E-Mail-Adresse des Empfängers.
/// </summary>
public string? EmailAddress { get => _emailAddress?.ToLower(); init => _emailAddress = _emailAddress?.ToLower() ?? "h.tek@digitaldata.works"; }
};
/// <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

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> SentRecipients { get; set; } = new List<ReceiverReadDto>();
/// <summary>
///
/// </summary>
public IEnumerable<ReceiverGetOrCreateCommand> UnsentRecipients { 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

@@ -0,0 +1,48 @@
using Dapper;
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.SQL;
/// <summary>
///
/// </summary>
public class EnvelopeCreateReadSQL : ISQL<Envelope>
{
/// <summary>
///
/// </summary>
public string Raw => @"
USE [DD_ECM];
DECLARE @OUT_UID varchar(36);
EXEC [dbo].[PRSIG_API_CREATE_ENVELOPE]
@USER_ID = @UserId,
@TITLE = @Title,
@TFAEnabled = @TfaEnabled,
@MESSAGE = @Message,
@OUT_UID = @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 CreateParmas(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);
return parameters;
}
}

View File

@@ -0,0 +1,49 @@
using Dapper;
using EnvelopeGenerator.Application.Contracts.SQLExecutor;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.SQL;
/// <summary>
///
/// </summary>
public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
{
/// <summary>
///
/// </summary>
public string Raw => @"
USE [DD_ECM]
DECLARE @OUT_RECEIVER_ID int
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
@ENV_UID = @ENV_UID,
@EMAIL_ADRESS = @EMAIL_ADRESS ,
@SALUTATION = @SALUTATION,
@PHONE = @PHONE,
@OUT_RECEIVER_ID = @OUT_RECEIVER_ID OUTPUT
SELECT TOP(1) *
FROM 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

@@ -4,6 +4,7 @@ Imports GdPicture14
Imports Newtonsoft.Json.Linq
Imports EnvelopeGenerator.Common.Jobs
Imports System.IO
Imports EnvelopeGenerator.Common.Jobs.FinalizeDocument
Public Class frmFinalizePDF
Private Const CONNECTIONSTRING = "Server=sDD-VMP04-SQL17\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=+bk8oAbbQP1AzoHtvZUbd+Mbok2f8Fl4miEx1qssJ5yEaEWoQJ9prg4L14fURpPnqi1WMNs9fE4=;"
@@ -15,14 +16,14 @@ Public Class frmFinalizePDF
Private Manager As AnnotationManager
Private PDFBurner As FinalizeDocument.PDFBurner
Private pGDPictureLicenseKey As String = "kG1Qf9PwmqgR8aDmIW2zI_ebj48RzqAJegRxcystEmkbTGQqfkNBdFOXIb6C_A00Ra8zZkrHdfjqzOPXK7kgkF2YDhvrqKfqh4WDug2vOt0qO31IommzkANSuLjZ4zmraoubyEVd25rE3veQ2h_j7tGIoH_LyIHmy24GaXsxdG0yCzIBMdiLbMMMDwcPY-809KeZ83Grv76OVhFvcbBWyYc251vou1N-kGg5_ZlHDgfWoY85gTLRxafjD3KS_i9ARW4BMiy36y8n7UP2jN8kGRnW_04ubpFtfjJqvtsrP_J9D0x7bqV8xtVtT5JI6dpKsVTiMgDCrIcoFSo5gCC1fw9oUopX4TDCkBQttO4-WHBlOeq9dG5Yb0otonVmJKaQA2tP6sMR-lZDs3ql_WI9t91yPWgpssrJUxSHDd27_LMTH_owJIqkF3NOJd9mYQuAv22oNKFYbH8e41pVKb8cT33Y9CgcQ_sy6YDA5PTuIRi67mjKge_nD9rd0IN213Ir9M_EFWqg9e4haWzIdHXQUo0md70kVhPX4UIH_BKJnxEEnFfoFRNMh77bB0N4jkcBEHPl-ghOERv8dOztf4vCnNpzzWvcLD2cqWIm6THy8XGGq9h4hp8aEreRleSMwv9QQAC7mjLwhQ1rBYkpUHlpTjhTLnMwHknl6HH0Z6zzmsgkRKVyfquv94Pd7QbQfZrRka0ss_48pf9p8hAywEn81Q=="
Private ReadOnly _ignoredLabels As New List(Of String) From {"Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung"}
Private ReadOnly _pdfBurnerParams As New PDFBurnerParams()
Private Sub frmFinalizePDF_Load(sender As Object, e As EventArgs) Handles MyBase.Load
LogConfig = New LogConfig(LogConfig.PathType.CustomPath, Application.StartupPath)
Database = New MSSQLServer(LogConfig, MSSQLServer.DecryptConnectionString(CONNECTIONSTRING))
PDFBurner = New FinalizeDocument.PDFBurner(LogConfig, pGDPictureLicenseKey, _ignoredLabels)
PDFBurner = New FinalizeDocument.PDFBurner(LogConfig, pGDPictureLicenseKey, _pdfBurnerParams)
Viewer = New GdViewer()
Manager = New AnnotationManager()

View File

@@ -130,7 +130,7 @@
Public Const DATABASE = "DATABASE"
Public Const LOGCONFIG = "LOGCONFIG"
Public Const GDPICTURE = "GDPICTURE"
Public Const IGNORED_LABELS = "IgnoredLabels"
Public Const PDF_BURNER_PARAMS = "PDFBurnerParams"
Public Const GREEN_300 = "#bbf7d0"
Public Const RED_300 = "#fecaca"

View File

@@ -25,6 +25,7 @@
Public Property Message As String = My.Resources.Envelope.Please_read_and_sign_this_document
Public Property AddedWhen As Date
Public Property ChangedWhen As Date
Public Property User As New User()
Public Property Documents As New List(Of EnvelopeDocument)

View File

@@ -8,6 +8,7 @@
Public Property HasAccess As Boolean
Public Property IsAdmin As Boolean
Public Property GhostModeActive As Boolean
Public ReadOnly Property FullName() As String
Get

View File

@@ -281,6 +281,7 @@
<Compile Include="Entities\ElementStatus.vb" />
<Compile Include="Entities\EmailData.vb" />
<Compile Include="Entities\EmailTemplate.vb" />
<Compile Include="Jobs\FinalizeDocument\PDFBurnerParams.vb" />
<Compile Include="Services\TemplateService.vb" />
<Compile Include="Entities\Envelope.vb" />
<Compile Include="Entities\EnvelopeDocument.vb" />

View File

@@ -74,8 +74,8 @@ Namespace Jobs
InitializeServices(oState)
Logger.Debug("Loading PDFBurner..")
Dim ignoredLabels As List(Of String) = pContext.MergedJobDataMap.Item(Constants.IGNORED_LABELS)
PDFBurner = New PDFBurner(LogConfig, oGdPictureKey, ignoredLabels)
Dim pdfBurnerParams As PDFBurnerParams = pContext.MergedJobDataMap.Item(Constants.PDF_BURNER_PARAMS)
PDFBurner = New PDFBurner(LogConfig, oGdPictureKey, pdfBurnerParams)
Logger.Debug("Loading PDFMerger..")
PDFMerger = New PDFMerger(LogConfig, oGdPictureKey)

View File

@@ -16,9 +16,9 @@ Namespace Jobs.FinalizeDocument
Private Const ANNOTATION_TYPE_IMAGE = "pspdfkit/image"
Private Const ANNOTATION_TYPE_INK = "pspdfkit/ink"
Private Const ANNOTATION_TYPE_WIDGET = "pspdfkit/widget"
Private Property _ignoredLabels As List(Of String)
Private Property _pdfBurnerParams As PDFBurnerParams
Public Sub New(pLogConfig As LogConfig, pGDPictureLicenseKey As String, ignoredLabels As List(Of String))
Public Sub New(pLogConfig As LogConfig, pGDPictureLicenseKey As String, pdfBurnerParams As PDFBurnerParams)
MyBase.New(pLogConfig)
LicenseManager = New LicenseManager()
@@ -26,7 +26,7 @@ Namespace Jobs.FinalizeDocument
Manager = New AnnotationManager()
_ignoredLabels = ignoredLabels
_pdfBurnerParams = pdfBurnerParams
End Sub
Public Function BurnInstantJSONAnnotationsToPDF(pSourceBuffer As Byte(), pInstantJSONList As List(Of String)) As Byte()
@@ -68,7 +68,8 @@ Namespace Jobs.FinalizeDocument
Private Function AddInstantJSONAnnotationToPDF(pInstantJSON As String) As Boolean
Try
Dim oAnnotationData = JsonConvert.DeserializeObject(Of AnnotationData)(pInstantJSON)
oAnnotationData.annotations.Reverse()
Dim formFieldIndex = 0
For Each oAnnotation In oAnnotationData.annotations
Logger.Debug("Adding AnnotationID: " + oAnnotation.id)
Select Case oAnnotation.type
@@ -81,10 +82,12 @@ Namespace Jobs.FinalizeDocument
Case ANNOTATION_TYPE_WIDGET
'Add form field values
Dim formFieldValue = oAnnotationData.formFieldValues.FirstOrDefault(Function(fv) fv.name = oAnnotation.id)
If formFieldValue IsNot Nothing AndAlso Not _ignoredLabels.Contains(formFieldValue.value) Then
AddFormFieldValue(oAnnotation, formFieldValue)
If formFieldValue IsNot Nothing AndAlso Not _pdfBurnerParams.IgnoredLabels.Contains(formFieldValue.value) Then
AddFormFieldValue(oAnnotation, formFieldValue, formFieldIndex)
formFieldIndex += 1
End If
End Select
Next
Return True
@@ -144,13 +147,13 @@ Namespace Jobs.FinalizeDocument
End Function
Private Function AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue) As Boolean
Private Function AddFormFieldValue(pAnnotation As Annotation, formFieldValue As FormFieldValue, index As Integer) As Boolean
Try
' Convert pixels to Inches
Dim oBounds = pAnnotation.bbox.Select(AddressOf ToInches).ToList()
Dim oX = oBounds.Item(0)
Dim oY = oBounds.Item(1)
Dim oY = oBounds.Item(1) + _pdfBurnerParams.YOffset * index + _pdfBurnerParams.TopMargin
Dim oWidth = oBounds.Item(2)
Dim oHeight = oBounds.Item(3)
@@ -159,9 +162,9 @@ Namespace Jobs.FinalizeDocument
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
' Set the font properties
ant.FontName = "Arial"
ant.FontSize = 8
ant.FontStyle = FontStyle.Italic
ant.FontName = _pdfBurnerParams.FontName
ant.FontSize = _pdfBurnerParams.FontSize
ant.FontStyle = _pdfBurnerParams.FontStyle
Manager.SaveAnnotationsToPage()
Return True
Catch ex As Exception

View File

@@ -0,0 +1,16 @@
Imports System.Drawing
Namespace Jobs.FinalizeDocument
Public Class PDFBurnerParams
Public Property IgnoredLabels As New List(Of String) From {"Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung"}
Public Property TopMargin As Double = 0.1
Public Property YOffset As Double = -0.3
Public Property FontName As String = "Arial"
Public Property FontSize As Integer = 8
Public Property FontStyle As FontStyle = FontStyle.Italic
End Class
End Namespace

View File

@@ -35,6 +35,7 @@ Public Class EnvelopeModel
.Language = pRow.ItemEx("LANGUAGE", "de-DE"),
.Status = ObjectEx.ToEnum(Of Constants.EnvelopeStatus)(pRow.ItemEx("STATUS", Constants.EnvelopeStatus.EnvelopeCreated.ToString())),
.AddedWhen = pRow.Item("ADDED_WHEN"),
.ChangedWhen = pRow.Item("CHANGED_WHEN"),
.CertificationType = ObjectEx.ToEnum(Of Constants.CertificationType)(pRow.ItemEx("CERTIFICATION_TYPE", Constants.CertificationType.AdvancedElectronicSignature.ToString())),
.User = New User(),
.ExpiresWhen = pRow.ItemEx(Of Date)("EXPIRES_WHEN", Nothing),

View File

@@ -33,10 +33,10 @@ Public Class UserModel
Dim oRow = oTable.Rows.Item(0)
Dim oHasAccess = oRow.ItemEx("MODULE_ACCESS", False)
Dim oIsAdmin = oRow.ItemEx("IS_ADMIN", False)
Dim oGhostmode = oRow.ItemEx("GHOST_MODE_OVERRIDE", False)
pUser.HasAccess = oHasAccess
pUser.IsAdmin = oIsAdmin
pUser.GhostModeActive = oGhostmode
Return pUser
Catch ex As Exception
Logger.Error(ex)

View File

@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' indem Sie "*" wie unten gezeigt eingeben:
' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.4.3.0")>
<Assembly: AssemblyFileVersion("2.4.3.0")>
<Assembly: AssemblyVersion("2.4.4.0")>
<Assembly: AssemblyFileVersion("2.4.4.0")>

View File

@@ -220,6 +220,9 @@ Pattern: +491234567890</value>
<data name="Missing Receivers" xml:space="preserve">
<value>Fehlende Empfänger</value>
</data>
<data name="ModificationOriginFile_FormFields" xml:space="preserve">
<value />
</data>
<data name="New Envelope" xml:space="preserve">
<value>Neuer Umschlag</value>
</data>

View File

@@ -371,6 +371,15 @@ Namespace My.Resources
End Get
End Property
'''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die ähnelt.
'''</summary>
Public Shared ReadOnly Property ModificationOriginFile_FormFields() As String
Get
Return ResourceManager.GetString("ModificationOriginFile_FormFields", resourceCulture)
End Get
End Property
'''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Neuer Umschlag ähnelt.
'''</summary>

View File

@@ -325,6 +325,15 @@ Namespace My.Resources
End Get
End Property
'''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die ähnelt.
'''</summary>
Public Shared ReadOnly Property ModificationOriginFile_FormFields() As String
Get
Return ResourceManager.GetString("ModificationOriginFile_FormFields", resourceCulture)
End Get
End Property
'''<summary>
''' Sucht eine lokalisierte Zeichenfolge, die Nein ähnelt.
'''</summary>

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.3" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.0.0" />
<PackageReference Include="UserManager.Domain" Version="3.0.2" />
</ItemGroup>

View File

@@ -416,6 +416,7 @@
</EmbeddedResource>
<EmbeddedResource Include="frmGhostMode.resx">
<DependentUpon>frmGhostMode.vb</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="frmMain.en.resx">
<DependentUpon>frmMain.vb</DependentUpon>

View File

@@ -18,6 +18,9 @@ Public Class FlattenFormFields
Dim newFilesPath As String = Path.Combine(oFolder, "InputFieldsFlattend_" & Path.GetFileName(pFilePath))
If gdpicturePdf.SaveToFile(newFilesPath) = GdPictureStatus.OK Then
Dim oNameofFile = Path.GetFileName(newFilesPath)
MsgBox("Your PDF-file contained form-fields!" & vbNewLine & "We needed to adapt the file and created an new version!" & vbNewLine &
$"New filename: {oNameofFile}", MsgBoxStyle.Exclamation, "Information")
Return newFilesPath
End If

View File

@@ -200,7 +200,7 @@ Partial Public Class frmEnvelopeEditor
Private Sub btnSave_ItemClick(sender As Object, e As DevExpress.XtraBars.ItemClickEventArgs) Handles btnSave.ItemClick
Try
If SaveEnvelopeWithOutValidation() = True Then
bsitm_info.Caption = "Data saved succeddfully " + Now.ToString
bsitm_info.Caption = "Data saved successfully " + Now.ToString
Else
bsitm_info.Caption = "Exceprion - Error saving Data. Check LOG"
End If

View File

@@ -47,6 +47,7 @@ Partial Class frmMain
Me.colStatus = New DevExpress.XtraGrid.Columns.GridColumn()
Me.colTitle = New DevExpress.XtraGrid.Columns.GridColumn()
Me.colAddedWhen = New DevExpress.XtraGrid.Columns.GridColumn()
Me.GridColumn2 = New DevExpress.XtraGrid.Columns.GridColumn()
Me.RibbonControl = New DevExpress.XtraBars.Ribbon.RibbonControl()
Me.btnCreateEnvelope = New DevExpress.XtraBars.BarButtonItem()
Me.btnEditEnvelope = New DevExpress.XtraBars.BarButtonItem()
@@ -88,14 +89,16 @@ Partial Class frmMain
Me.GridColumn4 = New DevExpress.XtraGrid.Columns.GridColumn()
Me.GridColumn5 = New DevExpress.XtraGrid.Columns.GridColumn()
Me.GridColumn7 = New DevExpress.XtraGrid.Columns.GridColumn()
Me.GridColumn1 = New DevExpress.XtraGrid.Columns.GridColumn()
Me.XtraTabPageAdmin = New DevExpress.XtraTab.XtraTabPage()
Me.SplitContainerControl2 = New DevExpress.XtraEditors.SplitContainerControl()
Me.GridControlData = New DevExpress.XtraGrid.GridControl()
Me.GridViewData = New DevExpress.XtraGrid.Views.Grid.GridView()
Me.PanelControl1 = New DevExpress.XtraEditors.PanelControl()
Me.GroupControl2 = New DevExpress.XtraEditors.GroupControl()
Me.GroupControl1 = New DevExpress.XtraEditors.GroupControl()
Me.btnEvvallUs_lastmonth = New DevExpress.XtraEditors.SimpleButton()
Me.btnEvvallUs_thismonth = New DevExpress.XtraEditors.SimpleButton()
Me.GroupControl1 = New DevExpress.XtraEditors.GroupControl()
Me.btnEnvelopes_All = New DevExpress.XtraEditors.SimpleButton()
Me.btnEnvelopes_thisYear = New DevExpress.XtraEditors.SimpleButton()
Me.btnEnvelopes_lastmonth = New DevExpress.XtraEditors.SimpleButton()
@@ -103,7 +106,6 @@ Partial Class frmMain
Me.RefreshTimer = New System.Windows.Forms.Timer(Me.components)
Me.SaveFileDialog1 = New System.Windows.Forms.SaveFileDialog()
Me.XtraSaveFileDialog1 = New DevExpress.XtraEditors.XtraSaveFileDialog(Me.components)
Me.btnEvvallUs_lastmonth = New DevExpress.XtraEditors.SimpleButton()
CType(Me.SplitContainerControl1, System.ComponentModel.ISupportInitialize).BeginInit()
CType(Me.SplitContainerControl1.Panel1, System.ComponentModel.ISupportInitialize).BeginInit()
Me.SplitContainerControl1.Panel1.SuspendLayout()
@@ -279,11 +281,12 @@ Partial Class frmMain
'
'ViewEnvelopes
'
Me.ViewEnvelopes.Columns.AddRange(New DevExpress.XtraGrid.Columns.GridColumn() {Me.colEnvelopeId, Me.colContractType, Me.colStatus, Me.colTitle, Me.colAddedWhen})
Me.ViewEnvelopes.Columns.AddRange(New DevExpress.XtraGrid.Columns.GridColumn() {Me.colEnvelopeId, Me.colContractType, Me.colStatus, Me.colTitle, Me.colAddedWhen, Me.GridColumn2})
Me.ViewEnvelopes.GridControl = Me.GridEnvelopes
Me.ViewEnvelopes.Name = "ViewEnvelopes"
Me.ViewEnvelopes.OptionsBehavior.Editable = False
Me.ViewEnvelopes.OptionsBehavior.ReadOnly = True
Me.ViewEnvelopes.OptionsView.ShowAutoFilterRow = True
Me.ViewEnvelopes.OptionsView.ShowIndicator = False
'
'colEnvelopeId
@@ -318,6 +321,14 @@ Partial Class frmMain
Me.colAddedWhen.FieldName = "AddedWhen"
Me.colAddedWhen.Name = "colAddedWhen"
'
'GridColumn2
'
resources.ApplyResources(Me.GridColumn2, "GridColumn2")
Me.GridColumn2.DisplayFormat.FormatString = "G"
Me.GridColumn2.DisplayFormat.FormatType = DevExpress.Utils.FormatType.DateTime
Me.GridColumn2.FieldName = "ChangedWhen"
Me.GridColumn2.Name = "GridColumn2"
'
'RibbonControl
'
Me.RibbonControl.ExpandCollapseItem.Id = 0
@@ -637,11 +648,12 @@ Partial Class frmMain
'
'ViewCompleted
'
Me.ViewCompleted.Columns.AddRange(New DevExpress.XtraGrid.Columns.GridColumn() {Me.GridColumn3, Me.GridColumn4, Me.GridColumn5, Me.GridColumn7})
Me.ViewCompleted.Columns.AddRange(New DevExpress.XtraGrid.Columns.GridColumn() {Me.GridColumn3, Me.GridColumn4, Me.GridColumn5, Me.GridColumn7, Me.GridColumn1})
Me.ViewCompleted.GridControl = Me.GridCompleted
Me.ViewCompleted.Name = "ViewCompleted"
Me.ViewCompleted.OptionsBehavior.Editable = False
Me.ViewCompleted.OptionsBehavior.ReadOnly = True
Me.ViewCompleted.OptionsView.ShowAutoFilterRow = True
Me.ViewCompleted.OptionsView.ShowIndicator = False
'
'GridColumn3
@@ -665,9 +677,19 @@ Partial Class frmMain
'GridColumn7
'
resources.ApplyResources(Me.GridColumn7, "GridColumn7")
Me.GridColumn7.DisplayFormat.FormatString = "G"
Me.GridColumn7.DisplayFormat.FormatType = DevExpress.Utils.FormatType.DateTime
Me.GridColumn7.FieldName = "AddedWhen"
Me.GridColumn7.Name = "GridColumn7"
'
'GridColumn1
'
resources.ApplyResources(Me.GridColumn1, "GridColumn1")
Me.GridColumn1.DisplayFormat.FormatString = "G"
Me.GridColumn1.DisplayFormat.FormatType = DevExpress.Utils.FormatType.DateTime
Me.GridColumn1.FieldName = "ChangedWhen"
Me.GridColumn1.Name = "GridColumn1"
'
'XtraTabPageAdmin
'
Me.XtraTabPageAdmin.Controls.Add(Me.SplitContainerControl2)
@@ -688,7 +710,7 @@ Partial Class frmMain
'SplitContainerControl2.Panel2
'
resources.ApplyResources(Me.SplitContainerControl2.Panel2, "SplitContainerControl2.Panel2")
Me.SplitContainerControl2.SplitterPosition = 819
Me.SplitContainerControl2.SplitterPosition = 907
'
'GridControlData
'
@@ -702,6 +724,7 @@ Partial Class frmMain
'
Me.GridViewData.GridControl = Me.GridControlData
Me.GridViewData.Name = "GridViewData"
Me.GridViewData.OptionsView.ShowAutoFilterRow = True
'
'PanelControl1
'
@@ -717,6 +740,20 @@ Partial Class frmMain
resources.ApplyResources(Me.GroupControl2, "GroupControl2")
Me.GroupControl2.Name = "GroupControl2"
'
'btnEvvallUs_lastmonth
'
Me.btnEvvallUs_lastmonth.Appearance.BackColor = System.Drawing.Color.MediumPurple
Me.btnEvvallUs_lastmonth.Appearance.Options.UseBackColor = True
resources.ApplyResources(Me.btnEvvallUs_lastmonth, "btnEvvallUs_lastmonth")
Me.btnEvvallUs_lastmonth.Name = "btnEvvallUs_lastmonth"
'
'btnEvvallUs_thismonth
'
Me.btnEvvallUs_thismonth.Appearance.BackColor = System.Drawing.Color.MediumSlateBlue
Me.btnEvvallUs_thismonth.Appearance.Options.UseBackColor = True
resources.ApplyResources(Me.btnEvvallUs_thismonth, "btnEvvallUs_thismonth")
Me.btnEvvallUs_thismonth.Name = "btnEvvallUs_thismonth"
'
'GroupControl1
'
Me.GroupControl1.Controls.Add(Me.btnEnvelopes_All)
@@ -726,13 +763,6 @@ Partial Class frmMain
resources.ApplyResources(Me.GroupControl1, "GroupControl1")
Me.GroupControl1.Name = "GroupControl1"
'
'btnEvvallUs_thismonth
'
Me.btnEvvallUs_thismonth.Appearance.BackColor = System.Drawing.Color.MediumSlateBlue
Me.btnEvvallUs_thismonth.Appearance.Options.UseBackColor = True
resources.ApplyResources(Me.btnEvvallUs_thismonth, "btnEvvallUs_thismonth")
Me.btnEvvallUs_thismonth.Name = "btnEvvallUs_thismonth"
'
'btnEnvelopes_All
'
Me.btnEnvelopes_All.Appearance.BackColor = System.Drawing.Color.MediumTurquoise
@@ -773,13 +803,6 @@ Partial Class frmMain
'
Me.XtraSaveFileDialog1.FileName = "XtraSaveFileDialog1"
'
'btnEvvallUs_lastmonth
'
Me.btnEvvallUs_lastmonth.Appearance.BackColor = System.Drawing.Color.MediumPurple
Me.btnEvvallUs_lastmonth.Appearance.Options.UseBackColor = True
resources.ApplyResources(Me.btnEvvallUs_lastmonth, "btnEvvallUs_lastmonth")
Me.btnEvvallUs_lastmonth.Name = "btnEvvallUs_lastmonth"
'
'frmMain
'
resources.ApplyResources(Me, "$this")
@@ -906,4 +929,6 @@ Partial Class frmMain
Friend WithEvents BarStaticItemGhost As DevExpress.XtraBars.BarStaticItem
Friend WithEvents btnEvvallUs_thismonth As DevExpress.XtraEditors.SimpleButton
Friend WithEvents btnEvvallUs_lastmonth As DevExpress.XtraEditors.SimpleButton
Friend WithEvents GridColumn1 As DevExpress.XtraGrid.Columns.GridColumn
Friend WithEvents GridColumn2 As DevExpress.XtraGrid.Columns.GridColumn
End Class

View File

@@ -265,7 +265,7 @@
<value>2</value>
</data>
<data name="colContractType.Width" type="System.Int32, mscorlib">
<value>120</value>
<value>112</value>
</data>
<data name="colStatus.Caption" xml:space="preserve">
<value>Status</value>
@@ -277,7 +277,7 @@
<value>1</value>
</data>
<data name="colStatus.Width" type="System.Int32, mscorlib">
<value>193</value>
<value>180</value>
</data>
<data name="colTitle.Caption" xml:space="preserve">
<value>Titel</value>
@@ -289,7 +289,7 @@
<value>0</value>
</data>
<data name="colTitle.Width" type="System.Int32, mscorlib">
<value>575</value>
<value>538</value>
</data>
<data name="colAddedWhen.Caption" xml:space="preserve">
<value>Erstellt am</value>
@@ -301,7 +301,19 @@
<value>3</value>
</data>
<data name="colAddedWhen.Width" type="System.Int32, mscorlib">
<value>196</value>
<value>130</value>
</data>
<data name="GridColumn2.Caption" xml:space="preserve">
<value>Zuletzt geändert am</value>
</data>
<data name="GridColumn2.Visible" type="System.Boolean, mscorlib">
<value>True</value>
</data>
<data name="GridColumn2.VisibleIndex" type="System.Int32, mscorlib">
<value>4</value>
</data>
<data name="GridColumn2.Width" type="System.Int32, mscorlib">
<value>130</value>
</data>
<data name="RibbonControl.ExpandCollapseItem.ImageOptions.ImageIndex" type="System.Int32, mscorlib">
<value>0</value>
@@ -1052,7 +1064,7 @@
<value>2</value>
</data>
<data name="GridColumn3.Width" type="System.Int32, mscorlib">
<value>120</value>
<value>100</value>
</data>
<data name="GridColumn4.Caption" xml:space="preserve">
<value>Status</value>
@@ -1064,7 +1076,7 @@
<value>1</value>
</data>
<data name="GridColumn4.Width" type="System.Int32, mscorlib">
<value>195</value>
<value>163</value>
</data>
<data name="GridColumn5.Caption" xml:space="preserve">
<value>Titel</value>
@@ -1076,7 +1088,7 @@
<value>0</value>
</data>
<data name="GridColumn5.Width" type="System.Int32, mscorlib">
<value>574</value>
<value>482</value>
</data>
<data name="GridColumn7.Caption" xml:space="preserve">
<value>Erstellt am</value>
@@ -1088,7 +1100,19 @@
<value>3</value>
</data>
<data name="GridColumn7.Width" type="System.Int32, mscorlib">
<value>195</value>
<value>120</value>
</data>
<data name="GridColumn1.Caption" xml:space="preserve">
<value>Zuletzt geändert am</value>
</data>
<data name="GridColumn1.Visible" type="System.Boolean, mscorlib">
<value>True</value>
</data>
<data name="GridColumn1.VisibleIndex" type="System.Int32, mscorlib">
<value>4</value>
</data>
<data name="GridColumn1.Width" type="System.Int32, mscorlib">
<value>120</value>
</data>
<data name="GridCompleted.Size" type="System.Drawing.Size, System.Drawing">
<value>1088, 469</value>
@@ -1139,7 +1163,7 @@
<value>0, 0</value>
</data>
<data name="GridControlData.Size" type="System.Drawing.Size, System.Drawing">
<value>819, 390</value>
<value>907, 390</value>
</data>
<data name="GridControlData.TabIndex" type="System.Int32, mscorlib">
<value>1</value>
@@ -1262,7 +1286,7 @@
<value>2</value>
</data>
<data name="GroupControl2.Text" xml:space="preserve">
<value>Umschläge alle User</value>
<value>Umschläge alle User (abrechnungsrelevant)</value>
</data>
<data name="&gt;&gt;GroupControl2.Name" xml:space="preserve">
<value>GroupControl2</value>
@@ -1717,6 +1741,12 @@
<data name="&gt;&gt;colAddedWhen.Type" xml:space="preserve">
<value>DevExpress.XtraGrid.Columns.GridColumn, DevExpress.XtraGrid.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a</value>
</data>
<data name="&gt;&gt;GridColumn2.Name" xml:space="preserve">
<value>GridColumn2</value>
</data>
<data name="&gt;&gt;GridColumn2.Type" xml:space="preserve">
<value>DevExpress.XtraGrid.Columns.GridColumn, DevExpress.XtraGrid.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a</value>
</data>
<data name="&gt;&gt;btnCreateEnvelope.Name" xml:space="preserve">
<value>btnCreateEnvelope</value>
</data>
@@ -1939,6 +1969,12 @@
<data name="&gt;&gt;GridColumn7.Type" xml:space="preserve">
<value>DevExpress.XtraGrid.Columns.GridColumn, DevExpress.XtraGrid.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a</value>
</data>
<data name="&gt;&gt;GridColumn1.Name" xml:space="preserve">
<value>GridColumn1</value>
</data>
<data name="&gt;&gt;GridColumn1.Type" xml:space="preserve">
<value>DevExpress.XtraGrid.Columns.GridColumn, DevExpress.XtraGrid.v21.2, Version=21.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a</value>
</data>
<data name="&gt;&gt;GridViewData.Name" xml:space="preserve">
<value>GridViewData</value>
</data>

View File

@@ -538,7 +538,7 @@ Public Class frmMain
End If
bbtnitmEB.Enabled = False
RefreshTimer.Start()
If USER_GHOST_MODE_ACTIVE Then
If USER_GHOST_MODE_ACTIVE Or MYUSER.GhostModeActive Then
frmGhostMode.ShowDialog()
If USER_GHOST_MODE_USRNAME <> "" Then
MyUserModel = New UserModel(MyState)
@@ -546,7 +546,8 @@ Public Class frmMain
Dim oUser = MyUserModel.SelectUser()
If oUser IsNot Nothing Then
MyUserModel.CheckUserLogin(oUser)
BarStaticItemGhost.Caption = $"GhostMode active: {USER_GHOST_MODE_USRNAME} - End signFLOW to quit mode"
BarStaticItemGhost.Caption = $"GhostMode active: {USER_GHOST_MODE_USRNAME} - End signFLOW to quit ghost-mode"
Me.Text = $"GhostMode active: {USER_GHOST_MODE_USRNAME} - End signFLOW to quit ghost - mode"
BarStaticItemGhost.Visibility = DevExpress.XtraBars.BarItemVisibility.Always
LoadEnvelopeData()
End If

View File

@@ -1,201 +1,150 @@
using DigitalData.Core.Abstractions.Application;
using DigitalData.UserManager.Application.Contracts;
using DigitalData.UserManager.Application.DTOs.User;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using DigitalData.UserManager.Application.DTOs.Auth;
using Microsoft.AspNetCore.Authorization;
using EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.GeneratorAPI.Controllers
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public partial class AuthController : ControllerBase
{
private readonly ILogger<AuthController> _logger;
private readonly IUserService _userService;
private readonly IDirectorySearchService _dirSearchService;
/// <summary>
/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus.
/// Initializes a new instance of the <see cref="AuthController"/> class.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public partial class AuthController : ControllerBase
/// <param name="logger">The logger instance.</param>
/// <param name="userService">The user service instance.</param>
/// <param name="dirSearchService">The directory search service instance.</param>
public AuthController(ILogger<AuthController> logger, IUserService userService, IDirectorySearchService dirSearchService)
{
private readonly ILogger<AuthController> _logger;
private readonly IUserService _userService;
private readonly IDirectorySearchService _dirSearchService;
/// <summary>
/// Initializes a new instance of the <see cref="AuthController"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <param name="userService">The user service instance.</param>
/// <param name="dirSearchService">The directory search service instance.</param>
public AuthController(ILogger<AuthController> logger, IUserService userService, IDirectorySearchService dirSearchService)
{
_logger = logger;
_userService = userService;
_dirSearchService = dirSearchService;
}
/// <summary>
/// Authentifiziert einen Benutzer und generiert ein JWT-Token. Wenn 'cookie' wahr ist, wird das Token als HTTP-Only-Cookie zurückgegeben.
/// </summary>
/// <param name="login">Benutzeranmeldedaten (Benutzername und Passwort).</param>
/// <param name="cookie">Wenn wahr, wird das JWT-Token auch als HTTP-Only-Cookie gesendet.</param>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth?cookie=true
/// {
/// "username": "MaxMustermann",
/// "password": "Geheim123!"
/// }
///
/// POST /api/auth?cookie=true
/// {
/// "id": "1",
/// "password": "Geheim123!"
/// }
///
/// </remarks>
/// <response code="200">Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist.</response>
/// <response code="401">Unbefugt. Ungültiger Benutzername oder Passwort.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> Login([FromBody] Login login, [FromQuery] bool cookie = false)
{
try
{
bool isValid = _dirSearchService.ValidateCredentials(login.Username, login.Password);
if (!isValid)
return Unauthorized();
//find the user
var uRes = await _userService.ReadByUsernameAsync(login.Username);
if (!uRes.IsSuccess || uRes.Data is null)
{
return Forbid();
}
UserReadDto user = uRes.Data;
// Create claims
var claims = new List<Claim>
{
new (ClaimTypes.NameIdentifier, user.Id.ToString()),
new (ClaimTypes.Name, user.Username),
new (ClaimTypes.Surname, user.Name!),
new (ClaimTypes.GivenName, user.Prename!),
new (ClaimTypes.Email, user.Email!),
};
// Create claimsIdentity
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
// Create authProperties
var authProperties = new AuthenticationProperties
{
IsPersistent = true,
AllowRefresh = true,
ExpiresUtc = DateTime.Now.AddMinutes(180)
};
// Sign in
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
return Ok();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred.\n{ErrorMessage}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// Authentifiziert einen Benutzer und generiert ein JWT-Token. Das Token wird als HTTP-only-Cookie zurückgegeben.
/// </summary>
/// <param name="login">Benutzeranmeldedaten (Benutzername und Passwort).</param>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth/form
/// {
/// "username": "MaxMustermann",
/// "password": "Geheim123!"
/// }
///
/// </remarks>
/// <response code="200">Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist.</response>
/// <response code="401">Unbefugt. Ungültiger Benutzername oder Passwort.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[AllowAnonymous]
[HttpPost]
[Route("form")]
public async Task<IActionResult> Login([FromForm] Login login)
{
return await Login(login, true);
}
/// <summary>
/// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie)
/// </summary>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth/logout
///
/// </remarks>
/// <response code="200">Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat.</response>
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[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.
/// </summary>
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
/// <remarks>
/// Sample request:
///
/// GET /api/auth
///
/// </remarks>
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize]
[HttpGet]
public IActionResult IsAuthenticated() => Ok();
_logger = logger;
_userService = userService;
_dirSearchService = dirSearchService;
}
/// <summary>
/// Authentifiziert einen Benutzer und generiert ein JWT-Token. Wenn 'cookie' wahr ist, wird das Token als HTTP-Only-Cookie zurückgegeben.
/// </summary>
/// <param name="login">Benutzeranmeldedaten (Benutzername und Passwort).</param>
/// <param name="cookie">Wenn wahr, wird das JWT-Token auch als HTTP-Only-Cookie gesendet.</param>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth?cookie=true
/// {
/// "username": "MaxMustermann",
/// "password": "Geheim123!"
/// }
///
/// POST /api/auth?cookie=true
/// {
/// "id": "1",
/// "password": "Geheim123!"
/// }
///
/// </remarks>
/// <response code="200">Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist.</response>
/// <response code="401">Unbefugt. Ungültiger Benutzername oder Passwort.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[AllowAnonymous]
[HttpPost]
public Task<IActionResult> Login([FromBody] Login login, [FromQuery] bool cookie = false)
{
// added to configure open API (swagger and scalar)
throw new NotImplementedException();
}
/// <summary>
/// Authentifiziert einen Benutzer und generiert ein JWT-Token. Das Token wird als HTTP-only-Cookie zurückgegeben.
/// </summary>
/// <param name="login">Benutzeranmeldedaten (Benutzername und Passwort).</param>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth/form
/// {
/// "username": "MaxMustermann",
/// "password": "Geheim123!"
/// }
///
/// </remarks>
/// <response code="200">Erfolgreiche Anmeldung. Gibt das JWT-Token im Antwortkörper oder als Cookie zurück, wenn 'cookie' wahr ist.</response>
/// <response code="401">Unbefugt. Ungültiger Benutzername oder Passwort.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[AllowAnonymous]
[HttpPost]
[Route("form")]
public Task<IActionResult> Login([FromForm] Login login)
{
// added to configure open API (swagger and scalar)
throw new NotImplementedException();
}
/// <summary>
/// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie)
/// </summary>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth/logout
///
/// </remarks>
/// <response code="200">Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat.</response>
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[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.
/// </summary>
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
/// <remarks>
/// Sample request:
///
/// GET /api/auth
///
/// </remarks>
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, "text/javascript")]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize]
[HttpGet]
public IActionResult IsAuthenticated() => Ok();
}

View File

@@ -5,10 +5,14 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers
{
public static class ControllerExtensions
{
public static int? GetId(this ClaimsPrincipal user)
=> int.TryParse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value, out int result)
public static int? GetIdOrDefault(this ClaimsPrincipal user)
=> int.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.FindFirstValue("sub"), out int result)
? result : null;
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.");
public static string? GetUsername(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Name)?.Value;

View File

@@ -1,8 +1,11 @@
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;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -27,16 +30,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>
@@ -75,4 +81,33 @@ public class EnvelopeController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
///
/// </summary>
/// <param name="envelope"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateAsync([FromQuery] CreateEnvelopeCommand envelope)
{
try
{
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);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}

View File

@@ -1,11 +1,16 @@
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.Domain.Entities;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Channels;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -26,17 +31,29 @@ public class EnvelopeReceiverController : ControllerBase
private readonly IMediator _mediator;
private readonly IMapper _mapper;
private readonly IEnvelopeExecutor _envelopeExecutor;
private readonly IEnvelopeReceiverExecutor _erExecutor;
/// <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>
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IEnvelopeReceiverService envelopeReceiverService, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor)
{
_logger = logger;
_erService = envelopeReceiverService;
_mediator = mediator;
_mapper = mapper;
_envelopeExecutor = envelopeExecutor;
_erExecutor = erExecutor;
}
/// <summary>
@@ -120,8 +137,7 @@ public class EnvelopeReceiverController : ControllerBase
/// <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 +173,38 @@ 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();
try
{
CancellationToken cancel = default;
int userId = User.GetId();
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.UnsentRecipients = unsentRecipients;
res.SentRecipients = _mapper.Map<IEnumerable<ReceiverReadDto>>(sentRecipients);
return Ok(res);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}

View File

@@ -19,12 +19,13 @@
<ItemGroup>
<PackageReference Include="AspNetCore.Scalar" Version="1.1.8" />
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
<PackageReference Include="DigitalData.Core.API" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
<PackageReference Include="Scalar.AspNetCore" Version="2.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.3" />
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.2.1" />
<PackageReference Include="DigitalData.Core.DTO" Version="2.0.1" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.0.0" />

View File

@@ -0,0 +1,10 @@
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'dotnet build'
}
}
}
}

View File

@@ -0,0 +1,28 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
/// <summary>
/// Represents the keys and default values used for authentication token handling
/// within the Envelope Generator API.
/// </summary>
public class AuthTokenKeys
{
/// <summary>
/// Gets the name of the cookie used to store the authentication token.
/// </summary>
public string Cookie { get; init; } = "AuthToken";
/// <summary>
/// Gets the name of the query string parameter used to pass the authentication token.
/// </summary>
public string QueryString { get; init; } = "AuthToken";
/// <summary>
/// Gets the expected issuer value for the authentication token.
/// </summary>
public string Issuer { get; init; } = "auth.digitaldata.works";
/// <summary>
/// Gets the expected audience value for the authentication token.
/// </summary>
public string Audience { get; init; } = "sign-flow-gen.digitaldata.works";
}

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

@@ -1,6 +1,5 @@
using DigitalData.Core.API;
using DigitalData.Core.Application;
using DigitalData.UserManager.Application;
using EnvelopeGenerator.Infrastructure;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Localization;
@@ -10,11 +9,19 @@ using Scalar.AspNetCore;
using Microsoft.OpenApi.Models;
using DigitalData.UserManager.DependencyInjection;
using EnvelopeGenerator.Application;
using DigitalData.Auth.Client;
using DigitalData.Core.Abstractions;
using EnvelopeGenerator.GeneratorAPI.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using DigitalData.Core.Abstractions.Security.Extensions;
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
var deferredProvider = new DeferredServiceProvider();
builder.Services.AddControllers();
//CORS Policy
@@ -85,6 +92,49 @@ builder.Services.AddOpenApi();
var connStr = config.GetConnectionString("Default") ?? throw new InvalidOperationException("There is no default connection string in appsettings.json.");
builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"));
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
{
var clientParams = deferredProvider.GetOptions<ClientParams>();
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
return new List<SecurityKey>() { publicKey.SecurityKey };
},
ValidateIssuer = true,
ValidIssuer = authTokenKeys.Issuer,
ValidateAudience = true,
ValidAudience = authTokenKeys.Audience,
};
opt.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// if there is no token read related cookie or query string
if (context.Token is null) // if there is no token
{
if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null)
context.Token = cookieToken;
else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken))
context.Token = queryStrToken;
}
return Task.CompletedTask;
}
};
});
// Authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
@@ -109,11 +159,13 @@ 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.MapOpenApi();
// Configure the HTTP request pipeline.

View File

@@ -22,7 +22,7 @@
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7174;http://localhost:5131",
"environmentVariables": {

View File

@@ -4,5 +4,15 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AuthClientParams": {
"Url": "https://localhost:7192/auth-hub",
"PublicKeys": [
{
"Issuer": "auth.digitaldata.works",
"Audience": "sign-flow-gen.digitaldata.works"
}
],
"RetryDelay": "00:00:05"
}
}

View File

@@ -20,5 +20,226 @@
"User": "(&(objectClass=user)(sAMAccountName=*))",
"Group": "(&(objectClass=group)(samAccountName=*))"
}
},
"AuthClientParams": {
"Url": "https://localhost:7192/auth-hub",
"PublicKeys": [
{
"Issuer": "auth.digitaldata.works",
"Audience": "sign-flow-gen.digitaldata.works"
}
],
"RetryDelay": "00:00:05"
},
"AuthTokenKeys": {
"Cookie": "AuthToken",
"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": []
},
"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,91 @@ 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>();
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,7 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.3" />
<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" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.0.0" />

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>();
await connection.OpenAsync(cancellation);
var parameters = EnvelopeCreateReadSQL.CreateParmas(userId, title, message, tfaEnabled);
var envelopes = await connection.QueryAsync<Envelope>(sql.Raw, parameters);
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.Logging;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Infrastructure.Executor;
public class EnvelopeReceiverExecutor: SQLExecutor, IEnvelopeReceiverExecutor
{
private readonly IEnvelopeReceiverRepository _erRepository;
public EnvelopeReceiverExecutor(ILogger<EnvelopeExecutor> logger, 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>();
await connection.OpenAsync(cancellation);
var envelopeReceivers = await connection.QueryAsync<EnvelopeReceiver>(sql.Raw, EnvelopeReceiverAddReadSQL.CreateParameters(envelope_uuid, emailAddress, salutation, phone));
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

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

@@ -1,16 +1,13 @@
Imports DigitalData.Modules.Config.ConfigAttributes
Imports EnvelopeGenerator.Common.Jobs.FinalizeDocument
Public Class Config
<ConnectionString>
Public Property ConnectionString As String = ""
Public Property Debug As Boolean = False
Public Property IntervalInMin As Integer = 1
Public Property IgnoredLabels As List(Of String) = New List(Of String) From {
"Date", "Datum",
"ZIP", "PLZ",
"Place", "Ort",
"Position", "Stellung"
}
Public Property PDFBurnerParams As PDFBurnerParams = New PDFBurnerParams()
End Class

View File

@@ -3,6 +3,7 @@ Imports DigitalData.Modules.Base
Imports DigitalData.Modules.Database
Imports DigitalData.Modules.Logging
Imports EnvelopeGenerator.Common.Jobs
Imports EnvelopeGenerator.Common.Jobs.FinalizeDocument
Imports Quartz
Public Class Scheduler
@@ -11,15 +12,15 @@ Public Class Scheduler
Private Scheduler As IScheduler
Private ReadOnly ConnectionString As String
Private ReadOnly LicenseKey As String
Private Property _ignoredLabels As List(Of String)
Private Property _pdfBurnerParams As PDFBurnerParams
Private Const JobName = "CertificateDocumentJob"
Public Sub New(pLogConfig As LogConfig, pConnectionString As String, pLicenseKey As String, ignoredLabels As List(Of String))
Public Sub New(pLogConfig As LogConfig, pConnectionString As String, pLicenseKey As String, pdfBurnerParams As PDFBurnerParams)
MyBase.New(pLogConfig)
ConnectionString = pConnectionString
LicenseKey = pLicenseKey
_ignoredLabels = ignoredLabels
_pdfBurnerParams = pdfBurnerParams
Dim oLogProvider = New LogProvider(Logger)
Logging.LogProvider.SetCurrentLogProvider(oLogProvider)
@@ -40,7 +41,7 @@ Public Class Scheduler
{Common.Constants.GDPICTURE, LicenseKey},
{Common.Constants.LOGCONFIG, LogConfig},
{Common.Constants.DATABASE, ConnectionString},
{Common.Constants.IGNORED_LABELS, _ignoredLabels}
{Common.Constants.PDF_BURNER_PARAMS, _pdfBurnerParams}
}
Logger.Debug("Initialized Job [{0}]", JobName)

View File

@@ -55,7 +55,7 @@ Public Class Service
Logger.Debug("Inititalize Quartz")
Scheduler = New Scheduler(LogConfig, Config.ConnectionString, oKey, Config.IgnoredLabels)
Scheduler = New Scheduler(LogConfig, Config.ConnectionString, oKey, Config.PDFBurnerParams)
Await Scheduler.Start(Config.IntervalInMin)
Logger.Info("Started [{0}] !", ServiceName)

View File

@@ -32,11 +32,27 @@ public class CommandManager
[Subcommand]
public IEnvelopeReceiverService EnvelopeReceiver => _envelopeReceiverService;
[Command]
public async Task ReadDocument(IConsole console, int? id = null, int? envelopeId = null)
[Command(ArgumentSeparatorStrategy = ArgumentSeparatorStrategy.EndOfOptions)]
public async Task ReadDocument(IConsole console,
[Option(Description = "ID of the document.")] int? id = null,
[Option(Description = "ID of the envelope containing the document.")] int? envelopeId = null,
[Option(Description = "Path to save the PDF")] bool save = false,
[Option(Description = "Directory to save the PDF")] string? dir = null,
[Option(Description = "Name of file to save the PDF")] string? fileName = null)
{
ReadDocumentQuery query = new(id, envelopeId);
var document = await _mediator.Send(query);
console.WriteLine(JsonSerializer.Serialize(document, Options));
console.WriteLine(JsonSerializer.Serialize(save ? document as ReadDocumentResponseBase : document, Options));
if (save)
{
dir ??= AppContext.BaseDirectory;
fileName ??= $"D{document?.Guid}E{document?.EnvelopeId}.pdf";
var path = Path.Combine(dir, fileName);
console.WriteLine("Save to " + path);
File.WriteAllBytes(path, document?.ByteData ?? Array.Empty<byte>());
}
}
}

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

@@ -19,7 +19,7 @@
<PackageReference Include="CommandDotNet" Version="7.0.5" />
<PackageReference Include="CommandDotNet.IoC.MicrosoftDependencyInjection" Version="5.0.1" />
<PackageReference Include="CommandDotNet.NameCasing" Version="4.0.2" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.3" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.Application" Version="3.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.20" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="7.0.20" />

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace EnvelopeGenerator.Terminal;
@@ -8,6 +9,10 @@ public class Program
{
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
var config = builder.Configuration;
builder.Services.AddCommandManagerRunner(config);

View File

@@ -23,7 +23,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.3" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.1.1" />
<PackageReference Include="DigitalData.Core.Application" Version="3.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />

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

@@ -2101,7 +2101,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="BuildBundlerMinifier2022" Version="2.9.9" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.4.3" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="3.6.0" />
<PackageReference Include="DigitalData.Core.API" Version="2.1.1" />
<PackageReference Include="DigitalData.Core.Application" Version="3.2.1" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.0.0" />

View File

@@ -90,7 +90,7 @@ try
});
// Add envelope generator services
builder.Services.AddEnvelopeGeneratorRepositories(options => options.UseSqlServer(connStr));
builder.Services.AddEnvelopeGeneratorInfrastructureServices(options => options.UseSqlServer(connStr));
builder.Services.AddEnvelopeGeneratorServices(config);