60 Commits

Author SHA1 Message Date
eb96842122 Refactor: Rename EmailTemplateQuery to EmailTemplateQueryBase
Refactored the EmailTemplateQuery record to EmailTemplateQueryBase across the codebase. Updated all references, method signatures, inheritance, and documentation to use the new base type. No functional changes; this improves clarity and generalization for email template queries.
2026-02-06 15:19:33 +01:00
a8cb8f935c No changes detected in this commit
No code was added or removed in this commit. The diff does not reflect any modifications to the source files.
2026-02-06 15:18:21 +01:00
387456659d Remove Obsolete attribute from Handle method
The [Obsolete("Use IRepository")] attribute was removed from the Handle method in ReadEmailTemplateQueryHandler, eliminating related compiler warnings and indicating that the method is no longer considered obsolete.
2026-02-06 15:15:46 +01:00
17e3a67fe5 Refactor handler to use generic IRepository for templates
Replaces IEmailTemplateRepository with IRepository<EmailTemplate> in ReadEmailTemplateQueryHandler. Updates method logic to use LINQ queries and FirstOrDefaultAsync. Cleans up obsolete attributes, comments, and using directives. Renames cancellation token parameter for consistency.
2026-02-06 15:14:46 +01:00
632723de5a Refactor email template DTOs and response handling
Replaced ReadEmailTemplateResponse with EmailTemplateDto as the standard DTO for email templates. Updated ReadEmailTemplateQuery and its handler to use EmailTemplateDto. Removed the obsolete MappingProfile and cleaned up EmailTemplateDto with improved documentation. Simplified UpdateEmailTemplateCommandHandler by removing conditional updates for Body and Subject. These changes streamline DTO usage and reduce redundancy in email template handling.
2026-02-06 15:01:04 +01:00
6611b92fdb Update EmailTemplateDto: add timestamps, make fields nullable
Removed ApiExplorerSettings attribute. Changed Body and Subject to nullable strings with init accessors and updated their documentation. Added AddedWhen and ChangedWhen timestamp properties to track template creation and modification dates. Cleaned up property formatting.
2026-02-06 14:14:31 +01:00
5baa28bac8 Move handler to ReadEmailTemplateQuery.cs, remove old file
Refactored by moving ReadEmailTemplateQueryHandler and its implementation into ReadEmailTemplateQuery.cs. Updated using directives accordingly. Deleted the now-redundant ReadEmailTemplateQueryHandler.cs file. No logic changes were made. This consolidates related query and handler code for better maintainability.
2026-02-06 13:53:51 +01:00
c965975f82 Refactor EmailTemplateController to use primary constructor
- Enforce [Authorize(Policy = AuthPolicy.Sender)] on controller
- Switch to primary constructor for dependency injection
- Remove obsolete constructor and private fields
- Update method logic to use constructor parameters directly
- Improve XML documentation and code clarity
- Ensure consistent use of MediatR for command/query handling
2026-02-06 13:51:50 +01:00
d480dd3a36 Refactor DocumentController for async policy-based auth
Refactored DocumentController to use IAuthorizationService and async policy checks via IsUserInPolicyAsync instead of role checks. Implemented IAuthController interface and removed ILogger dependency. Updated usings for new authorization logic.
2026-02-06 13:46:18 +01:00
ae7f0b80f3 Refactor AuthController for interface and policy checks
Refactored AuthController to implement IAuthController and expose AuthService. Removed the protected IsUserInPolicyAsync method in favor of using an extension method for policy checks. Updated the Logout logic to use the new approach. Consolidated using directives into a single line.
2026-02-06 13:41:45 +01:00
ef7c9c2b97 Add IAuthController interface and policy check extension
Introduced IAuthController with AuthService and User properties to standardize authentication handling. Added AuthorizationControllerExtensions with IsUserInPolicyAsync to simplify policy-based authorization checks. Included necessary using directives.
2026-02-06 13:39:14 +01:00
27f0aae8e0 Remove unused AuthExtensions.cs and its extension method
Deleted the AuthExtensions.cs file, which included the AuthorizePolicyAsync extension for IAuthorizationService. This method and related code were no longer needed.
2026-02-06 13:23:17 +01:00
1b10162c85 Refactor AuthController policy checks and response types
Introduce IsUserInPolicyAsync for cleaner policy checks in AuthController and update Logout to use it. Adjust Logout's response type to void and improve documentation and formatting.
2026-02-06 13:23:08 +01:00
bd0426dbee Refactor AuthController for improved policy-based auth
- Inject IAuthorizationService for flexible policy checks
- Replace role checks in Logout with async policy authorization
- Merge IsAuthenticated into Check endpoint with optional role
- Update Check response type and clean up imports
2026-02-06 13:04:57 +01:00
b1551537c8 Add ReceiverOrReceiverTFA constant to AuthPolicy
Added the ReceiverOrReceiverTFA constant to the AuthPolicy class in EnvelopeGenerator.Domain.Constants. This constant is defined as nameof(ReceiverOrReceiverTFA) + nameof(AuthPolicy).
2026-02-06 13:03:02 +01:00
95b2ab5aed Add AuthExtensions with AuthorizePolicyAsync method
Introduced a static AuthExtensions class providing an AuthorizePolicyAsync extension method for IAuthorizationService. This method streamlines policy-based authorization checks by returning a boolean result for a given user and policy name.
2026-02-06 11:50:06 +01:00
ebed51b46a Refactor receiver roles: rename FullyAuth/PreAuth for clarity
Renamed receiver roles FullyAuth → Receiver.Full and PreAuth → Receiver.TFA across the codebase for improved clarity and consistency. Updated all usages, [Authorize] attributes, role checks, authentication logic, and authorization policies to use the new role names. Marked old constants as obsolete and pointed them to the new values. This change enhances code readability and groups receiver roles under the Receiver static class.
2026-02-06 10:49:28 +01:00
0d2425c9cf Refactor to use named authorization policies in controllers
Replaced direct role-based [Authorize] attributes with named
authorization policies (e.g., AuthPolicy.Receiver,
AuthPolicy.SenderOrReceiver) in AnnotationController,
DocumentController, and ReadOnlyController. Added and registered
new policies in Program.cs and updated AuthPolicy constants.
This centralizes and simplifies authorization management.
2026-02-03 16:20:26 +01:00
c6c8747d23 Add ReceiverTFA constant to AuthPolicy class
Added the ReceiverTFA constant to the AuthPolicy class in the EnvelopeGenerator.Domain.Constants namespace. This new constant can be used to represent authentication policies specific to two-factor authentication for receivers.
2026-02-03 16:10:14 +01:00
eb345a0e4d Relax and rename auth policies for sender/receiver roles
Replaced SenderOrReceiverFullyAuth and ReceiverFullyAuth policies with more general SenderOrReceiver and Receiver policies. Updated policy definitions in AuthPolicy.cs to use nameof for clarity. Adjusted AddAuthorizationBuilder configuration and [Authorize] attributes in controllers to use the new, less restrictive policies, simplifying authorization logic.
2026-02-03 16:08:15 +01:00
1b95b9d7e0 Refactor authorization policy naming to AuthPolicy
Renamed AuthorizationPolicies to AuthPolicy and updated all references to use the new naming convention for authorization policy constants. This improves consistency and clarity across the codebase.
2026-02-03 16:01:28 +01:00
d99193979f Update to AddAuthorizationBuilder for policy config
Switched from AddAuthorization to AddAuthorizationBuilder for
defining authorization policies, resulting in more concise and
modern code. Policy logic and requirements remain unchanged.
2026-02-03 15:21:48 +01:00
8742ea6025 Switch to policy-based authorization for controllers
Replaced role-based [Authorize] attributes with policy-based ones in AuthController and TfaRegistrationController. This centralizes authorization logic and allows for more flexible access control.
2026-02-03 15:16:30 +01:00
2b8edc697a Add custom authorization policies and minor Swagger fix
Introduce SenderOrReceiverFullyAuth and ReceiverFullyAuth policies for role-based authorization. Register these policies in Program.cs. Also, fix OpenApiReference type for Swagger security configuration.
2026-02-03 15:15:04 +01:00
7c88d4ed4b Update Sender role constant to "EGSender"
Changed the value of the Sender constant in the Role class from "Sender" to "EGSender" to ensure consistency with updated naming conventions.
2026-02-03 14:54:10 +01:00
a6be907307 Refactor AuthController roles and add check endpoint
- Change AuthController to use IOptions<AuthTokenKeys> for config
- Restrict Logout and new Check endpoints to Sender and Receiver.FullyAuth roles
- Update Logout logic to handle cookie deletion or sign-out based on user role
- Add GET /api/auth/check to verify user role via query param
- Add necessary using statements for new dependencies
2026-02-03 14:54:02 +01:00
2fcea78574 Add Swagger doc filter for /api/auth proxy login endpoint
Introduced AuthProxyDocumentFilter to programmatically document the POST /api/auth proxy login endpoint in Swagger. The filter defines request body schemas, example values, query parameter, and response codes. Registered the filter in Program.cs for OpenAPI generation.
2026-02-03 11:13:53 +01:00
e8e428f935 Update default Audience value in AuthTokenKeys
Changed the default Audience property in AuthTokenKeys from "sign-flow-gen.digitaldata.works" to "sign-flow.digitaldata.works" to reflect the correct expected audience for authentication tokens.
2026-02-03 11:13:29 +01:00
9450ed3486 Remove old Login endpoint and related documentation
Removed the previous Login method from AuthController, including its XML documentation and Swagger/OpenAPI annotations. This prepares the controller for a revised authentication implementation.
2026-02-03 10:45:17 +01:00
583a07c646 Add YARP reverse proxy support to API project
Integrated YARP by adding the Yarp.ReverseProxy package, including yarp.json for proxy configuration, and updating Program.cs to load and map reverse proxy routes. This enables the API to forward requests based on yarp.json settings.
2026-02-03 10:44:32 +01:00
51ad4fbc2c Add YARP reverse proxy route for auth login requests
Configured yarp.json to proxy POST /api/auth requests to the
auth-hub cluster at http://172.24.12.39:9090, rewriting the
path to /api/auth/sign-flow before forwarding.
2026-02-03 10:39:33 +01:00
50ac7570ea Refactor GetDocument to unify sender and receiver logic
Combined sender and receiver document retrieval into a single
GetDocument endpoint. The endpoint now authorizes both Sender
and Receiver.FullyAuth roles, handling their logic based on
role detection. Sender requires a query parameter; receiver
extracts envelope ID from claims and disallows query params.
Updated method signature and endpoint documentation.
2026-02-03 10:06:03 +01:00
5465996563 Refactor document retrieval endpoints and authorization
- Updated DocumentController to use class-level [Authorize] and method-level role-based authorization for sender and receiver endpoints.
- Replaced ReadEnvelopeReceiverQuery with ReadDocumentQuery for sender document retrieval; simplified response logic.
- Added a new endpoint for fully authenticated receivers to fetch documents by envelope ID from user claims.
- Refactored ReadDocumentQuery and handler to always return DocumentDto, throw NotFoundException when needed, and use _repo.Query.
- Cleaned up using directives and removed legacy error handling and logging.
2026-02-03 09:48:33 +01:00
1b840f4ae3 Refactor AuthController to use primary constructor
Refactored AuthController to use C# 12 primary constructor syntax for ILogger<AuthController> injection. Removed obsolete IUserService and IDirectorySearchService dependencies, their fields, and the old constructor. This streamlines the controller and prepares it for MediatR-based service handling.
2026-02-02 16:29:31 +01:00
3923a3b403 Refactor claim retrieval with GetRequiredClaimOfSender
Added a private extension method GetRequiredClaimOfSender to ClaimsPrincipal for retrieving the first available value from multiple claim types, throwing a detailed exception if none are found. Refactored GetId to use this method, improving code reuse and clarity when handling user claims.
2026-02-02 16:27:45 +01:00
ada621ac46 Refactor claim access to enforce required user claims
Replaced nullable claim accessors with strict versions that throw exceptions if required claims are missing or invalid. Updated controller logic to use new methods and removed fallback/error handling for missing claims, ensuring stricter claim validation throughout the codebase.
2026-02-02 16:17:53 +01:00
abbe6a26a9 Refactor ControllerExtensions to SenderClaimExtensions
Renamed the extension class for claims handling and added a private GetRequiredClaimOfSender method for ClaimsPrincipal. This method throws a detailed exception when a required claim is missing, improving error reporting and debugging.
2026-02-02 16:11:29 +01:00
3066dac541 Rename EnvelopeAuthExtensions to ReceiverClaimExtensions
Refactored the class name in ReceiverClaimExtensions.cs to better reflect its focus on receiver-specific claim extension methods rather than general envelope authentication. No functional changes were made.
2026-02-02 15:58:47 +01:00
b1aa6d6639 Refactor claim extraction methods for receiver context
Renamed authentication-related extension methods to clarify that they extract claims for the "receiver" context (e.g., GetAuthReceiverSignature → GetReceiverSignatureOfReceiver). Updated all usages in AnnotationController and ReadOnlyController. Also renamed the helper method GetRequiredClaim to GetRequiredClaimOfReceiver for improved clarity and reduced ambiguity.
2026-02-02 15:58:07 +01:00
31fe1c34f2 Remove GetClaimValue extension from EnvelopeAuthExtensions
The GetClaimValue method, which delegated to GetRequiredClaim for retrieving claim values by type, has been removed from the EnvelopeAuthExtensions class. Other functionality in the class remains unchanged.
2026-02-02 15:13:10 +01:00
d7644bfe07 Move ClaimsPrincipal extensions to API.Extensions namespace
Refactored ControllerExtensions: moved user claim extraction
methods from EnvelopeGenerator.API.Controllers to the new
EnvelopeGenerator.API.Extensions namespace. Updated all
references and using statements accordingly. No logic changes;
improves code organization and clarity.
2026-02-02 15:07:27 +01:00
4759b16a85 Mark GetAnnotationParams as obsolete (PSPDF Kit deprecated)
Added [Obsolete] attribute to GetAnnotationParams in ConfigController to indicate that PSPDF Kit will no longer be used and the method is deprecated. This warns developers to avoid using this method in future development.
2026-02-02 15:05:15 +01:00
cfdfb43631 Restrict annotation endpoints to Receiver.FullyAuth role
Updated [Authorize] attributes to require Receiver.FullyAuth role on AnnotationController and relevant methods. Removed redundant claim checks now enforced by role-based authorization. Clarified [Obsolete] message for PSPDF Kit endpoint.
2026-02-02 14:55:44 +01:00
6254bb6e3f Update auth role and envelopeId check in CreateAsync
Changed [Authorize] to require Receiver.FullyAuth role for CreateAsync, restricting access to receiver users. Removed explicit null check and logging for envelopeId claim, allowing the method to proceed without this validation.
2026-02-02 14:55:10 +01:00
f995fa9fc3 Refactor claim accessors to enforce required claims
Refactored EnvelopeAuthExtensions to require presence of all key authentication claims. Added GetRequiredClaim helper that throws detailed exceptions if claims are missing or invalid, replacing nullable return types with non-nullable ones. This ensures authentication logic fails fast and provides clearer error messages when claims are misconfigured or tampered with.
2026-02-02 14:54:59 +01:00
c2fefe798d Add Sender constant to Role in Domain.Constants
Added a new Sender constant to the Role class within the EnvelopeGenerator.Domain.Constants namespace, allowing it to be used alongside existing Receiver role constants.
2026-02-02 11:58:30 +01:00
849a282ec5 Refactor Role constants; add Receiver class, mark obsolete
Refactored the Role class by introducing a nested static Receiver class containing PreAuth and FullyAuth constants. Marked the original Role.PreAuth and Role.FullyAuth as [Obsolete] with guidance to use the new Receiver constants. Added a conditional using directive for System for NETFRAMEWORK compatibility.
2026-02-02 11:55:17 +01:00
6b23dcdba7 Refactor: unify role constants under new Role class
Replaced all usages of ReceiverRole with the new Role class in EnvelopeGenerator.Domain.Constants. Removed ReceiverRole.cs and added Role.cs with PreAuth and FullyAuth constants. Updated all [Authorize] attributes and role checks in controllers and authentication logic to use Role.FullyAuth and Role.PreAuth. This centralizes role management for improved maintainability and clarity.
2026-02-02 11:53:26 +01:00
a60d0f63e2 Update CORS origins and authentication config values
Expanded AllowedOrigins for CORS to include additional URLs. Updated AuthClientParams URL and adjusted Audience values in both AuthClientParams and AuthTokenKeys to use sign-flow.digitaldata.works. No other settings were changed.
2026-02-02 11:43:15 +01:00
2481059b49 Update AuthClientParams URL and Audience in dev settings
Changed AuthClientParams "Url" to use the new endpoint (http://172.24.12.39:9090/auth-hub) and updated the "Audience" in "PublicKeys" from "sign-flow-gen.digitaldata.works" to "sign-flow.digitaldata.works" in appsettings.Development.json.
2026-02-02 11:42:17 +01:00
6334097d5e Remove CommonServices project reference from API
The EnvelopeGenerator.API.csproj file no longer includes a direct ProjectReference to EnvelopeGenerator.CommonServices.vbproj. This change decouples the API project from the CommonServices project.
2026-02-02 10:30:09 +01:00
9baa126c8c Update LocalizationController namespace and localizer types
Changed namespace to EnvelopeGenerator.API.Controllers. Updated _mLocalizer and its constructor parameter to use IStringLocalizer<Resource> instead of IStringLocalizer<Model>. Removed unused EnvelopeGenerator.CommonServices using directive.
2026-02-02 10:29:44 +01:00
1ef46013cd Remove EnvelopeGenerator.Terminal from solution
EnvelopeGenerator.Terminal project and all related configuration
and solution folder mappings have been removed from
EnvelopeGenerator.sln. This cleans up the solution by excluding
the Terminal project and its build settings.
2026-02-02 10:26:12 +01:00
72dffd1043 Update SQL to use User.GetId() for current user context
Replaced usage of the userId variable with User.GetId() when formatting the SQL query in EnvelopeReceiverController. This ensures the user ID is dynamically retrieved from the authenticated user context, improving accuracy and security.
2026-02-02 10:17:55 +01:00
eda30472b9 No changes detected in diff
No code was added or removed in the provided diff; only context lines were present.
2026-02-02 10:14:15 +01:00
75846573da Add XML docs and standardize repository access patterns
Added XML documentation to extension and handler classes for improved maintainability. Refactored repository access to use .Query instead of .ReadOnly() for consistency. Updated async extension methods for better readability and error handling.
2026-02-02 10:07:50 +01:00
f59c0d90ad Refactor namespaces to EnvelopeGenerator.API
Replaced all EnvelopeGenerator.GeneratorAPI namespaces with EnvelopeGenerator.API across controllers, models, extensions, middleware, and annotation-related files. Updated using/import statements and namespace declarations accordingly. Added wwwroot folder to project file. Minor code adjustments made for consistency. This unifies API naming for improved clarity and maintainability.
2026-02-02 10:00:21 +01:00
cf40449112 remove angular website 2026-02-02 09:56:31 +01:00
a59d4836d4 Add EnvelopeGenerator.API project, remove GeneratorAPI
Added EnvelopeGenerator.API project to the solution with appropriate build configurations and folder placement. Removed the EnvelopeGenerator.GeneratorAPI project and all related solution references and configurations.
2026-01-30 15:12:28 +01:00
f475cf4ea9 Remove dotnet-ef tool config and IIS publish profiles
Deleted dotnet-tools.json (dotnet-ef config) and IIS publish profiles for .NET 7 and .NET 9 (IISProfileNet7Win64.pubxml, IISProfileNet9Win64.pubxml) to clean up unused deployment and tooling files.
2026-01-30 15:12:10 +01:00
185 changed files with 765 additions and 812 deletions

View File

@@ -6,19 +6,19 @@ using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using EnvelopeGenerator.API.Extensions;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Manages annotations and signature lifecycle for envelopes.
/// </summary>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Policy = AuthPolicy.Receiver)]
[ApiController]
[Route("api/[controller]")]
public class AnnotationController : ControllerBase
@@ -54,19 +54,13 @@ public class AnnotationController : ControllerBase
/// </summary>
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
/// <param name="cancel">Cancellation token.</param>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpPost]
[Obsolete("This endpoint is for PSPDF Kit.")]
[Obsolete("PSPDF Kit will no longer be used.")]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{
var signature = User.GetAuthReceiverSignature();
var uuid = User.GetAuthEnvelopeUuid();
if (signature is null || uuid is null)
{
_logger.LogError("Authorization failed: authenticated user does not have a valid signature or envelope UUID.");
return Unauthorized("User authentication is incomplete. Missing required claims for processing this request.");
}
var signature = User.GetReceiverSignatureOfReceiver();
var uuid = User.GetEnvelopeUuidOfReceiver();
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
@@ -93,20 +87,14 @@ public class AnnotationController : ControllerBase
/// Rejects the document for the current receiver.
/// </summary>
/// <param name="reason">Optional rejection reason.</param>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpPost("reject")]
[Obsolete("Use MediatR")]
public async Task<IActionResult> Reject([FromBody] string? reason = null)
{
var signature = User.GetAuthReceiverSignature();
var uuid = User.GetAuthEnvelopeUuid();
var mail = User.GetAuthReceiverMail();
if (uuid is null || signature is null || mail is null)
{
_logger.LogEnvelopeError(uuid: uuid, signature: signature,
message: @$"Unauthorized POST request in api\\envelope\\reject. One of claims, Envelope, signature or mail ({mail}) is null.");
return Unauthorized();
}
var signature = User.GetReceiverSignatureOfReceiver();
var uuid = User.GetEnvelopeUuidOfReceiver();
var mail = User.GetReceiverMailOfReceiver();
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);

View File

@@ -0,0 +1,76 @@
using EnvelopeGenerator.API.Controllers.Interfaces;
using EnvelopeGenerator.API.Models;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.API.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(IOptions<AuthTokenKeys> authTokenKeyOptions, IAuthorizationService authService) : ControllerBase, IAuthController
{
private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value;
/// <summary>
///
/// </summary>
public IAuthorizationService AuthService { get; } = authService;
/// <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(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
Response.Cookies.Delete(authTokenKeys.Cookie);
else if (await this.IsUserInPolicyAsync(AuthPolicy.ReceiverOrReceiverTFA))
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
else
return Unauthorized();
return Ok();
}
/// <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(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[HttpGet("check")]
[Authorize]
public IActionResult Check(string? role = null)
=> role is not null && !User.IsInRole(role)
? Unauthorized()
: Ok();
}

View File

@@ -1,9 +1,9 @@
using EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Exposes configuration data required by the client applications.
@@ -22,6 +22,7 @@ public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParams
/// Returns annotation configuration that was previously rendered by MVC.
/// </summary>
[HttpGet("Annotations")]
[Obsolete("PSPDF Kit will no longer be used.")]
public IActionResult GetAnnotationParams()
{
return Ok(_annotationParams.AnnotationJSObject);

View File

@@ -0,0 +1,63 @@
using EnvelopeGenerator.API.Controllers.Interfaces;
using EnvelopeGenerator.API.Extensions;
using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Provides access to envelope documents for authenticated receivers.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="DocumentController"/> class.
/// </remarks>
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class DocumentController(IMediator mediator, IAuthorizationService authService) : ControllerBase, IAuthController
{
/// <summary>
///
/// </summary>
public IAuthorizationService AuthService => authService;
/// <summary>
/// Returns the document bytes receiver.
/// </summary>
/// <param name="query">Encoded envelope key.</param>
/// <param name="cancel">Cancellation token.</param>
[HttpGet]
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
{
// Sender: expects query with envelope key
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
{
if (query is null)
return BadRequest("Missing document query.");
var senderDoc = await mediator.Send(query, cancel);
return senderDoc.ByteData is byte[] senderDocByte
? File(senderDocByte, "application/octet-stream")
: NotFound("Document is empty.");
}
// Receiver: resolve envelope id from claims
if (await this.IsUserInPolicyAsync(AuthPolicy.Receiver))
{
if (query is not null)
return BadRequest("Query parameters are not allowed for receiver role.");
var envelopeId = User.GetEnvelopeIdOfReceiver();
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
return receiverDoc.ByteData is byte[] receiverDocByte
? File(receiverDocByte, "application/octet-stream")
: NotFound("Document is empty.");
}
return Unauthorized();
}
}

View File

@@ -11,39 +11,26 @@ using EnvelopeGenerator.Application.Common.Dto;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Controller for managing temp templates.
/// Steuerung zur Verwaltung von E-Mail-Vorlagen.
/// </summary>
/// <remarks>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </remarks>
/// <param name="mapper">
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class EmailTemplateController : ControllerBase
[Authorize(Policy = AuthPolicy.Sender)]
public class EmailTemplateController(IMapper mapper, IRepository<EmailTemplate> repository, IMediator mediator) : ControllerBase
{
private readonly IMapper _mapper;
private readonly IRepository<EmailTemplate> _repository;
private readonly IMediator _mediator;
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </summary>
/// <param name="mapper">
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
[Obsolete("Use MediatR")]
public EmailTemplateController(IMapper mapper, IRepository<EmailTemplate> repository, IMediator mediator)
{
_mapper = mapper;
_repository = repository;
_mediator = mediator;
}
/// <summary>
/// Ruft E-Mail-Vorlagen basierend auf der angegebenen Abfrage ab.
/// Gibt alles zurück, wenn keine Id- oder Typ-Informationen eingegeben wurden.
@@ -63,12 +50,12 @@ public class EmailTemplateController : ControllerBase
{
if (emailTemplate is null || (emailTemplate.Id is null && emailTemplate.Type is null))
{
var temps = await _repository.Query.ToListAsync();
return Ok(_mapper.Map<IEnumerable<EmailTemplateDto>>(temps));
var temps = await repository.Query.ToListAsync();
return Ok(mapper.Map<IEnumerable<EmailTemplateDto>>(temps));
}
else
{
var temp = await _mediator.Send(emailTemplate);
var temp = await mediator.Send(emailTemplate);
return temp is null ? NotFound() : Ok(temp);
}
}
@@ -95,11 +82,11 @@ public class EmailTemplateController : ControllerBase
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
[HttpPut]
public async Task<IActionResult> Update([FromQuery] EmailTemplateQuery? temp = null, [FromBody] UpdateEmailTemplateCommand? update = null)
public async Task<IActionResult> Update([FromQuery] EmailTemplateQueryBase? temp = null, [FromBody] UpdateEmailTemplateCommand? update = null)
{
if (update is null)
{
await _mediator.Send(new ResetEmailTemplateCommand(temp));
await mediator.Send(new ResetEmailTemplateCommand(temp));
return Ok();
}
else if (temp is null)
@@ -109,7 +96,7 @@ public class EmailTemplateController : ControllerBase
else
{
update.EmailTemplateQuery = temp;
await _mediator.Send(update);
await mediator.Send(update);
return Ok();
}
}

View File

@@ -1,11 +1,12 @@
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.API.Extensions;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Envelopes.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.

View File

@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Envelopes.Queries;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.GeneratorAPI.Models;
using EnvelopeGenerator.API.Models;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -13,8 +13,9 @@ using System.Data;
using EnvelopeGenerator.Application.Common.SQL;
using EnvelopeGenerator.Application.Common.Dto.Receiver;
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
using EnvelopeGenerator.API.Extensions;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Controller für die Verwaltung von Umschlagempfängern.
@@ -65,16 +66,7 @@ public class EnvelopeReceiverController : ControllerBase
[HttpGet]
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
{
var username = User.GetUsernameOrDefault();
if (username is null)
{
_logger.LogError(@"Envelope Receiver dto cannot be sent because username claim is null. Potential authentication and authorization error. The value of other claims are [id: {id}], [username: {username}], [name: {name}], [prename: {prename}], [email: {email}].",
User.GetId(), User.GetUsernameOrDefault(), User.GetNameOrDefault(), User.GetPrenameOrDefault(), User.GetEmailOrDefault());
return StatusCode(StatusCodes.Status500InternalServerError);
}
envelopeReceiver = envelopeReceiver with { Username = username };
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
var result = await _mediator.Send(envelopeReceiver);
@@ -225,20 +217,16 @@ public class EnvelopeReceiverController : ControllerBase
using (SqlConnection conn = new(_cnnStr))
{
conn.Open();
var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), userId.ToSqlParam());
using (SqlCommand cmd = new SqlCommand(formattedSQL_hist, conn))
{
var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), User.GetId().ToSqlParam());
using SqlCommand cmd = new(formattedSQL_hist, conn);
cmd.CommandType = CommandType.Text;
using (SqlDataReader reader = cmd.ExecuteReader())
{
using SqlDataReader reader = cmd.ExecuteReader();
if (reader.Read())
{
bool outSuccess = reader.GetBoolean(0);
}
}
}
}
#endregion
return Ok(res);

View File

@@ -6,7 +6,7 @@ using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.Common.Extensions;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
@@ -113,6 +113,6 @@ public class HistoryController : ControllerBase
public async Task<IActionResult> GetAllAsync([FromQuery] ReadHistoryQuery historyQuery, CancellationToken cancel)
{
var history = await _mediator.Send(historyQuery, cancel).ThrowIfEmpty(Exceptions.NotFound);
return Ok((historyQuery.OnlyLast ?? false) ? history.MaxBy(h => h.AddedWhen) : history);
return Ok((historyQuery.OnlyLast) ? history.MaxBy(h => h.AddedWhen) : history);
}
}

View File

@@ -0,0 +1,38 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
namespace EnvelopeGenerator.API.Controllers.Interfaces;
/// <summary>
///
/// </summary>
public interface IAuthController
{
/// <summary>
///
/// </summary>
IAuthorizationService AuthService { get; }
/// <summary>
///
/// </summary>
ClaimsPrincipal User { get; }
}
/// <summary>
///
/// </summary>
public static class AuthControllerExtensions
{
/// <summary>
///
/// </summary>
/// <param name="controller"></param>
/// <param name="policyName"></param>
/// <returns></returns>
public static async Task<bool> IsUserInPolicyAsync(this IAuthController controller, string policyName)
{
var result = await controller.AuthService.AuthorizeAsync(controller.User, policyName);
return result.Succeeded;
}
}

View File

@@ -1,12 +1,12 @@
using DigitalData.Core.API;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.CommonServices;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
using EnvelopeGenerator.Application.Resources;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
@@ -19,7 +19,7 @@ public class LocalizationController : ControllerBase
private static readonly Guid L_KEY = Guid.NewGuid();
private readonly ILogger<LocalizationController> _logger;
private readonly IStringLocalizer<Model> _mLocalizer;
private readonly IStringLocalizer<Resource> _mLocalizer;
private readonly IStringLocalizer<Resource> _localizer;
private readonly IMemoryCache _cache;
@@ -34,7 +34,7 @@ public class LocalizationController : ControllerBase
ILogger<LocalizationController> logger,
IStringLocalizer<Resource> localizer,
IMemoryCache memoryCache,
IStringLocalizer<Model> _modelLocalizer)
IStringLocalizer<Resource> _modelLocalizer)
{
_logger = logger;
_localizer = localizer;

View File

@@ -2,12 +2,12 @@ using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using EnvelopeGenerator.API.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Manages read-only envelope sharing flows.
@@ -37,22 +37,17 @@ public class ReadOnlyController : ControllerBase
/// </summary>
/// <param name="createDto">Creation payload.</param>
[HttpPost]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Policy = AuthPolicy.Receiver)]
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
{
var authReceiverMail = User.GetAuthReceiverMail();
var authReceiverMail = User.GetReceiverMailOfReceiver();
if (authReceiverMail is null)
{
_logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
return Unauthorized();
}
var envelopeId = User.GetAuthEnvelopeId();
if (envelopeId is null)
{
_logger.LogError("Envelope Id claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto));
return Unauthorized();
}
var envelopeId = User.GetEnvelopeIdOfReceiver();
createDto.AddedWho = authReceiverMail;
createDto.EnvelopeId = envelopeId;

View File

@@ -3,8 +3,7 @@ using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.Resources;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.GeneratorAPI.Extensions;
using EnvelopeGenerator.GeneratorAPI.Models;
using EnvelopeGenerator.API.Models;
using Ganss.Xss;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
@@ -13,7 +12,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
namespace EnvelopeGenerator.API.Controllers;
/// <summary>
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
@@ -112,7 +111,7 @@ public class TfaRegistrationController : ControllerBase
/// <summary>
/// Logs out the envelope receiver from cookie authentication.
/// </summary>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpPost("auth/logout")]
public async Task<IActionResult> LogOutAsync()
{

View File

@@ -0,0 +1,70 @@
using EnvelopeGenerator.API.Models;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace EnvelopeGenerator.API.Documentation;
/// <summary>
///
/// </summary>
public sealed class AuthProxyDocumentFilter : IDocumentFilter
{
/// <summary>
///
/// </summary>
/// <param name="swaggerDoc"></param>
/// <param name="context"></param>
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
const string path = "/api/auth";
var loginSchema = context.SchemaGenerator.GenerateSchema(typeof(Login), context.SchemaRepository);
var loginExample = new OpenApiObject
{
["password"] = new OpenApiString(""),
["username"] = new OpenApiString("")
};
var operation = new OpenApiOperation
{
Summary = "Proxy login (auth-hub)",
Description = "Proxies the request to the auth service. Add query parameter `cookie=true|false`.",
Tags = [new() { Name = "Auth" }],
Parameters =
{
new OpenApiParameter
{
Name = "cookie",
In = ParameterLocation.Query,
Required = false,
Schema = new OpenApiSchema { Type = "boolean", Default = new OpenApiBoolean(true) },
Example = new OpenApiBoolean(true),
Description = "If true, auth service sets the auth cookie."
}
},
RequestBody = new OpenApiRequestBody
{
Required = true,
Content =
{
["application/json"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample },
["multipart/form-data"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample }
}
},
Responses =
{
["200"] = new OpenApiResponse { Description = "OK (proxied response)" },
["401"] = new OpenApiResponse { Description = "Unauthorized" }
}
};
swaggerDoc.Paths[path] = new OpenApiPathItem
{
Operations =
{
[OperationType.Post] = operation
}
};
}
}

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.API;
/// <summary>
/// Provides custom claim types for envelope-related information.
/// </summary>
public static class EnvelopeClaimTypes
{
/// <summary>
/// Claim type for the title of an envelope.
/// </summary>
public static readonly string Title = $"Envelope{nameof(Title)}";
/// <summary>
/// Claim type for the ID of an envelope.
/// </summary>
public static readonly string Id = $"Envelope{nameof(Id)}";
}

View File

@@ -17,6 +17,17 @@
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ClientApp\**" />
<Content Remove="ClientApp\**" />
<EmbeddedResource Remove="ClientApp\**" />
<None Remove="ClientApp\**" />
</ItemGroup>
<ItemGroup>
<None Include="yarp.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspNetCore.Scalar" Version="1.1.8" />
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
@@ -28,6 +39,7 @@
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
@@ -49,41 +61,13 @@
</ItemGroup>
<ItemGroup>
<Folder Include="ClientApp\" />
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj" />
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\chunk-35PBLQEP.js">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\chunk-IUSOII6E.js">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\favicon.ico">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\index.html">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\login\index.html">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\main-ZN7C4PGY.js">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\polyfills-N6LQB2YD.js">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="ClientApp\envelope-generator-ui\dist\envelope-generator-ui\browser\styles-S5ZWIC2V.css">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,55 +1,69 @@
using System.Linq;
using System.Security.Claims;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace EnvelopeGenerator.GeneratorAPI.Extensions;
namespace EnvelopeGenerator.API.Extensions;
/// <summary>
/// Provides helper methods for working with envelope-specific authentication claims.
/// </summary>
public static class EnvelopeAuthExtensions
public static class ReceiverClaimExtensions
{
/// <summary>
/// Retrieves a claim value by type.
/// </summary>
/// <param name="user">The current claims principal.</param>
/// <param name="claimType">The claim type to resolve.</param>
/// <returns>The claim value or null when missing.</returns>
public static string? GetClaimValue(this ClaimsPrincipal user, string claimType) => user.FindFirstValue(claimType);
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, string claimType)
{
var value = user.FindFirstValue(claimType);
if (value is not null)
{
return value;
}
var identity = user.Identity;
var principalName = identity?.Name ?? "(anonymous)";
var authType = identity?.AuthenticationType ?? "(none)";
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
throw new InvalidOperationException(message);
}
/// <summary>
/// Gets the authenticated envelope UUID from the claims.
/// </summary>
public static string? GetAuthEnvelopeUuid(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.NameIdentifier);
public static string GetEnvelopeUuidOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.NameIdentifier);
/// <summary>
/// Gets the authenticated receiver signature from the claims.
/// </summary>
public static string? GetAuthReceiverSignature(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Hash);
public static string GetReceiverSignatureOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Hash);
/// <summary>
/// Gets the authenticated receiver display name from the claims.
/// </summary>
public static string? GetAuthReceiverName(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Name);
public static string GetReceiverNameOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Name);
/// <summary>
/// Gets the authenticated receiver email address from the claims.
/// </summary>
public static string? GetAuthReceiverMail(this ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.Email);
public static string GetReceiverMailOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Email);
/// <summary>
/// Gets the authenticated envelope title from the claims.
/// </summary>
public static string? GetAuthEnvelopeTitle(this ClaimsPrincipal user) => user.FindFirstValue(EnvelopeClaimTypes.Title);
public static string GetEnvelopeTitleOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Title);
/// <summary>
/// Gets the authenticated envelope identifier from the claims.
/// </summary>
public static int? GetAuthEnvelopeId(this ClaimsPrincipal user)
public static int GetEnvelopeIdOfReceiver(this ClaimsPrincipal user)
{
var envIdStr = user.FindFirstValue(EnvelopeClaimTypes.Id);
return int.TryParse(envIdStr, out var envId) ? envId : null;
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Id);
if (!int.TryParse(envIdStr, out var envId))
{
throw new InvalidOperationException($"Claim '{EnvelopeClaimTypes.Id}' is not a valid integer.");
}
return envId;
}
/// <summary>

View File

@@ -0,0 +1,95 @@
using System.Security.Claims;
namespace EnvelopeGenerator.API.Extensions
{
/// <summary>
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
/// </summary>
public static class SenderClaimExtensions
{
private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, string claimType)
{
var value = user.FindFirstValue(claimType);
if (value is not null)
{
return value;
}
var identity = user.Identity;
var principalName = identity?.Name ?? "(anonymous)";
var authType = identity?.AuthenticationType ?? "(none)";
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
throw new InvalidOperationException(message);
}
private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, params string[] claimTypes)
{
string? value = null;
foreach (var claimType in claimTypes)
{
value = user.FindFirstValue(claimType);
if (value is not null)
return value;
}
var identity = user.Identity;
var principalName = identity?.Name ?? "(anonymous)";
var authType = identity?.AuthenticationType ?? "(none)";
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
var message = $"Required claim among [{string.Join(", ", claimTypes)}] is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
throw new InvalidOperationException(message);
}
/// <summary>
/// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The user's ID as an integer.</returns>
/// <exception cref="InvalidOperationException">Thrown if the user ID claim is missing or invalid.</exception>
public static int GetId(this ClaimsPrincipal user)
{
var idValue = user.GetRequiredClaimOfSender(ClaimTypes.NameIdentifier, "sub");
if (!int.TryParse(idValue, out var result))
{
throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token.");
}
return result;
}
/// <summary>
/// Retrieves the username from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The username as a string.</returns>
public static string GetUsername(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.Name);
/// <summary>
/// Retrieves the user's surname (last name) from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The surname as a string.</returns>
public static string GetName(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.Surname);
/// <summary>
/// Retrieves the user's given name (first name) from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The given name as a string.</returns>
public static string GetPrename(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.GivenName);
/// <summary>
/// Retrieves the user's email address from the claims.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The email address as a string.</returns>
public static string GetEmail(this ClaimsPrincipal user)
=> user.GetRequiredClaimOfSender(ClaimTypes.Email);
}
}

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Middleware;
namespace EnvelopeGenerator.API.Middleware;
using DigitalData.Core.Exceptions;
using Microsoft.AspNetCore.Http;

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
/// <summary>
/// Represents the keys and default values used for authentication token handling
@@ -24,5 +24,5 @@ public class AuthTokenKeys
/// <summary>
/// Gets the expected audience value for the authentication token.
/// </summary>
public string Audience { get; init; } = "sign-flow-gen.digitaldata.works";
public string Audience { get; init; } = "sign-flow.digitaldata.works";
}

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
/// <summary>
///

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models
namespace EnvelopeGenerator.API.Models
{
/// <summary>
/// Represents a hyperlink for contact purposes with various HTML attributes.

View File

@@ -1,6 +1,6 @@
using System.Globalization;
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
public class Culture
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
public class Cultures : List<Culture>
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
public class CustomImages : Dictionary<string, Image>
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
public class ErrorViewModel
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
public class Image
{

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
/// <summary>
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
public class MainViewModel
{

View File

@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
public record Annotation : IAnnotation
{

View File

@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
public class AnnotationParams
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
/// <summary>
/// The Background is an annotation for the PSPDF Kit. However, it has no function.

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
public record Color
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
public static class Extensions
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models.PsPdfKitAnnotation;
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
public interface IAnnotation
{

View File

@@ -1,4 +1,4 @@
namespace EnvelopeGenerator.GeneratorAPI.Models;
namespace EnvelopeGenerator.API.Models;
/// <summary>
/// Represents the parameters for two-factor authentication (2FA) registration.

View File

@@ -1,6 +1,7 @@
using DigitalData.Core.API;
using DigitalData.Core.Application;
using EnvelopeGenerator.Infrastructure;
using EnvelopeGenerator.Domain.Constants;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
@@ -11,11 +12,11 @@ using DigitalData.UserManager.DependencyInjection;
using EnvelopeGenerator.Application;
using DigitalData.Auth.Client;
using DigitalData.Core.Abstractions;
using EnvelopeGenerator.GeneratorAPI.Models;
using EnvelopeGenerator.API.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using DigitalData.Core.Abstractions.Security.Extensions;
using EnvelopeGenerator.GeneratorAPI.Middleware;
using EnvelopeGenerator.API.Middleware;
using NLog.Web;
using NLog;
@@ -26,6 +27,8 @@ try
{
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true);
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
if (!builder.Environment.IsDevelopment())
@@ -39,6 +42,8 @@ try
var deferredProvider = new DeferredServiceProvider();
builder.Services.AddControllers();
builder.Services.AddHttpClient();
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
// CORS Policy
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
@@ -89,7 +94,7 @@ try
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
@@ -102,6 +107,8 @@ try
{
options.IncludeXmlComments(xmlFile);
}
options.DocumentFilter<EnvelopeGenerator.API.Documentation.AuthProxyDocumentFilter>();
});
builder.Services.AddOpenApi();
@@ -170,6 +177,16 @@ try
options.SlidingExpiration = true;
});
builder.Services.AddAuthorizationBuilder()
.AddPolicy(AuthPolicy.SenderOrReceiver, policy =>
policy.RequireRole(Role.Sender, Role.Receiver.Full))
.AddPolicy(AuthPolicy.Sender, policy =>
policy.RequireRole(Role.Sender))
.AddPolicy(AuthPolicy.Receiver, policy =>
policy.RequireRole(Role.Receiver.Full))
.AddPolicy(AuthPolicy.ReceiverTFA, policy =>
policy.RequireRole(Role.Receiver.TFA));
// User manager
#pragma warning disable CS0618 // Type or member is obsolete
builder.Services.AddUserManager<EGDbContext>();
@@ -241,6 +258,7 @@ try
app.UseAuthentication();
app.UseAuthorization();
app.MapReverseProxy();
app.MapControllers();
app.Run();

View File

@@ -6,11 +6,11 @@
}
},
"AuthClientParams": {
"Url": "https://localhost:7192/auth-hub",
"Url": "http://172.24.12.39:9090/auth-hub",
"PublicKeys": [
{
"Issuer": "auth.digitaldata.works",
"Audience": "sign-flow-gen.digitaldata.works"
"Audience": "sign-flow.digitaldata.works"
}
],
"RetryDelay": "00:00:05"

View File

@@ -9,7 +9,7 @@
}
},
"AllowedHosts": "*",
"AllowedOrigins": [ "http://localhost:4200" ],
"AllowedOrigins": [ "http://localhost:4200", "http://172.24.12.39:9090", "https://localhost:8088", "http://localhost:5131", "http://localhost:7192" ],
"ConnectionStrings": {
"Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;",
"DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
@@ -24,11 +24,11 @@
}
},
"AuthClientParams": {
"Url": "https://localhost:7192/auth-hub",
"Url": "http://172.24.12.39:9090/auth-hub",
"PublicKeys": [
{
"Issuer": "auth.digitaldata.works",
"Audience": "sign-flow-gen.digitaldata.works"
"Audience": "sign-flow.digitaldata.works"
}
],
"RetryDelay": "00:00:05"
@@ -37,7 +37,7 @@
"Cookie": "AuthToken",
"QueryString": "AuthToken",
"Issuer": "auth.digitaldata.works",
"Audience": "work-flow.digitaldata.works"
"Audience": "sign-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.

View File

@@ -0,0 +1,25 @@
{
"ReverseProxy": {
"Routes": {
"auth-login": {
"ClusterId": "auth-hub",
"Match": {
"Path": "/api/auth",
"Methods": [ "POST" ]
},
"Transforms": [
{ "PathSet": "/api/auth/sign-flow" }
]
}
},
"Clusters": {
"auth-hub": {
"Destinations": {
"primary": {
"Address": "http://172.24.12.39:9090"
}
}
}
}
}
}

View File

@@ -1,17 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Common.Dto;
namespace EnvelopeGenerator.Application.Common.Dto
/// <summary>
///
/// </summary>
public record EmailTemplateDto
{
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record EmailTemplateDto
{
/// <summary>
///
/// </summary>
public int Id{ get; init; }
public int Id { get; init; }
/// <summary>
///
@@ -19,13 +16,22 @@ namespace EnvelopeGenerator.Application.Common.Dto
public required string Name { get; init; }
/// <summary>
///
/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde.
/// </summary>
public required string Body { get; set; }
public DateTime AddedWhen { get; init; }
/// <summary>
///
/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein.
/// </summary>
public required string Subject { get; set; }
};
}
public string? Body { get; init; }
/// <summary>
/// Der Betreff der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Subject { get; init; }
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.
/// </summary>
public DateTime? ChangedWhen { get; init; }
};

View File

@@ -3,8 +3,19 @@ using System.Text;
namespace EnvelopeGenerator.Application.Common.Extensions
{
/// <summary>
///
/// </summary>
public static class LoggerExtensions
{
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="envelopeReceiverId"></param>
/// <param name="exception"></param>
/// <param name="message"></param>
/// <param name="args"></param>
public static void LogEnvelopeError(this ILogger logger, string envelopeReceiverId, Exception? exception = null, string? message = null, params object?[] args)
{
var sb = new StringBuilder().AppendLine(envelopeReceiverId.DecodeEnvelopeReceiverId().ToTitle());
@@ -18,6 +29,15 @@ namespace EnvelopeGenerator.Application.Common.Extensions
logger.Log(LogLevel.Error, exception, sb.AppendLine(exception.Message).ToString(), args);
}
/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="uuid"></param>
/// <param name="signature"></param>
/// <param name="exception"></param>
/// <param name="message"></param>
/// <param name="args"></param>
public static void LogEnvelopeError(this ILogger logger, string? uuid, string? signature = null, Exception? exception = null, string? message = null, params object?[] args)
{
var sb = new StringBuilder($"Envelope Uuid: {uuid}");
@@ -34,6 +54,11 @@ namespace EnvelopeGenerator.Application.Common.Extensions
logger.Log(LogLevel.Error, exception, sb.ToString(), args);
}
/// <summary>
///
/// </summary>
/// <param name="envelopeReceiverTuple"></param>
/// <returns></returns>
public static string ToTitle(this (string? UUID, string? Signature) envelopeReceiverTuple)
{
return $"UUID is {envelopeReceiverTuple.UUID} and signature is {envelopeReceiverTuple.Signature}";

View File

@@ -2,16 +2,42 @@
using Microsoft.Extensions.Localization;
using System.Text.Encodings.Web;
namespace EnvelopeGenerator.Application.Common.Extensions
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
///
/// </summary>
public static class XSSExtensions
{
public static class XSSExtensions
{
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="encoder"></param>
/// <returns></returns>
public static string? TryEncode(this string? value, UrlEncoder encoder) => value is null ? value : encoder.Encode(value);
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="encoder"></param>
/// <returns></returns>
public static string? TryEncode(this LocalizedString? value, UrlEncoder encoder) => value is null ? null : encoder.Encode(value);
/// <summary>
///
/// </summary>
/// <param name="html"></param>
/// <param name="sanitizer"></param>
/// <returns></returns>
public static string? TrySanitize(this string? html, HtmlSanitizer sanitizer) => html is null ? html : sanitizer.Sanitize(html);
/// <summary>
///
/// </summary>
/// <param name="html"></param>
/// <param name="sanitizer"></param>
/// <returns></returns>
public static string? TrySanitize(this LocalizedString? html, HtmlSanitizer sanitizer) => html is null ? null : sanitizer.Sanitize(html);
}
}

View File

@@ -4,6 +4,7 @@ using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using AutoMapper;
using EnvelopeGenerator.Application.Common.Dto;
using DigitalData.Core.Exceptions;
namespace EnvelopeGenerator.Application.Documents.Queries;
@@ -12,14 +13,14 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
/// </summary>
/// <param name="Id">The unique identifier of the document. Optional.</param>
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto?>
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
{
}
/// <summary>
/// Handles queries for reading <see cref="Document"/> data based on either the document ID or the envelope ID.
/// </summary>
public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, DocumentDto?>
public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, DocumentDto>
{
/// <summary>
/// TempRepo for accessing <see cref="Document"/> entities.
@@ -50,20 +51,19 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
/// <exception cref="InvalidOperationException">
/// Thrown when neither <see cref="ReadDocumentQuery.Id"/> nor <see cref="ReadDocumentQuery.EnvelopeId"/> is provided.
/// </exception>
public async Task<DocumentDto?> Handle(ReadDocumentQuery query, CancellationToken cancel)
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
{
if (query.Id is not null)
{
var doc = await _repo.ReadOnly().Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
var doc = await _repo.Query.Where(d => d.Id == query.Id).FirstOrDefaultAsync(cancel);
return _mapper.Map<DocumentDto>(doc);
}
else if (query.EnvelopeId is not null)
{
var doc = await _repo.ReadOnly().Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
var doc = await _repo.Query.Where(d => d.EnvelopeId == query.EnvelopeId).FirstOrDefaultAsync(cancel);
return _mapper.Map<DocumentDto>(doc);
}
throw new InvalidOperationException(
$"Invalid {nameof(ReadDocumentQuery)}: either {nameof(query.Id)} or {nameof(query.EnvelopeId)} must be provided.");
throw new NotFoundException();
}
}

View File

@@ -6,7 +6,7 @@ namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset;
/// <summary>
/// Ein Befehl zum Zurücksetzen einer E-Mail-Vorlage auf die Standardwerte.
/// Erbt von <see cref="EmailTemplateQuery"/> und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage.<br/><br/>
/// Erbt von <see cref="EmailTemplateQueryBase"/> und ermöglicht die Angabe einer optionalen ID und eines Typs der E-Mail-Vorlage.<br/><br/>
/// Beispiele:<br/>
/// 0 - DocumentReceived: Benachrichtigung über den Empfang eines Dokuments.<br/>
/// 1 - DocumentSigned: Benachrichtigung über die Unterzeichnung eines Dokuments.<br/>
@@ -19,13 +19,13 @@ namespace EnvelopeGenerator.Application.EmailTemplates.Commands.Reset;
/// 8 - DocumentRejected_REC (Für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.<br/>
/// 9 - DocumentRejected_REC_2 (Für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.<br/>
/// </summary>
public record ResetEmailTemplateCommand : EmailTemplateQuery, IRequest
public record ResetEmailTemplateCommand : EmailTemplateQueryBase, IRequest
{
/// <summary>
///
/// </summary>
/// <param name="orginal"></param>
public ResetEmailTemplateCommand(EmailTemplateQuery? orginal = null) : base(orginal ?? new())
public ResetEmailTemplateCommand(EmailTemplateQueryBase? orginal = null) : base(orginal ?? new())
{
}

View File

@@ -19,7 +19,7 @@ public record UpdateEmailTemplateCommand(string? Body = null, string? Subject =
/// Die Abfrage, die die E-Mail-Vorlage darstellt, die aktualisiert werden soll.
/// </param>
[JsonIgnore]
public EmailTemplateQuery? EmailTemplateQuery { get; set; }
public EmailTemplateQueryBase? EmailTemplateQuery { get; set; }
/// <summary>
///

View File

@@ -22,6 +22,7 @@ public class UpdateEmailTemplateCommandHandler : IRequestHandler<UpdateEmailTemp
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public UpdateEmailTemplateCommandHandler(IRepository<EmailTemplate> repository, IMapper mapper)
{
_repository = repository;
@@ -61,12 +62,6 @@ public class UpdateEmailTemplateCommandHandler : IRequestHandler<UpdateEmailTemp
throw new NotFoundException();
}
if (request.Body is not null)
tempDto.Body = request.Body;
if (request.Subject is not null)
tempDto.Subject = request.Subject;
await _repository.UpdateAsync(tempDto, t => t.Id == tempDto.Id, cancel);
}
}

View File

@@ -19,6 +19,6 @@ namespace EnvelopeGenerator.Application.EmailTemplates;
/// 8 - DocumentRejected_REC (Für den ablehnenden Empfänger): Mail an den ablehnenden Empfänger, wenn das Dokument abgelehnt wird.
/// 9 - DocumentRejected_REC_2 (Für sonstige Empfänger): Mail an andere Empfänger (Brief), wenn das Dokument abgelehnt wird.
/// </param>
public record EmailTemplateQuery(int? Id = null, EmailTemplateType? Type = null)
public record EmailTemplateQueryBase(int? Id = null, EmailTemplateType? Type = null)
{
}

View File

@@ -1,24 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
using EnvelopeGenerator.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EnvelopeGenerator.Application.EmailTemplates;
/// <summary>
///
/// </summary>
public class MappingProfile : Profile
{
/// <summary>
///
/// </summary>
public MappingProfile()
{
CreateMap<EmailTemplate, ReadEmailTemplateResponse>();
}
}

View File

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

View File

@@ -1,52 +0,0 @@
using AutoMapper;
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
/// <summary>
///
/// </summary>
public class ReadEmailTemplateQueryHandler : IRequestHandler<ReadEmailTemplateQuery, ReadEmailTemplateResponse?>
{
private readonly IMapper _mapper;
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
private readonly IEmailTemplateRepository _repository;
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </summary>
/// <param name="mapper">
/// <param name="repository">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
[Obsolete("Use Read-method returning IReadQuery<TEntity> instead.")]
public ReadEmailTemplateQueryHandler(IMapper mapper, IEmailTemplateRepository repository)
{
_mapper = mapper;
_repository = repository;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
[Obsolete("Use IRepository")]
public async Task<ReadEmailTemplateResponse?> Handle(ReadEmailTemplateQuery request, CancellationToken cancellationToken)
{
var temp = request.Id is int id
? await _repository.ReadByIdAsync(id)
: request.Type is EmailTemplateType type
? await _repository.ReadByNameAsync(type)
: throw new InvalidOperationException("Either a valid integer ID or a valid EmailTemplateType must be provided in the request.");
var res = _mapper.Map<ReadEmailTemplateResponse>(temp);
return res;
}
}

View File

@@ -1,37 +0,0 @@
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
/// <summary>
/// Stellt die Antwort für eine Abfrage von E-Mail-Vorlagen bereit.
/// </summary>
public class ReadEmailTemplateResponse
{
/// <summary>
/// Die eindeutige Kennung der E-Mail-Vorlage.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Name des Typs
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage hinzugefügt wurde.
/// </summary>
public DateTime AddedWhen { get; set; }
/// <summary>
/// Der Inhalt (Body) der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Body { get; set; }
/// <summary>
/// Der Betreff der E-Mail-Vorlage. Kann null sein.
/// </summary>
public string? Subject { get; set; }
/// <summary>
/// Das Datum und die Uhrzeit, wann die Vorlage zuletzt geändert wurde. Kann null sein.
/// </summary>
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -0,0 +1,55 @@
using AutoMapper;
using MediatR;
using EnvelopeGenerator.Application.Common.Dto;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.EmailTemplates.Queries.Read;
/// <summary>
/// Stellt eine Abfrage dar, um eine E-Mail-Vorlage zu lesen.
/// Diese Klasse erbt von <see cref="EmailTemplateQueryBase"/>.
/// </summary>
public record ReadEmailTemplateQuery : EmailTemplateQueryBase, IRequest<EmailTemplateDto?>
{
}
/// <summary>
///
/// </summary>
public class ReadEmailTemplateQueryHandler : IRequestHandler<ReadEmailTemplateQuery, EmailTemplateDto?>
{
private readonly IMapper _mapper;
private readonly IRepository<EmailTemplate> _repo;
/// <summary>
/// Initialisiert eine neue Instanz der <see cref="EmailTemplateController"/>-Klasse.
/// </summary>
/// <param name="mapper">
/// <param name="repo">
/// Die AutoMapper-Instanz, die zum Zuordnen von Objekten verwendet wird.
/// </param>
public ReadEmailTemplateQueryHandler(IMapper mapper, IRepository<EmailTemplate> repo)
{
_mapper = mapper;
_repo = repo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public async Task<EmailTemplateDto?> Handle(ReadEmailTemplateQuery request, CancellationToken cancel)
{
var query = request.Id is int id
? _repo.Query.Where(temp => temp.Id == id)
: _repo.Query.Where(temp => temp.Name == request.Type!.ToString());
return _mapper.Map<EmailTemplateDto>(await query.FirstOrDefaultAsync(cancel));
}
}

View File

@@ -67,10 +67,11 @@ public static class Extensions
/// <param name="key"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string key, CancellationToken cancel = default)
public static async Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string key, CancellationToken cancel = default)
{
var q = new ReadEnvelopeReceiverQuery() { Key = key };
return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
var envRcvs = await mediator.Send(q, cancel);
return envRcvs.FirstOrDefault();
}
/// <summary>
@@ -81,12 +82,13 @@ public static class Extensions
/// <param name="signature"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public static Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string uuid, string signature, CancellationToken cancel = default)
public static async Task<EnvelopeReceiverDto?> ReadEnvelopeReceiverAsync(this IMediator mediator, string uuid, string signature, CancellationToken cancel = default)
{
var q = new ReadEnvelopeReceiverQuery();
q.Envelope.Uuid = uuid;
q.Receiver.Signature = signature;
return mediator.Send(q, cancel).Then(envRcvs => envRcvs.FirstOrDefault());
var envRcvs = await mediator.Send(q, cancel);
return envRcvs.FirstOrDefault();
}
/// <summary>
/// Verarbeitet <see cref="ReadEnvelopeReceiverQuery"/> und liefert passende <see cref="EnvelopeReceiverDto"/>-Ergebnisse.

View File

@@ -68,6 +68,6 @@ public class ReceiverAlreadySignedQueryHandler : IRequestHandler<ReceiverAlready
/// <returns></returns>
public async Task<bool> Handle(ReceiverAlreadySignedQuery request, CancellationToken cancel = default)
{
return await _repo.ReadOnly().Where(request).Where(h => h.Status == EnvelopeStatus.DocumentSigned).AnyAsync(cancel);
return await _repo.Query.Where(request).Where(h => h.Status == EnvelopeStatus.DocumentSigned).AnyAsync(cancel);
}
}

View File

@@ -82,7 +82,7 @@ public class CreateHistoryCommandHandler : IRequestHandler<CreateHistoryCommand,
if(request.UserReference is null)
{
var receivers = await _erRepo
.ReadOnly()
.Query
.Where(request)
.Include(er => er.Receiver)
.ToListAsync(cancel);

View File

@@ -67,6 +67,7 @@ public class CreateReceiverCommandHandler : IRequestHandler<CreateReceiverComman
///
/// </summary>
/// <param name="repo"></param>
/// <param name="mapper"></param>
public CreateReceiverCommandHandler(IRepository<Receiver> repo, IMapper mapper)
{
_repo = repo;
@@ -81,7 +82,7 @@ public class CreateReceiverCommandHandler : IRequestHandler<CreateReceiverComman
/// <returns></returns>
public async Task<(ReceiverDto Receiver, bool AlreadyExists)> Handle(CreateReceiverCommand request, CancellationToken cancel)
{
var receiver = await _repo.ReadOnly()
var receiver = await _repo.Query
.Where(r => r.EmailAddress == request.EmailAddress)
.SingleOrDefaultAsync(cancel);

View File

@@ -0,0 +1,15 @@
namespace EnvelopeGenerator.Domain.Constants
{
public static class AuthPolicy
{
public const string SenderOrReceiver = nameof(SenderOrReceiver) + nameof(AuthPolicy);
public const string ReceiverOrReceiverTFA = nameof(ReceiverOrReceiverTFA) + nameof(AuthPolicy);
public const string Sender = nameof(Sender) + nameof(AuthPolicy);
public const string Receiver = nameof(Receiver) + nameof(AuthPolicy);
public const string ReceiverTFA = nameof(ReceiverTFA) + nameof(AuthPolicy);
}
}

View File

@@ -1,8 +0,0 @@
namespace EnvelopeGenerator.Domain.Constants
{
public static class ReceiverRole
{
public const string PreAuth = "PreAuth";
public const string FullyAuth = "FullyAuth";
}
}

View File

@@ -0,0 +1,23 @@
#if NETFRAMEWORK
using System;
#endif
namespace EnvelopeGenerator.Domain.Constants
{
public static class Role
{
[Obsolete("Use Receiver.TFA")]
public const string ReceiverTFA = Receiver.TFA;
[Obsolete("Use Receiver.Full")]
public const string ReceiverFull = Receiver.Full;
public static class Receiver
{
public const string TFA = "EGReceiverTFA";
public const string Full = "EGReceiver";
}
public const string Sender = "EGSender";
}
}

View File

@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.3",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@@ -1,144 +0,0 @@
using DigitalData.Core.Abstraction.Application;
using DigitalData.UserManager.Application.Contracts;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using EnvelopeGenerator.GeneratorAPI.Models;
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;
[Obsolete("Use MediatR")]
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>
[Obsolete("Use MediatR")]
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 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()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
/// <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

@@ -1,62 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace EnvelopeGenerator.GeneratorAPI.Controllers
{
/// <summary>
/// Provides extension methods for extracting user information from a <see cref="ClaimsPrincipal"/>.
/// </summary>
public static class ControllerExtensions
{
/// <summary>
/// Attempts to retrieve the user's ID from the claims. Returns null if the ID is not found or invalid.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The user's ID as an integer, or null if not found or invalid.</returns>
public static int? GetIdOrDefault(this ClaimsPrincipal user)
=> int.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.FindFirstValue("sub"), out int result)
? result : null;
/// <summary>
/// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The user's ID as an integer.</returns>
/// <exception cref="InvalidOperationException">Thrown if the user ID claim is missing or invalid.</exception>
public static int GetId(this ClaimsPrincipal user)
=> user.GetIdOrDefault()
?? throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token.");
/// <summary>
/// Retrieves the username from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The username as a string, or null if not found.</returns>
public static string? GetUsernameOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Name)?.Value;
/// <summary>
/// Retrieves the user's surname (last name) from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The surname as a string, or null if not found.</returns>
public static string? GetNameOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Surname)?.Value;
/// <summary>
/// Retrieves the user's given name (first name) from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The given name as a string, or null if not found.</returns>
public static string? GetPrenameOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.GivenName)?.Value;
/// <summary>
/// Retrieves the user's email address from the claims, if available.
/// </summary>
/// <param name="user">The <see cref="ClaimsPrincipal"/> representing the user.</param>
/// <returns>The email address as a string, or null if not found.</returns>
public static string? GetEmailOrDefault(this ClaimsPrincipal user)
=> user.FindFirst(ClaimTypes.Email)?.Value;
}
}

View File

@@ -1,43 +0,0 @@
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Extensions;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
/// <summary>
/// Provides access to envelope documents for authenticated receivers.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="DocumentController"/> class.
/// </remarks>
[Authorize(Roles = ReceiverRole.FullyAuth)]
[ApiController]
[Route("api/[controller]")]
public class DocumentController(IMediator mediator, ILogger<DocumentController> logger) : ControllerBase
{
/// <summary>
/// Returns the document bytes for the specified envelope receiver key.
/// </summary>
/// <param name="query">Encoded envelope key.</param>
/// <param name="cancel">Cancellation token.</param>
[HttpGet]
public async Task<IActionResult> GetDocument(ReadEnvelopeReceiverQuery query, CancellationToken cancel)
{
var envRcv = await mediator.Send(query, cancel).FirstAsync(Exceptions.NotFound);
var byteData = envRcv.Envelope?.Documents?.FirstOrDefault()?.ByteData;
if (byteData is null || byteData.Length == 0)
{
logger.LogError("Document byte data is null or empty for envelope-receiver entity:\n{envelopeKey}.",
envRcv.ToJson(Format.Json.ForDiagnostics));
throw new NotFoundException("Document is empty.");
}
return File(byteData, "application/octet-stream");
}
}

View File

@@ -1,18 +0,0 @@
namespace EnvelopeGenerator.GeneratorAPI
{
/// <summary>
/// Provides custom claim types for envelope-related information.
/// </summary>
public static class EnvelopeClaimTypes
{
/// <summary>
/// Claim type for the title of an envelope.
/// </summary>
public static readonly string Title = $"Envelope{nameof(Title)}";
/// <summary>
/// Claim type for the ID of an envelope.
/// </summary>
public static readonly string Id = $"Envelope{nameof(Id)}";
}
}

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>4fdae4ba-f512-444a-9e18-111047d3ef02</ProjectGuid>
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Gen\Api\net7\win64\$(Version)\$(Version).zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>SignFlowGen</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>4fdae4ba-f512-444a-9e18-111047d3ef02</ProjectGuid>
<DesktopBuildPackageLocation>P:\Install .Net\0 DD - Smart UP\signFLOW\Gen\Api\net9\win64\$(Version)\$(Version).zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>SignFlowGen</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="3em" height="2.5em" viewBox="0 0 24 24" style="stroke: #a9a8ad;">
<path opacity="0.5" d="M15.9998 2L14.9998 2C12.1714 2 10.7576 2.00023 9.87891 2.87891C9.00023 3.75759 9.00023 5.1718 9.00023 8.00023L9.00023 16.0002C9.00023 18.8287 9.00023 20.2429 9.87891 21.1215C10.7574 22 12.1706 22 14.9976 22L14.9998 22L15.9998 22C18.8282 22 20.2424 22 21.1211 21.1213C21.9998 20.2426 21.9998 18.8284 21.9998 16L21.9998 8L21.9998 7.99998C21.9998 5.17157 21.9998 3.75736 21.1211 2.87868C20.2424 2 18.8282 2 15.9998 2Z" fill="#1C274C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.25098 11.999C1.25098 11.5848 1.58676 11.249 2.00098 11.249L13.9735 11.249L12.0129 9.56845C11.6984 9.29889 11.662 8.82541 11.9315 8.51092C12.2011 8.19642 12.6746 8.16 12.9891 8.42957L16.4891 11.4296C16.6553 11.5721 16.751 11.7801 16.751 11.999C16.751 12.218 16.6553 12.426 16.4891 12.5685L12.9891 15.5685C12.6746 15.838 12.2011 15.8016 11.9315 15.4871C11.662 15.1726 11.6984 14.6991 12.0129 14.4296L13.9735 12.749L2.00098 12.749C1.58676 12.749 1.25098 12.4132 1.25098 11.999Z" fill="#1C274C"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Web.Controllers;
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Roles = Role.ReceiverFull)]
[ApiController]
[Route("api/[controller]")]
public class AnnotationController : ControllerBase
@@ -42,7 +42,7 @@ public class AnnotationController : ControllerBase
_logger = logger;
}
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Roles = Role.ReceiverFull)]
[HttpPost]
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
{
@@ -80,7 +80,7 @@ public class AnnotationController : ControllerBase
return Ok();
}
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Roles = Role.ReceiverFull)]
[HttpPost("reject")]
[Obsolete("Use DigitalData.Core.Exceptions and .Middleware")]
public async Task<IActionResult> Reject([FromBody] string? reason = null)

View File

@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Web.Controllers;
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Roles = Role.ReceiverFull)]
[ApiController]
[Route("api/[controller]")]
public class DocumentController : ControllerBase

View File

@@ -107,7 +107,7 @@ public class EnvelopeController : ViewControllerBase
return this.ViewEnvelopeNotFound();
}
var er_secret = er_secret_res.Data;
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
await HttpContext.SignInEnvelopeAsync(er_secret, Role.ReceiverFull);
return await CreateShowEnvelopeView(er_secret);
}
#endregion UseAccessCode
@@ -172,7 +172,7 @@ public class EnvelopeController : ViewControllerBase
}
// show envelope if already logged in
if (User.IsInRole(ReceiverRole.FullyAuth))
if (User.IsInRole(Role.ReceiverFull))
return await CreateShowEnvelopeView(er_secret);
if (auth.HasMulti)
@@ -206,7 +206,7 @@ public class EnvelopeController : ViewControllerBase
.WithData("ErrorMessage", _localizer.WrongEnvelopeReceiverId());
}
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.FullyAuth);
await HttpContext.SignInEnvelopeAsync(er_secret, Role.ReceiverFull);
return await CreateShowEnvelopeView(er_secret);
}
@@ -225,9 +225,9 @@ public class EnvelopeController : ViewControllerBase
&& uuidClaim == er.Envelope?.Uuid
&& signatureClaim is not null
&& signatureClaim == er.Receiver?.Signature
&& User.IsInRole(ReceiverRole.FullyAuth))
&& User.IsInRole(Role.ReceiverFull))
{
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
await HttpContext.SignInEnvelopeAsync(er, Role.ReceiverFull);
//add PSPDFKit licence key
ViewData["PSPDFKitLicenseKey"] = _configuration["PSPDFKitLicenseKey"];
@@ -262,7 +262,7 @@ public class EnvelopeController : ViewControllerBase
return this.ViewDocumentNotFound();
}
await HttpContext.SignInEnvelopeAsync(er, ReceiverRole.FullyAuth);
await HttpContext.SignInEnvelopeAsync(er, Role.ReceiverFull);
ViewData["ReadAndConfirm"] = er.Envelope.ReadOnly;
@@ -334,7 +334,7 @@ public class EnvelopeController : ViewControllerBase
await _rcvService.UpdateAsync(rcv);
}
await HttpContext.SignInEnvelopeAsync(er_secret, ReceiverRole.PreAuth);
await HttpContext.SignInEnvelopeAsync(er_secret, Role.ReceiverTFA);
return await TFAViewAsync(auth.UserSelectSMS, er_secret, envelopeReceiverId);
}
@@ -348,7 +348,7 @@ public class EnvelopeController : ViewControllerBase
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (!User.IsInRole(ReceiverRole.PreAuth) || !_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey))
if (!User.IsInRole(Role.ReceiverTFA) || !_envSmsHandler.VerifyTotp(auth.SmsCode!, er_secret.Receiver.TotpSecretkey))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer.WrongAccessCode();
@@ -364,7 +364,7 @@ public class EnvelopeController : ViewControllerBase
if (er_secret.Receiver!.TotpSecretkey is null)
throw new InvalidOperationException($"TotpSecretkey of DTO cannot validate without TotpSecretkey. Dto: {JsonConvert.SerializeObject(er_secret)}");
if (!User.IsInRole(ReceiverRole.PreAuth) || !_authenticator.VerifyTotp(auth.AuthenticatorCode!, er_secret.Receiver.TotpSecretkey, window: VerificationWindow.RfcSpecifiedNetworkDelay))
if (!User.IsInRole(Role.ReceiverTFA) || !_authenticator.VerifyTotp(auth.AuthenticatorCode!, er_secret.Receiver.TotpSecretkey, window: VerificationWindow.RfcSpecifiedNetworkDelay))
{
Response.StatusCode = StatusCodes.Status401Unauthorized;
ViewData["ErrorMessage"] = _localizer.WrongAccessCode();

View File

@@ -34,7 +34,7 @@ namespace EnvelopeGenerator.Web.Controllers
}
[HttpPost]
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Roles = Role.ReceiverFull)]
[Obsolete("Use MediatR")]
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
{

View File

@@ -91,7 +91,7 @@ public class TFARegController : ViewControllerBase
}
}
[Authorize(Roles = ReceiverRole.FullyAuth)]
[Authorize(Roles = Role.ReceiverFull)]
[HttpPost("auth/logout")]
public async Task<IActionResult> LogOut()
{

View File

@@ -17,8 +17,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Infrastru
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Application", "EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj", "{5A9984F8-51A2-4558-A415-EC5FEED7CF7D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.GeneratorAPI", "EnvelopeGenerator.GeneratorAPI\EnvelopeGenerator.GeneratorAPI.csproj", "{E5E12BA4-60C1-48BA-9053-0F8B62B38124}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{9943209E-1744-4944-B1BA-4F87FC1A0EEB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}"
@@ -29,8 +27,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastru
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "presentation", "presentation", "{E3C758DC-914D-4B7E-8457-0813F1FDB0CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Terminal", "EnvelopeGenerator.Terminal\EnvelopeGenerator.Terminal.csproj", "{A9F9B431-BB9B-49B8-9E2C-0703634A653A}"
EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Form", "EnvelopeGenerator.Form\EnvelopeGenerator.Form.vbproj", "{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor", "EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj", "{211619F5-AE25-4BA5-A552-BACAFE0632D3}"
@@ -41,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Jobs", "E
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.WorkerService", "EnvelopeGenerator.WorkerService\EnvelopeGenerator.WorkerService.csproj", "{E3676510-7030-4E85-86E1-51E483E2A3B6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -75,14 +73,6 @@ Global
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D}.Release|Any CPU.Build.0 = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{E5E12BA4-60C1-48BA-9053-0F8B62B38124}.Release|Any CPU.Build.0 = Debug|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9F9B431-BB9B-49B8-9E2C-0703634A653A}.Release|Any CPU.Build.0 = Release|Any CPU
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -103,6 +93,10 @@ Global
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3676510-7030-4E85-86E1-51E483E2A3B6}.Release|Any CPU.Build.0 = Release|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC768913-6270-14F4-1DD3-69C87A659462}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -115,16 +109,15 @@ Global
{4F32A98D-E6F0-4A09-BD97-1CF26107E837} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{63E32615-0ECA-42DC-96E3-91037324B7C7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{5A9984F8-51A2-4558-A415-EC5FEED7CF7D} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{E5E12BA4-60C1-48BA-9053-0F8B62B38124} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{9943209E-1744-4944-B1BA-4F87FC1A0EEB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{E3C758DC-914D-4B7E-8457-0813F1FDB0CB} = {134D4164-B291-4E19-99B9-E4FA3AFAB62C}
{A9F9B431-BB9B-49B8-9E2C-0703634A653A} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
{3D0514EA-2681-4B13-AD71-35CC6363DBD7} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{E3676510-7030-4E85-86E1-51E483E2A3B6} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7}

Some files were not shown because too many files have changed in this diff Show More