Compare commits
3 Commits
380a5f4c0c
...
feat/remov
| Author | SHA1 | Date | |
|---|---|---|---|
| b46c3c8ebe | |||
| 9f712a6b8c | |||
| 4449743105 |
17
EnvelopeGenerator.API/AuthScheme.cs
Normal file
17
EnvelopeGenerator.API/AuthScheme.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace EnvelopeGenerator.API;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static class AuthScheme
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Scheme name used for per-envelope receiver JWT authentication.
|
||||||
|
/// </summary>
|
||||||
|
public const string Receiver = "EnvelopeGenerator.API.ReceiverJWT";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scheme name used for per-envelope sender JWT authentication.
|
||||||
|
/// </summary>
|
||||||
|
public const string Sender = "EnvelopeGenerator.API.SenderJWT";
|
||||||
|
}
|
||||||
132
EnvelopeGenerator.API/Controllers/AnnotationController.cs
Normal file
132
EnvelopeGenerator.API/Controllers/AnnotationController.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.DTO;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
|
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Queries;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages annotations and signature lifecycle for envelopes.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AnnotationController : ControllerBase
|
||||||
|
{
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
private readonly IEnvelopeHistoryService _historyService;
|
||||||
|
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||||
|
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
private readonly ILogger<AnnotationController> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="AnnotationController"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
public AnnotationController(
|
||||||
|
ILogger<AnnotationController> logger,
|
||||||
|
IEnvelopeHistoryService envelopeHistoryService,
|
||||||
|
IEnvelopeReceiverService envelopeReceiverService,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_historyService = envelopeHistoryService;
|
||||||
|
_envelopeReceiverService = envelopeReceiverService;
|
||||||
|
_mediator = mediator;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates or updates annotations for the authenticated envelope receiver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="psPdfKitAnnotation">Annotation payload.</param>
|
||||||
|
/// <param name="cancel">Cancellation token.</param>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost]
|
||||||
|
[Obsolete("PSPDF Kit will no longer be used.")]
|
||||||
|
public async Task<IActionResult> CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var signature = User.ReceiverSignature();
|
||||||
|
var uuid = User.EnvelopeUuid();
|
||||||
|
|
||||||
|
var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound);
|
||||||
|
|
||||||
|
if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null)
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
if (await _mediator.IsSignedAsync(uuid, signature, cancel))
|
||||||
|
return Problem(statusCode: StatusCodes.Status409Conflict);
|
||||||
|
else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
||||||
|
return Problem(statusCode: StatusCodes.Status423Locked);
|
||||||
|
|
||||||
|
var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel);
|
||||||
|
var docSignedNotification = envelopeReceiverDto is not null
|
||||||
|
? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation }
|
||||||
|
: throw new NotFoundException("Envelope receiver is not found.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _mediator.Publish(docSignedNotification, cancel);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
await _mediator.Publish(new RemoveSignatureNotification()
|
||||||
|
{
|
||||||
|
EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId,
|
||||||
|
ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId
|
||||||
|
}, cancel);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rejects the document for the current receiver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">Optional rejection reason.</param>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("reject")]
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
public async Task<IActionResult> Reject([FromBody] string? reason = null)
|
||||||
|
{
|
||||||
|
var signature = User.ReceiverSignature();
|
||||||
|
var uuid = User.EnvelopeUuid();
|
||||||
|
var mail = User.ReceiverMail();
|
||||||
|
|
||||||
|
var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||||
|
|
||||||
|
if (envRcvRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(envRcvRes.Notices);
|
||||||
|
return Unauthorized("you are not authorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason);
|
||||||
|
if (histRes.IsSuccess)
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject");
|
||||||
|
_logger.LogNotice(histRes.Notices);
|
||||||
|
return StatusCode(500, histRes.Messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
EnvelopeGenerator.API/Controllers/AuthController.cs
Normal file
117
EnvelopeGenerator.API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
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(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[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(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
public IActionResult Check(string? role = null)
|
||||||
|
=> role is not null && !User.IsInRole(role)
|
||||||
|
? Unauthorized()
|
||||||
|
: Ok();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key.
|
||||||
|
/// The request must carry a cookie named <c>AuthTokenSignFLOWReceiver.{envelopeKey}</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey">The unique envelope key extracted from the route.</param>
|
||||||
|
/// <response code="200">Valid per-envelope token found.</response>
|
||||||
|
/// <response code="401">Token is missing, expired or invalid.</response>
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("check/envelope/{envelopeKey}")]
|
||||||
|
public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the per-envelope receiver cookie for the given envelope key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey">The unique envelope key whose cookie should be deleted.</param>
|
||||||
|
/// <response code="200">Cookie successfully deleted.</response>
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
|
[HttpPost("logout/envelope/{envelopeKey}")]
|
||||||
|
public IActionResult LogoutEnvelopeReceiver([FromRoute] string envelopeKey)
|
||||||
|
{
|
||||||
|
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||||
|
Response.Cookies.Delete(cookieName);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all per-envelope receiver cookies from the current request.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">All envelope receiver cookies successfully deleted.</response>
|
||||||
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
|
[HttpPost("logout/envelope")]
|
||||||
|
public IActionResult LogoutAllEnvelopeReceivers()
|
||||||
|
{
|
||||||
|
foreach (var cookieName in Request.Cookies.Keys.Where(k => CookieNames.IsEnvelopeReceiverCookie(k, authTokenKeys.Cookie)))
|
||||||
|
Response.Cookies.Delete(cookieName);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal file
84
EnvelopeGenerator.API/Controllers/CacheController.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.API.Options;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages cached data for receivers using distributed cache.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
public class CacheController(
|
||||||
|
IDistributedCache cache,
|
||||||
|
IOptions<CacheOptions> cacheOptions) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a receiver's signature in cache for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> SaveSignature(
|
||||||
|
[FromRoute] string envelopeKey,
|
||||||
|
[FromBody] SignatureCacheRequest request,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
var json = JsonSerializer.Serialize(request);
|
||||||
|
|
||||||
|
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
|
||||||
|
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
|
||||||
|
: null;
|
||||||
|
|
||||||
|
await cache.SetStringAsync(cacheKey, json, options ?? new DistributedCacheEntryOptions(), cancel);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a cached signature for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> GetSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
var json = await cache.GetStringAsync(cacheKey, cancel);
|
||||||
|
|
||||||
|
if (json is null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var signature = JsonSerializer.Deserialize<SignatureCacheRequest>(json);
|
||||||
|
return Ok(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a cached signature for the specified envelope.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpDelete("SignatureCapture/{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
|
||||||
|
await cache.RemoveAsync(cacheKey, cancel);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for caching signature data.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SignatureCacheRequest(
|
||||||
|
string DataUrl,
|
||||||
|
string FullName,
|
||||||
|
string Place,
|
||||||
|
string? Position = null);
|
||||||
31
EnvelopeGenerator.API/Controllers/ConfigController.cs
Normal file
31
EnvelopeGenerator.API/Controllers/ConfigController.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exposes configuration data required by the client applications.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of <see cref="ConfigController"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||||
|
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.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>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class DocReceiverElementController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DocReceiverElementController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: update to use signature query
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> Get(string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
int envelopeId = User.EnvelopeId();
|
||||||
|
|
||||||
|
int receiverId = User.ReceiverId();
|
||||||
|
|
||||||
|
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||||
|
|
||||||
|
if (doc.Elements is not IEnumerable<DocReceiverElementDto> docSignatures)
|
||||||
|
return NotFound("Document is empty.");
|
||||||
|
|
||||||
|
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
|
||||||
|
|
||||||
|
if (rcvSignatures is null)
|
||||||
|
return NotFound("No signatures found for the current receiver.");
|
||||||
|
else
|
||||||
|
return Ok(rcvSignatures);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
EnvelopeGenerator.API/Controllers/DocumentController.cs
Normal file
84
EnvelopeGenerator.API/Controllers/DocumentController.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
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>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger<DocumentController> logger) : 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.EnvelopeId();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the document for the specified envelope key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
int envelopeId = User.EnvelopeId();
|
||||||
|
|
||||||
|
var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||||
|
|
||||||
|
if (senderDoc.ByteData is not byte[] senderDocByte)
|
||||||
|
return NotFound("Document is empty.");
|
||||||
|
|
||||||
|
Response.Headers.ContentDisposition = $"inline; filename=\"{envelopeKey}.pdf\"";
|
||||||
|
return File(senderDocByte, "application/pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
69
EnvelopeGenerator.API/Controllers/EmailTemplateController.cs
Normal file
69
EnvelopeGenerator.API/Controllers/EmailTemplateController.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.EmailTemplates.Commands;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Application.EmailTemplates.Queries;
|
||||||
|
|
||||||
|
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="mediator">
|
||||||
|
/// Die Mediator-Instanz, die zum Senden von Befehlen und Abfragen verwendet wird.
|
||||||
|
/// </param>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize(Policy = AuthPolicy.Sender)]
|
||||||
|
public class EmailTemplateController(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft E-Mail-Vorlagen basierend auf der angegebenen Abfrage ab.
|
||||||
|
/// Gibt alles zurück, wenn keine Id- oder Typ-Informationen eingegeben wurden.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="emailTemplate">Die Abfrageparameter zum Abrufen von E-Mail-Vorlagen.</param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns>Gibt HTTP-Antwort zurück</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sample request:
|
||||||
|
/// GET /api/EmailTemplate?emailTemplateId=123
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="200">Wenn die E-Mail-Vorlagen erfolgreich abgerufen werden.</response>
|
||||||
|
/// <response code="400">Wenn die Abfrageparameter ungültig sind.</response>
|
||||||
|
/// <response code="401">Wenn der Benutzer nicht authentifiziert ist.</response>
|
||||||
|
/// <response code="404">Wenn die gesuchte Abfrage nicht gefunden wird.</response>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] ReadEmailTemplateQuery emailTemplate, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(emailTemplate, cancel);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an temp template or resets it if no update command is provided.
|
||||||
|
/// Aktualisiert eine E-Mail-Vorlage oder setzt sie zurück, wenn kein Aktualisierungsbefehl angegeben ist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <response code="200">Wenn die E-Mail-Vorlage erfolgreich aktualisiert oder zurückgesetzt wird.</response>
|
||||||
|
/// <response code="400">Wenn die Abfrage ohne einen String gesendet wird.</response>
|
||||||
|
/// <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([FromBody] UpdateEmailTemplateCommand update, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
await mediator.Send(update, cancel);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
126
EnvelopeGenerator.API/Controllers/EnvelopeController.cs
Normal file
126
EnvelopeGenerator.API/Controllers/EnvelopeController.cs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
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.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Die API ermöglicht das Abrufen und Verwalten von Umschlägen basierend auf Benutzerinformationen und Statusfiltern.
|
||||||
|
///
|
||||||
|
/// Mögliche Antworten:
|
||||||
|
/// - 200 OK: Die Anfrage war erfolgreich, und die angeforderten Daten werden zurückgegeben.
|
||||||
|
/// - 400 Bad Request: Die Anfrage war fehlerhaft oder unvollständig.
|
||||||
|
/// - 401 Unauthorized: Der Benutzer ist nicht authentifiziert.
|
||||||
|
/// - 403 Forbidden: Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.
|
||||||
|
/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden.
|
||||||
|
/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class EnvelopeController : ControllerBase
|
||||||
|
{
|
||||||
|
public record DeleteEnvelopeRequest(string Reason);
|
||||||
|
|
||||||
|
private readonly ILogger<EnvelopeController> _logger;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erstellt eine neue Instanz des EnvelopeControllers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Der Logger, der für das Protokollieren von Informationen verwendet wird.</param>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public EnvelopeController(ILogger<EnvelopeController> logger, IMediator mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelope"></param>
|
||||||
|
/// <returns>Eine IActionResult-Instanz, die die abgerufenen Umschläge oder einen Fehlerstatus enthält.</returns>
|
||||||
|
/// <response code="200">Die Anfrage war erfolgreich, und die Umschläge werden zurückgegeben.</response>
|
||||||
|
/// <response code="400">Die Anfrage war fehlerhaft oder unvollständig.</response>
|
||||||
|
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
||||||
|
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAsync([FromQuery] ReadEnvelopeQuery envelope)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(envelope.Authorize(User.GetId()));
|
||||||
|
return result.Any() ? Ok(result) : NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query"></param>
|
||||||
|
/// <param name="view">Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).</param>
|
||||||
|
/// <returns>Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.</returns>
|
||||||
|
/// <response code="200">Das Dokument wurde erfolgreich abgerufen.</response>
|
||||||
|
/// <response code="404">Das Dokument wurde nicht gefunden oder ist nicht verfügbar.</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[HttpGet("doc-result")]
|
||||||
|
public async Task<IActionResult> GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false)
|
||||||
|
{
|
||||||
|
var envelopes = await _mediator.Send(query.Authorize(User.GetId()));
|
||||||
|
var envelope = envelopes.FirstOrDefault();
|
||||||
|
|
||||||
|
if (envelope is null)
|
||||||
|
return NotFound("Envelope not available.");
|
||||||
|
if (envelope.DocResult is null)
|
||||||
|
return NotFound("The document has not been fully signed or the result has not yet been released.");
|
||||||
|
|
||||||
|
if (view)
|
||||||
|
{
|
||||||
|
Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\"");
|
||||||
|
return File(envelope.DocResult, "application/pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[NonAction]
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||||
|
{
|
||||||
|
var res = await _mediator.Send(command.WithAuth(User.GetId()));
|
||||||
|
|
||||||
|
if (res is null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command));
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = id,
|
||||||
|
Reason = request?.Reason ?? string.Empty
|
||||||
|
}.WithAuth(User.GetId()));
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
281
EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs
Normal file
281
EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||||
|
using EnvelopeGenerator.Application.Envelopes.Queries;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.API.Models;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Data;
|
||||||
|
using EnvelopeGenerator.Application.Common.SQL;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller für die Verwaltung von Umschlagempfängern.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
public class EnvelopeReceiverController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<EnvelopeReceiverController> _logger;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IEnvelopeExecutor _envelopeExecutor;
|
||||||
|
private readonly IEnvelopeReceiverExecutor _erExecutor;
|
||||||
|
private readonly IDocumentExecutor _documentExecutor;
|
||||||
|
private readonly string _cnnStr;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Konstruktor für den EnvelopeReceiverController.
|
||||||
|
/// </summary>
|
||||||
|
public EnvelopeReceiverController(ILogger<EnvelopeReceiverController> logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions<ConnectionString> csOpt)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
_mapper = mapper;
|
||||||
|
_envelopeExecutor = envelopeExecutor;
|
||||||
|
_erExecutor = erExecutor;
|
||||||
|
_documentExecutor = documentExecutor;
|
||||||
|
_cnnStr = csOpt.Value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeReceiver">Die Abfrageparameter für die Filterung von Umschlagempfängern.</param>
|
||||||
|
/// <returns>Eine HTTP-Antwort mit der Liste der gefundenen Umschlagempfänger oder einem Fehlerstatus.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Endpunkt ermöglicht es, Umschlagempfänger basierend auf dem Benutzernamen und optionalen Statusfiltern abzurufen.
|
||||||
|
/// Wenn der Benutzername nicht ermittelt werden kann, wird ein Serverfehler zurückgegeben.
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="200">Die Liste der Umschlagempfänger wurde erfolgreich abgerufen.</response>
|
||||||
|
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
|
||||||
|
{
|
||||||
|
envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
|
||||||
|
|
||||||
|
var result = await _mediator.Send(envelopeReceiver);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeKey"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpGet("{envelopeKey}")]
|
||||||
|
public async Task<IActionResult> GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var er = await _mediator.Send(new ReadEnvelopeReceiverQuery()
|
||||||
|
{
|
||||||
|
Key = envelopeKey
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
return Ok(er.SingleOrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="receiver">Abfrage, bei der nur eine der Angaben ID, Signatur oder E-Mail-Adresse des Empfängers eingegeben werden muss.</param>
|
||||||
|
/// <returns>Eine HTTP-Antwort mit dem Namen des Empfängers oder einem Fehlerstatus.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Endpunkt ermöglicht es, den Namen des zuletzt verwendeten Empfängers basierend auf der E-Mail-Adresse abzurufen.
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="200">Der Name des Empfängers wurde erfolgreich abgerufen.</response>
|
||||||
|
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||||
|
/// <response code="404">Kein Empfänger gefunden.</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("salute")]
|
||||||
|
public async Task<IActionResult> GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
|
||||||
|
{
|
||||||
|
var name = await _mediator.Send(receiver);
|
||||||
|
return name is null ? NotFound() : Ok(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns>HTTP-Antwort</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Sample request:
|
||||||
|
///
|
||||||
|
/// POST /api/envelope
|
||||||
|
/// {
|
||||||
|
/// "title": "Vertragsdokument",
|
||||||
|
/// "message": "Bitte unterschreiben Sie dieses Dokument.",
|
||||||
|
/// "document": {
|
||||||
|
/// "dataAsBase64": "dGVzdC1iYXNlNjQtZGF0YQ=="
|
||||||
|
/// },
|
||||||
|
/// "receivers": [
|
||||||
|
/// {
|
||||||
|
/// "emailAddress": "example@example.com",
|
||||||
|
/// "signatures": [
|
||||||
|
/// {
|
||||||
|
/// "x": 100,
|
||||||
|
/// "y": 200,
|
||||||
|
/// "page": 1
|
||||||
|
/// }
|
||||||
|
/// ],
|
||||||
|
/// "name": "Max Mustermann",
|
||||||
|
/// "phoneNumber": "+49123456789"
|
||||||
|
/// }
|
||||||
|
/// ],
|
||||||
|
/// "tfaEnabled": false
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="202">Envelope-Erstellung und Sendeprozessbefehl erfolgreich</response>
|
||||||
|
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
|
||||||
|
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
|
||||||
|
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
#region Create Envelope
|
||||||
|
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Add receivers
|
||||||
|
List<EnvelopeReceiver> sentReceivers = new();
|
||||||
|
List<ReceiverGetOrCreateCommand> unsentReceivers = new();
|
||||||
|
|
||||||
|
foreach (var receiver in request.Receivers)
|
||||||
|
{
|
||||||
|
var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
|
||||||
|
|
||||||
|
if (envelopeReceiver is null)
|
||||||
|
unsentReceivers.Add(receiver);
|
||||||
|
else
|
||||||
|
sentReceivers.Add(envelopeReceiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = _mapper.Map<CreateEnvelopeReceiverResponse>(envelope);
|
||||||
|
res.UnsentReceivers = unsentReceivers;
|
||||||
|
res.SentReceiver = _mapper.Map<List<ReceiverDto>>(sentReceivers.Select(er => er.Receiver));
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Add document
|
||||||
|
var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
|
||||||
|
|
||||||
|
if (document is null)
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Add document element
|
||||||
|
// @DOC_ID, @RECEIVER_ID, @POSITION_X, @POSITION_Y, @PAGE
|
||||||
|
string sql = @"
|
||||||
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
|
EXEC [dbo].[PRSIG_API_ADD_DOC_RECEIVER_ELEM]
|
||||||
|
{0},
|
||||||
|
{1},
|
||||||
|
{2},
|
||||||
|
{3},
|
||||||
|
{4},
|
||||||
|
@OUT_SUCCESS OUTPUT;
|
||||||
|
|
||||||
|
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||||
|
|
||||||
|
foreach (var rcv in res.SentReceiver)
|
||||||
|
foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty<Application.EnvelopeReceivers.Commands.DocReceiverElementCreateDto>())
|
||||||
|
{
|
||||||
|
using SqlConnection conn = new(_cnnStr);
|
||||||
|
conn.Open();
|
||||||
|
|
||||||
|
var formattedSQL = string.Format(sql, document.Id.ToSqlParam(), rcv.Id.ToSqlParam(), sign.X.ToSqlParam(), sign.Y.ToSqlParam(), sign.Page.ToSqlParam());
|
||||||
|
|
||||||
|
using SqlCommand cmd = new(formattedSQL, conn);
|
||||||
|
cmd.CommandType = CommandType.Text;
|
||||||
|
|
||||||
|
using SqlDataReader reader = cmd.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
|
document.Id, rcv.Id, sign.Page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Create history
|
||||||
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
|
string sql_hist = @"
|
||||||
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
|
{0},
|
||||||
|
{1},
|
||||||
|
{2},
|
||||||
|
@OUT_SUCCESS OUTPUT;
|
||||||
|
|
||||||
|
SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
|
||||||
|
|
||||||
|
using (SqlConnection conn = new(_cnnStr))
|
||||||
|
{
|
||||||
|
conn.Open();
|
||||||
|
var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), User.GetId().ToSqlParam());
|
||||||
|
using SqlCommand cmd = new(formattedSQL_hist, conn);
|
||||||
|
cmd.CommandType = CommandType.Text;
|
||||||
|
|
||||||
|
using SqlDataReader reader = cmd.ExecuteReader();
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
|
envelope.Uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool IsBase64String(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Convert.FromBase64String(input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
EnvelopeGenerator.API/Controllers/EnvelopeTypeController.cs
Normal file
39
EnvelopeGenerator.API/Controllers/EnvelopeTypeController.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class EnvelopeTypeController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<EnvelopeTypeController> _logger;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public EnvelopeTypeController(ILogger<EnvelopeTypeController> logger, IMediator mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetAllAsync()
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
EnvelopeGenerator.API/Controllers/HistoryController.cs
Normal file
118
EnvelopeGenerator.API/Controllers/HistoryController.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Queries;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class HistoryController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Konstruktor für den HistoryController.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="memoryCache"></param>
|
||||||
|
/// <param name="mediator"></param>
|
||||||
|
public HistoryController(IMemoryCache memoryCache, IMediator mediator)
|
||||||
|
{
|
||||||
|
_memoryCache = memoryCache;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt alle möglichen Verweise auf alle möglichen Include in einem Verlaufsdatensatz zurück. (z. B. DocumentSigned bezieht sich auf Receiver.)
|
||||||
|
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
||||||
|
/// 1 - Sender:
|
||||||
|
/// Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 1* beginnen.
|
||||||
|
/// 2 - Receiver:
|
||||||
|
/// Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 2* beginnen.
|
||||||
|
/// 3 - System:
|
||||||
|
/// Historische Datensätze, die sich auf den allgemeinen Zustand des Umschlags beziehen. Diese haben Statuscodes, die mit 3* beginnen.
|
||||||
|
/// 4 - Unknown:
|
||||||
|
/// Ein unbekannter Datensatz weist auf einen möglichen Mangel oder eine Unstimmigkeit im Aktualisierungsprozess der Anwendung hin.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
[HttpGet("related")]
|
||||||
|
[Authorize]
|
||||||
|
public IActionResult GetReferenceTypes(ReferenceType? referenceType = null)
|
||||||
|
{
|
||||||
|
return referenceType is null
|
||||||
|
? Ok(_memoryCache.GetEnumAsDictionary<ReferenceType>("gen.api", ReferenceType.Unknown))
|
||||||
|
: Ok(referenceType.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt alle möglichen Include in einem Verlaufsdatensatz zurück.
|
||||||
|
/// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können.
|
||||||
|
/// 1003: EnvelopeQueued
|
||||||
|
/// 1006: EnvelopeCompletelySigned
|
||||||
|
/// 1007: EnvelopeReportCreated
|
||||||
|
/// 1008: EnvelopeArchived
|
||||||
|
/// 1009: EnvelopeDeleted
|
||||||
|
/// 10007: EnvelopeRejected
|
||||||
|
/// 10009: EnvelopeWithdrawn
|
||||||
|
/// 2001: AccessCodeRequested
|
||||||
|
/// 2002: AccessCodeCorrect
|
||||||
|
/// 2003: AccessCodeIncorrect
|
||||||
|
/// 2004: DocumentOpened
|
||||||
|
/// 2005: DocumentSigned
|
||||||
|
/// 2006: DocumentForwarded
|
||||||
|
/// 2007: DocumentRejected
|
||||||
|
/// 2008: EnvelopeShared
|
||||||
|
/// 2009: EnvelopeViewed
|
||||||
|
/// 3001: MessageInvitationSent (Wird von Trigger verwendet)
|
||||||
|
/// 3002: MessageAccessCodeSent
|
||||||
|
/// 3003: MessageConfirmationSent
|
||||||
|
/// 3004: MessageDeletionSent
|
||||||
|
/// 3005: MessageCompletionSent
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="status">
|
||||||
|
/// Abfrageparameter, der angibt, auf welche Referenz sich der Include bezieht.
|
||||||
|
/// 1 - Sender: Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 1* beginnen.
|
||||||
|
/// 2 - Receiver: Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 2* beginnen.
|
||||||
|
/// 3 - System: Diese werden durch Datenbank-Trigger aktualisiert und sind in den Tabellen EnvelopeHistory und EmailOut zu finden.Sie arbeiten
|
||||||
|
/// integriert mit der Anwendung EmailProfiler, um E-Mails zu versenden und haben die Codes, die mit 3* beginnen.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>Gibt die HTTP-Antwort zurück.</returns>
|
||||||
|
/// <response code="200"></response>
|
||||||
|
[HttpGet("status")]
|
||||||
|
[Authorize]
|
||||||
|
public IActionResult GetEnvelopeStatus([FromQuery] EnvelopeStatus? status = null)
|
||||||
|
{
|
||||||
|
return status is null
|
||||||
|
? Ok(_memoryCache.GetEnumAsDictionary<EnvelopeStatus>("gen.api", Status.NonHist, Status.RelatedToFormApp))
|
||||||
|
: Ok(status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft die gesamte Umschlaghistorie basierend auf den angegebenen Abfrageparametern ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="historyQuery">Die Abfrageparameter, die die Filterkriterien für die Umschlaghistorie definieren.</param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns>Eine Liste von Historieneinträgen, die den angegebenen Kriterien entsprechen, oder nur der letzte Eintrag.</returns>
|
||||||
|
/// <response code="200">Die Anfrage war erfolgreich, und die Umschlaghistorie wird zurückgegeben.</response>
|
||||||
|
/// <response code="400">Die Anfrage war ungültig oder unvollständig.</response>
|
||||||
|
/// <response code="401">Der Benutzer ist nicht authentifiziert.</response>
|
||||||
|
/// <response code="403">Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.</response>
|
||||||
|
/// <response code="500">Ein unerwarteter Fehler ist aufgetreten.</response>
|
||||||
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> GetAllAsync([FromQuery] ReadHistoryQuery historyQuery, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var history = await _mediator.Send(historyQuery, cancel).ThrowIfEmpty(Exceptions.NotFound);
|
||||||
|
return Ok((historyQuery.OnlyLast) ? history.MaxBy(h => h.AddedWhen) : history);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
EnvelopeGenerator.API/Controllers/LocalizationController.cs
Normal file
121
EnvelopeGenerator.API/Controllers/LocalizationController.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using DigitalData.Core.API;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
|
||||||
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class LocalizationController : ControllerBase
|
||||||
|
{
|
||||||
|
private static readonly Guid L_KEY = Guid.NewGuid();
|
||||||
|
|
||||||
|
private readonly ILogger<LocalizationController> _logger;
|
||||||
|
private readonly IStringLocalizer<Resource> _mLocalizer;
|
||||||
|
private readonly IStringLocalizer<Resource> _localizer;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Konstruktor für den <see cref="LocalizationController"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger für die Protokollierung.</param>
|
||||||
|
/// <param name="localizer">Lokalisierungsdienst für Ressourcen.</param>
|
||||||
|
/// <param name="memoryCache">Speicher-Cache für die Zwischenspeicherung von Daten.</param>
|
||||||
|
/// <param name="_modelLocalizer">Lokalisierungsdienst für Modelle.</param>
|
||||||
|
public LocalizationController(
|
||||||
|
ILogger<LocalizationController> logger,
|
||||||
|
IStringLocalizer<Resource> localizer,
|
||||||
|
IMemoryCache memoryCache,
|
||||||
|
IStringLocalizer<Resource> _modelLocalizer)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_localizer = localizer;
|
||||||
|
_cache = memoryCache;
|
||||||
|
_mLocalizer = _modelLocalizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft alle lokalisierten Daten ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Eine Liste aller lokalisierten Daten.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult GetAll() => Ok(_cache.GetOrCreate(Language ?? string.Empty + L_KEY, _ => _mLocalizer.ToDictionary()));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft die aktuelle Sprache ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Die aktuelle Sprache oder ein NotFound-Ergebnis, wenn keine Sprache gesetzt ist.</returns>
|
||||||
|
[HttpGet("lang")]
|
||||||
|
public IActionResult GetLanguage() => Language is null ? NotFound() : Ok(Language);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Setzt die Sprache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">Die zu setzende Sprache.</param>
|
||||||
|
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gesetzt wurde, oder ein BadRequest-Ergebnis, wenn die Eingabe ungültig ist.</returns>
|
||||||
|
[HttpPost("lang")]
|
||||||
|
public IActionResult SetLanguage([FromQuery] string language)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(language))
|
||||||
|
return BadRequest();
|
||||||
|
|
||||||
|
Language = language;
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Löscht die aktuelle Sprache.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Ein Ok-Ergebnis, wenn die Sprache erfolgreich gelöscht wurde.</returns>
|
||||||
|
[HttpDelete("lang")]
|
||||||
|
public IActionResult DeleteLanguage()
|
||||||
|
{
|
||||||
|
Language = null;
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eigenschaft für die Verwaltung der aktuellen Sprache über Cookies.
|
||||||
|
/// </summary>
|
||||||
|
private string? Language
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(cookieValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0];
|
||||||
|
return culture?.Value ?? null;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var cookieOptions = new CookieOptions()
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddYears(1),
|
||||||
|
Secure = false,
|
||||||
|
SameSite = SameSiteMode.Strict,
|
||||||
|
HttpOnly = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Response.Cookies.Append(
|
||||||
|
CookieRequestCultureProvider.DefaultCookieName,
|
||||||
|
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)),
|
||||||
|
cookieOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
EnvelopeGenerator.API/Controllers/ReadOnlyController.cs
Normal file
91
EnvelopeGenerator.API/Controllers/ReadOnlyController.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.DTO;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages read-only envelope sharing flows.
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class ReadOnlyController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<ReadOnlyController> _logger;
|
||||||
|
private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
|
||||||
|
private readonly IEnvelopeMailService _mailService;
|
||||||
|
private readonly IEnvelopeHistoryService _historyService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ReadOnlyController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyController(ILogger<ReadOnlyController> logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_readOnlyService = readOnlyService;
|
||||||
|
_mailService = mailService;
|
||||||
|
_historyService = historyService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new read-only receiver for the current envelope.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="createDto">Creation payload.</param>
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[Obsolete("Use MediatR")]
|
||||||
|
public async Task<IActionResult> CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto)
|
||||||
|
{
|
||||||
|
var authReceiverMail = User.ReceiverMail();
|
||||||
|
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.EnvelopeId();
|
||||||
|
|
||||||
|
createDto.AddedWho = authReceiverMail;
|
||||||
|
createDto.EnvelopeId = envelopeId;
|
||||||
|
|
||||||
|
var creationRes = await _readOnlyService.CreateAsync(createDto: createDto);
|
||||||
|
|
||||||
|
if (creationRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(creationRes);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id);
|
||||||
|
if (readRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(creationRes);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newReadOnly = readRes.Data;
|
||||||
|
|
||||||
|
return await _mailService.SendAsync(newReadOnly).ThenAsync<int, IActionResult>(SuccessAsync: async _ =>
|
||||||
|
{
|
||||||
|
var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared);
|
||||||
|
if (histRes.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto));
|
||||||
|
_logger.LogNotice(histRes.Notices);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
},
|
||||||
|
|
||||||
|
Fail: (msg, ntc) =>
|
||||||
|
{
|
||||||
|
_logger.LogNotice(ntc);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
47
EnvelopeGenerator.API/Controllers/ReceiverController.cs
Normal file
47
EnvelopeGenerator.API/Controllers/ReceiverController.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller für die Verwaltung von Empfängern.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
|
||||||
|
/// </remarks>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
|
public class ReceiverController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mediator">Mediator für Anfragen.</param>
|
||||||
|
public ReceiverController(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ruft eine Liste von Empfängern ab, basierend auf den angegebenen Abfrageparametern.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
|
||||||
|
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver)
|
||||||
|
{
|
||||||
|
if (!receiver.HasAnyCriteria)
|
||||||
|
{
|
||||||
|
var all = await _mediator.Send(new ReadReceiverQuery());
|
||||||
|
return Ok(all);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _mediator.Send(receiver);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
EnvelopeGenerator.API/Controllers/TfaRegistrationController.cs
Normal file
129
EnvelopeGenerator.API/Controllers/TfaRegistrationController.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.DTO;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Application.Resources;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.API.Models;
|
||||||
|
using Ganss.Xss;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/tfa")]
|
||||||
|
public class TfaRegistrationController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly ILogger<TfaRegistrationController> _logger;
|
||||||
|
private readonly IEnvelopeReceiverService _envelopeReceiverService;
|
||||||
|
private readonly IAuthenticator _authenticator;
|
||||||
|
private readonly IReceiverService _receiverService;
|
||||||
|
private readonly TFARegParams _parameters;
|
||||||
|
private readonly IStringLocalizer<Resource> _localizer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TfaRegistrationController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public TfaRegistrationController(
|
||||||
|
ILogger<TfaRegistrationController> logger,
|
||||||
|
IEnvelopeReceiverService envelopeReceiverService,
|
||||||
|
IAuthenticator authenticator,
|
||||||
|
IReceiverService receiverService,
|
||||||
|
IOptions<TFARegParams> tfaRegParamsOptions,
|
||||||
|
IStringLocalizer<Resource> localizer)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_envelopeReceiverService = envelopeReceiverService;
|
||||||
|
_authenticator = authenticator;
|
||||||
|
_receiverService = receiverService;
|
||||||
|
_parameters = tfaRegParamsOptions.Value;
|
||||||
|
_localizer = localizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates registration metadata (QR code and deadline) for a receiver.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="envelopeReceiverId">Encoded envelope receiver id.</param>
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("{envelopeReceiverId}")]
|
||||||
|
public async Task<IActionResult> RegisterAsync(string envelopeReceiverId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (uuid, signature) = envelopeReceiverId.DecodeEnvelopeReceiverId();
|
||||||
|
|
||||||
|
if (uuid is null || signature is null)
|
||||||
|
{
|
||||||
|
_logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId());
|
||||||
|
return Unauthorized(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretResult = await _envelopeReceiverService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature);
|
||||||
|
if (secretResult.IsFailed)
|
||||||
|
{
|
||||||
|
_logger.LogNotice(secretResult.Notices);
|
||||||
|
return NotFound(new { message = _localizer.WrongEnvelopeReceiverId() });
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelopeReceiver = secretResult.Data;
|
||||||
|
|
||||||
|
if (!envelopeReceiver.Envelope!.TFAEnabled)
|
||||||
|
return Unauthorized(new { message = _localizer.WrongAccessCode() });
|
||||||
|
|
||||||
|
var receiver = envelopeReceiver.Receiver;
|
||||||
|
receiver!.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
|
||||||
|
await _receiverService.UpdateAsync(receiver);
|
||||||
|
var totpQr64 = _authenticator.GenerateTotpQrCode(userEmail: receiver.EmailAddress, secretKey: receiver.TotpSecretkey).ToBase64String();
|
||||||
|
|
||||||
|
if (receiver.TfaRegDeadline is null)
|
||||||
|
{
|
||||||
|
receiver.TfaRegDeadline = _parameters.Deadline;
|
||||||
|
await _receiverService.UpdateAsync(receiver);
|
||||||
|
}
|
||||||
|
else if (receiver.TfaRegDeadline <= DateTime.Now)
|
||||||
|
{
|
||||||
|
return StatusCode(StatusCodes.Status410Gone, new { message = _localizer.WrongAccessCode() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
envelopeReceiver.EnvelopeId,
|
||||||
|
envelopeReceiver.Envelope!.Uuid,
|
||||||
|
envelopeReceiver.Receiver!.Signature,
|
||||||
|
receiver.TfaRegDeadline,
|
||||||
|
TotpQR64 = totpQr64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer.WrongEnvelopeReceiverId());
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs out the envelope receiver from cookie authentication.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
|
[HttpPost("auth/logout")]
|
||||||
|
public async Task<IActionResult> LogOutAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "{message}", ex.Message);
|
||||||
|
return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
EnvelopeGenerator.API/Documentation/AuthProxyDocumentFilter.cs
Normal file
123
EnvelopeGenerator.API/Documentation/AuthProxyDocumentFilter.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
AddLoginOperation(swaggerDoc, context);
|
||||||
|
AddEnvelopeReceiverLoginOperation(swaggerDoc, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddLoginOperation(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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddEnvelopeReceiverLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||||
|
{
|
||||||
|
const string path = "/api/Auth/envelope-receiver/{key}";
|
||||||
|
|
||||||
|
var bodySchema = context.SchemaGenerator.GenerateSchema(typeof(EnvelopeReceiverLogin), context.SchemaRepository);
|
||||||
|
|
||||||
|
var operation = new OpenApiOperation
|
||||||
|
{
|
||||||
|
Summary = "Envelope receiver login (auth-hub proxy)",
|
||||||
|
Description = "Proxies the envelope receiver login to the auth service. " +
|
||||||
|
"The `cookie` query parameter is always forwarded as `true` so the auth service sets the per-envelope cookie automatically.",
|
||||||
|
Tags = [new() { Name = "Auth" }],
|
||||||
|
Parameters =
|
||||||
|
{
|
||||||
|
new OpenApiParameter
|
||||||
|
{
|
||||||
|
Name = "key",
|
||||||
|
In = ParameterLocation.Path,
|
||||||
|
Required = true,
|
||||||
|
Schema = new OpenApiSchema { Type = "string" },
|
||||||
|
Description = "The unique envelope receiver key."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RequestBody = new OpenApiRequestBody
|
||||||
|
{
|
||||||
|
Required = false,
|
||||||
|
Content =
|
||||||
|
{
|
||||||
|
["multipart/form-data"] = new OpenApiMediaType { Schema = bodySchema }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Responses =
|
||||||
|
{
|
||||||
|
["200"] = new OpenApiResponse { Description = "OK – per-envelope cookie set by auth service." },
|
||||||
|
["401"] = new OpenApiResponse { Description = "Unauthorized – invalid or missing access code." }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
swaggerDoc.Paths[path] = new OpenApiPathItem
|
||||||
|
{
|
||||||
|
Operations =
|
||||||
|
{
|
||||||
|
[OperationType.Post] = operation
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
84
EnvelopeGenerator.API/EnvelopeGenerator.API.csproj
Normal file
84
EnvelopeGenerator.API/EnvelopeGenerator.API.csproj
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<PackageId>EnvelopeGenerator.GeneratorAPI</PackageId>
|
||||||
|
<Title></Title>
|
||||||
|
<Authors>Digital Data GmbH</Authors>
|
||||||
|
<Company>Digital Data GmbH</Company>
|
||||||
|
<Product>EnvelopeGenerator.GeneratorAPI</Product>
|
||||||
|
<Version>1.4.0</Version>
|
||||||
|
<FileVersion>1.4.0</FileVersion>
|
||||||
|
<AssemblyVersion>1.4.0</AssemblyVersion>
|
||||||
|
<PackageOutputPath>Copyright © 2025 Digital Data GmbH. All rights reserved.</PackageOutputPath>
|
||||||
|
<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.Claims" Version="1.0.3" />
|
||||||
|
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||||
|
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||||
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.28" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
|
<PackageReference Include="itext" Version="8.0.5" />
|
||||||
|
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" Condition="'$(TargetFramework)' == 'net9.0'" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
|
<PackageReference Include="NLog" Version="5.2.5" />
|
||||||
|
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
||||||
|
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
<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'">
|
||||||
|
<PackageReference Include="System.DirectoryServices" Version="7.0.1" />
|
||||||
|
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="7.0.1" />
|
||||||
|
<PackageReference Include="System.DirectoryServices.Protocols" Version="7.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
|
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
||||||
|
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
|
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
|
||||||
|
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
|
||||||
|
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
96
EnvelopeGenerator.API/Extensions/ReceiverClaimExtensions.cs
Normal file
96
EnvelopeGenerator.API/Extensions/ReceiverClaimExtensions.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
using Microsoft.IdentityModel.JsonWebTokens;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides helper methods for working with envelope-specific authentication claims.
|
||||||
|
/// </summary>
|
||||||
|
public static class ReceiverClaimExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="claimType"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
private static string GetRequiredClaimValue(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 GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes)
|
||||||
|
{
|
||||||
|
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
|
||||||
|
{
|
||||||
|
var value = user.FindFirstValue(claimType);
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
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(s) '{string.Join("', '", claimTypes)}' are 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 EnvelopeUuid(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated receiver signature from the claims.
|
||||||
|
/// </summary>
|
||||||
|
public static string ReceiverSignature(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated receiver email address from the claims.
|
||||||
|
/// </summary>
|
||||||
|
public static string ReceiverMail(this ClaimsPrincipal user)
|
||||||
|
=> user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated envelope identifier from the claims.
|
||||||
|
/// </summary>
|
||||||
|
public static int EnvelopeId(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId);
|
||||||
|
if (int.TryParse(envIdStr, out var envId))
|
||||||
|
return envId;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authenticated receiver identifier from the claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public static int ReceiverId(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId);
|
||||||
|
if (int.TryParse(rcvIdStr, out var rcvId))
|
||||||
|
return rcvId;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
95
EnvelopeGenerator.API/Extensions/SenderClaimExtensions.cs
Normal file
95
EnvelopeGenerator.API/Extensions/SenderClaimExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
EnvelopeGenerator.API/Jenkinsfile
vendored
Normal file
10
EnvelopeGenerator.API/Jenkinsfile
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
stages {
|
||||||
|
stage('Build') {
|
||||||
|
steps {
|
||||||
|
sh 'dotnet build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Middleware;
|
||||||
|
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware for handling exceptions globally in the application.
|
||||||
|
/// Captures exceptions thrown during the request pipeline execution,
|
||||||
|
/// logs them, and returns an appropriate HTTP response with a JSON error message.
|
||||||
|
/// </summary>
|
||||||
|
public class ExceptionHandlingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">The next middleware in the request pipeline.</param>
|
||||||
|
/// <param name="logger">The logger instance for logging exceptions.</param>
|
||||||
|
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the middleware to handle the HTTP request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The HTTP context of the current request.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context); // Continue down the pipeline
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await HandleExceptionAsync(context, ex, _logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles exceptions by logging them and writing an appropriate JSON response.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The HTTP context of the current request.</param>
|
||||||
|
/// <param name="exception">The exception that occurred.</param>
|
||||||
|
/// <param name="logger">The logger instance for logging the exception.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
string message;
|
||||||
|
|
||||||
|
switch (exception)
|
||||||
|
{
|
||||||
|
case BadRequestException badRequestEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
message = badRequestEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotFoundException notFoundEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
message = notFoundEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ForbiddenException forbiddenEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||||
|
message = forbiddenEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
message = "An unexpected error occurred.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
14
EnvelopeGenerator.API/Models/Auth.cs
Normal file
14
EnvelopeGenerator.API/Models/Auth.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||||
|
{
|
||||||
|
public bool HasAccessCode => AccessCode is not null;
|
||||||
|
|
||||||
|
public bool HasSmsCode => SmsCode is not null;
|
||||||
|
|
||||||
|
public bool HasAuthenticatorCode => AuthenticatorCode is not null;
|
||||||
|
|
||||||
|
public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1;
|
||||||
|
|
||||||
|
public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode);
|
||||||
|
}
|
||||||
28
EnvelopeGenerator.API/Models/AuthTokenKeys.cs
Normal file
28
EnvelopeGenerator.API/Models/AuthTokenKeys.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the keys and default values used for authentication token handling
|
||||||
|
/// within the Envelope Generator API.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthTokenKeys
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the cookie used to store the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string Cookie { get; init; } = "AuthToken";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the query string parameter used to pass the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string QueryString { get; init; } = "AuthToken";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the expected issuer value for the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string Issuer { get; init; } = "auth.digitaldata.works";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the expected audience value for the authentication token.
|
||||||
|
/// </summary>
|
||||||
|
public string Audience { get; init; } = "sign-flow.digitaldata.works";
|
||||||
|
}
|
||||||
12
EnvelopeGenerator.API/Models/ConnectionString.cs
Normal file
12
EnvelopeGenerator.API/Models/ConnectionString.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class ConnectionString
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
60
EnvelopeGenerator.API/Models/ContactLink.cs
Normal file
60
EnvelopeGenerator.API/Models/ContactLink.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a hyperlink for contact purposes with various HTML attributes.
|
||||||
|
/// </summary>
|
||||||
|
public class ContactLink
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the label of the hyperlink.
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; init; } = "Contact";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL that the hyperlink points to.
|
||||||
|
/// </summary>
|
||||||
|
public string Href { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the target where the hyperlink should open.
|
||||||
|
/// Commonly used values are "_blank", "_self", "_parent", "_top".
|
||||||
|
/// </summary>
|
||||||
|
public string Target { get; set; } = "_blank";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the relationship of the linked URL as space-separated link types.
|
||||||
|
/// Examples include "nofollow", "noopener", "noreferrer".
|
||||||
|
/// </summary>
|
||||||
|
public string Rel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the filename that should be downloaded when clicking the hyperlink.
|
||||||
|
/// This attribute will only have an effect if the href attribute is set.
|
||||||
|
/// </summary>
|
||||||
|
public string Download { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the language of the linked resource. Useful when linking to
|
||||||
|
/// content in another language.
|
||||||
|
/// </summary>
|
||||||
|
public string HrefLang { get; set; } = "en";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the MIME type of the linked URL. Helps browsers to handle
|
||||||
|
/// the type correctly when the link is clicked.
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets additional information about the hyperlink, typically viewed
|
||||||
|
/// as a tooltip when the mouse hovers over the link.
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets an identifier for the hyperlink, unique within the HTML document.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
EnvelopeGenerator.API/Models/Culture.cs
Normal file
17
EnvelopeGenerator.API/Models/Culture.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class Culture
|
||||||
|
{
|
||||||
|
private string _language = string.Empty;
|
||||||
|
public string Language { get => _language;
|
||||||
|
init {
|
||||||
|
_language = value;
|
||||||
|
Info = new(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public string FIClass { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public CultureInfo? Info { get; init; }
|
||||||
|
}
|
||||||
12
EnvelopeGenerator.API/Models/Cultures.cs
Normal file
12
EnvelopeGenerator.API/Models/Cultures.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class Cultures : List<Culture>
|
||||||
|
{
|
||||||
|
public IEnumerable<string> Languages => this.Select(c => c.Language);
|
||||||
|
|
||||||
|
public IEnumerable<string> FIClasses => this.Select(c => c.FIClass);
|
||||||
|
|
||||||
|
public Culture Default => this.First();
|
||||||
|
|
||||||
|
public Culture? this[string? language] => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault();
|
||||||
|
}
|
||||||
6
EnvelopeGenerator.API/Models/CustomImages.cs
Normal file
6
EnvelopeGenerator.API/Models/CustomImages.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class CustomImages : Dictionary<string, Image>
|
||||||
|
{
|
||||||
|
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
|
||||||
|
}
|
||||||
7
EnvelopeGenerator.API/Models/EnvelopeReceiverLogin.cs
Normal file
7
EnvelopeGenerator.API/Models/EnvelopeReceiverLogin.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request body for the envelope-receiver login endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="AccessCode">The access code sent to the receiver.</param>
|
||||||
|
public record EnvelopeReceiverLogin(string? AccessCode = null);
|
||||||
10
EnvelopeGenerator.API/Models/ErrorViewModel.cs
Normal file
10
EnvelopeGenerator.API/Models/ErrorViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class ErrorViewModel
|
||||||
|
{
|
||||||
|
public string Title { get; init; } = "404";
|
||||||
|
|
||||||
|
public string Subtitle { get; init; } = "Hmmm...";
|
||||||
|
|
||||||
|
public string Body { get; init; } = "It looks like one of the developers fell asleep";
|
||||||
|
}
|
||||||
10
EnvelopeGenerator.API/Models/Image.cs
Normal file
10
EnvelopeGenerator.API/Models/Image.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class Image
|
||||||
|
{
|
||||||
|
public string Src { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public Dictionary<string, string> Classes { get; init; } = new();
|
||||||
|
|
||||||
|
public string GetClassIn(string page) => Classes.TryGetValue(page, out var cls) && cls is not null ? cls : string.Empty;
|
||||||
|
}
|
||||||
13
EnvelopeGenerator.API/Models/Login.cs
Normal file
13
EnvelopeGenerator.API/Models/Login.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Password">Das erforderliche Passwort für das Login.</param>
|
||||||
|
/// <param name="UserId">Die optionale ID des Benutzers.</param>
|
||||||
|
/// <param name="Username">Der optionale Benutzername.</param>
|
||||||
|
public record Login([Required] string Password, int? UserId = null, string? Username = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
6
EnvelopeGenerator.API/Models/MainViewModel.cs
Normal file
6
EnvelopeGenerator.API/Models/MainViewModel.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
public class MainViewModel
|
||||||
|
{
|
||||||
|
public string? Title { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public record Annotation : IAnnotation
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
|
||||||
|
#region Bound Annotation
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? HorBoundAnnotName { get; init; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? VerBoundAnnotName { get; init; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Layout
|
||||||
|
[JsonIgnore]
|
||||||
|
public double? MarginLeft { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double MarginLeftRatio { get; init; } = 1;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double? MarginTop { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double MarginTopRatio { get; init; } = 1;
|
||||||
|
|
||||||
|
public double? Width { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double WidthRatio { get; init; } = 1;
|
||||||
|
|
||||||
|
public double? Height { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double HeightRatio { get; init; } = 1;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Position
|
||||||
|
public double Left => (MarginLeft ?? 0) + (HorBoundAnnot?.HorBoundary ?? 0);
|
||||||
|
|
||||||
|
public double Top => (MarginTop ?? 0) + (VerBoundAnnot?.VerBoundary ?? 0);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Boundary
|
||||||
|
[JsonIgnore]
|
||||||
|
public double HorBoundary => Left + (Width ?? 0);
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public double VerBoundary => Top + (Height ?? 0);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region BoundAnnot
|
||||||
|
[JsonIgnore]
|
||||||
|
public Annotation? HorBoundAnnot { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public Annotation? VerBoundAnnot { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public Color? BackgroundColor { get; init; }
|
||||||
|
|
||||||
|
#region Border
|
||||||
|
public Color? BorderColor { get; init; }
|
||||||
|
|
||||||
|
public string? BorderStyle { get; init; }
|
||||||
|
|
||||||
|
public int? BorderWidth { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
internal Annotation Default
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
// To set null value, annotation must have null (0) value but null must has non-null value
|
||||||
|
if (MarginLeft == null && value.MarginLeft != null)
|
||||||
|
MarginLeft = value.MarginLeft * MarginLeftRatio;
|
||||||
|
|
||||||
|
if (MarginTop == null && value.MarginTop != null)
|
||||||
|
MarginTop = value.MarginTop * MarginTopRatio;
|
||||||
|
|
||||||
|
if (Width == null && value.Width != null)
|
||||||
|
Width = value.Width * WidthRatio;
|
||||||
|
|
||||||
|
if (Height == null && value.Height != null)
|
||||||
|
Height = value.Height * HeightRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public class AnnotationParams
|
||||||
|
{
|
||||||
|
public AnnotationParams()
|
||||||
|
{
|
||||||
|
_AnnotationJSObjectInitor = new(CreateAnnotationJSObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Background? Background { get; init; }
|
||||||
|
|
||||||
|
#region Annotation
|
||||||
|
[JsonIgnore]
|
||||||
|
public Annotation? DefaultAnnotation { get; init; }
|
||||||
|
|
||||||
|
private readonly List<Annotation> _annots = new List<Annotation>();
|
||||||
|
|
||||||
|
public bool TryGet(string name, out Annotation annotation)
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||||
|
annotation = _annots.FirstOrDefault(a => a.Name == name);
|
||||||
|
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||||
|
return annotation is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public required IEnumerable<Annotation> Annotations
|
||||||
|
{
|
||||||
|
get => _annots;
|
||||||
|
init
|
||||||
|
{
|
||||||
|
_annots = value.ToList();
|
||||||
|
|
||||||
|
if (DefaultAnnotation is not null)
|
||||||
|
foreach (var annot in _annots)
|
||||||
|
annot.Default = DefaultAnnotation;
|
||||||
|
|
||||||
|
for (int i = 0; i < _annots.Count; i++)
|
||||||
|
{
|
||||||
|
#region set bound annotations
|
||||||
|
// horizontal
|
||||||
|
if (_annots[i].HorBoundAnnotName is string horBoundAnnotName)
|
||||||
|
if (TryGet(horBoundAnnotName, out var horBoundAnnot))
|
||||||
|
_annots[i].HorBoundAnnot = horBoundAnnot;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||||
|
|
||||||
|
// vertical
|
||||||
|
if (_annots[i].VerBoundAnnotName is string verBoundAnnotName)
|
||||||
|
if (TryGet(verBoundAnnotName, out var verBoundAnnot))
|
||||||
|
_annots[i].VerBoundAnnot = verBoundAnnot;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined.");
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region AnnotationJSObject
|
||||||
|
private Dictionary<string, IAnnotation> CreateAnnotationJSObject()
|
||||||
|
{
|
||||||
|
var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation);
|
||||||
|
|
||||||
|
if (Background is not null)
|
||||||
|
{
|
||||||
|
Background.Locate(_annots);
|
||||||
|
dict.Add(Background.Name.ToLower(), Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Lazy<Dictionary<string, IAnnotation>> _AnnotationJSObjectInitor;
|
||||||
|
|
||||||
|
public Dictionary<string, IAnnotation> AnnotationJSObject => _AnnotationJSObjectInitor.Value;
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Background is an annotation for the PSPDF Kit. However, it has no function.
|
||||||
|
/// It is only the first annotation as a background for other annotations.
|
||||||
|
/// </summary>
|
||||||
|
public record Background : IAnnotation
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
public double Margin { get; init; }
|
||||||
|
|
||||||
|
public string Name { get; } = "Background";
|
||||||
|
|
||||||
|
public double? Width { get; set; }
|
||||||
|
|
||||||
|
public double? Height { get; set; }
|
||||||
|
|
||||||
|
public double Left { get; set; }
|
||||||
|
|
||||||
|
public double Top { get; set; }
|
||||||
|
|
||||||
|
public Color? BackgroundColor { get; init; }
|
||||||
|
|
||||||
|
#region Border
|
||||||
|
public Color? BorderColor { get; init; }
|
||||||
|
|
||||||
|
public string? BorderStyle { get; init; }
|
||||||
|
|
||||||
|
public int? BorderWidth { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Locate(IEnumerable<IAnnotation> annotations)
|
||||||
|
{
|
||||||
|
// set Top
|
||||||
|
if (annotations.MinBy(a => a.Top)?.Top is double minTop)
|
||||||
|
Top = minTop;
|
||||||
|
|
||||||
|
// set Left
|
||||||
|
if (annotations.MinBy(a => a.Left)?.Left is double minLeft)
|
||||||
|
Left = minLeft;
|
||||||
|
|
||||||
|
// set Width
|
||||||
|
if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight)
|
||||||
|
Width = maxRight - Left;
|
||||||
|
|
||||||
|
// set Height
|
||||||
|
if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom)
|
||||||
|
Height = maxBottom - Top;
|
||||||
|
|
||||||
|
// add margins
|
||||||
|
Top -= Margin;
|
||||||
|
Left -= Margin;
|
||||||
|
Width += Margin * 2;
|
||||||
|
Height += Margin * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
EnvelopeGenerator.API/Models/PsPdfKitAnnotation/Color.cs
Normal file
10
EnvelopeGenerator.API/Models/PsPdfKitAnnotation/Color.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public record Color
|
||||||
|
{
|
||||||
|
public int R { get; init; } = 0;
|
||||||
|
|
||||||
|
public int G { get; init; } = 0;
|
||||||
|
|
||||||
|
public int B { get; init; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0;
|
||||||
|
|
||||||
|
public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
|
||||||
|
public interface IAnnotation
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
double? Width { get; }
|
||||||
|
|
||||||
|
double? Height { get; }
|
||||||
|
|
||||||
|
double Left { get; }
|
||||||
|
|
||||||
|
double Top { get; }
|
||||||
|
|
||||||
|
Color? BackgroundColor { get; }
|
||||||
|
|
||||||
|
Color? BorderColor { get; }
|
||||||
|
|
||||||
|
string? BorderStyle { get; }
|
||||||
|
|
||||||
|
int? BorderWidth { get; }
|
||||||
|
}
|
||||||
17
EnvelopeGenerator.API/Models/TFARegParams.cs
Normal file
17
EnvelopeGenerator.API/Models/TFARegParams.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the parameters for two-factor authentication (2FA) registration.
|
||||||
|
/// </summary>
|
||||||
|
public class TFARegParams
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum allowed time for completing the registration process.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan TimeLimit { get; init; } = new(0, 30, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The deadline for registration, calculated as the current time plus the <see cref="TimeLimit"/>.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks);
|
||||||
|
}
|
||||||
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal file
18
EnvelopeGenerator.API/Options/CacheOptions.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace EnvelopeGenerator.API.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for distributed caching.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CacheOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration section name in appsettings.json.
|
||||||
|
/// </summary>
|
||||||
|
public const string SectionName = "Cache";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signature cache expiration time.
|
||||||
|
/// If null, signatures will not expire automatically.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? SignatureCacheExpiration { get; set; }
|
||||||
|
}
|
||||||
355
EnvelopeGenerator.API/Program.cs
Normal file
355
EnvelopeGenerator.API/Program.cs
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
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;
|
||||||
|
using System.Globalization;
|
||||||
|
using Scalar.AspNetCore;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using DigitalData.UserManager.DependencyInjection;
|
||||||
|
using EnvelopeGenerator.Application;
|
||||||
|
using DigitalData.Auth.Client;
|
||||||
|
using DigitalData.Core.Abstractions;
|
||||||
|
using EnvelopeGenerator.API.Models;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
|
using EnvelopeGenerator.API.Middleware;
|
||||||
|
using EnvelopeGenerator.API.Options;
|
||||||
|
using NLog.Web;
|
||||||
|
using NLog;
|
||||||
|
using DigitalData.Auth.Claims;
|
||||||
|
using EnvelopeGenerator.API;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
||||||
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
|
logger.Info("Logging initialized!");
|
||||||
|
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Host.UseNLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = builder.Configuration;
|
||||||
|
|
||||||
|
var deferredProvider = new DeferredServiceProvider();
|
||||||
|
|
||||||
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||||
|
});
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||||
|
|
||||||
|
// CORS Policy
|
||||||
|
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
||||||
|
throw new InvalidOperationException("AllowedOrigins section is missing in the configuration.");
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowSpecificOriginsPolicy", builder =>
|
||||||
|
{
|
||||||
|
builder.WithOrigins(allowedOrigins)
|
||||||
|
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowCredentials();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Swagger
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
|
{
|
||||||
|
Version = "v1",
|
||||||
|
Title = "signFLOW Absender-API",
|
||||||
|
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschlägen in der signFLOW-Anwendung.",
|
||||||
|
Contact = new OpenApiContact
|
||||||
|
{
|
||||||
|
Name = "Digital Data GmbH",
|
||||||
|
Url = new Uri("https://digitaldata.works/digitale-signatur#kontakt"),
|
||||||
|
Email = "info-flow@digitaldata.works"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "JWT-Autorisierungs-Header unter Verwendung des Bearer-Schemas.",
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Array.Empty<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
|
||||||
|
foreach (var xmlFile in xmlFiles)
|
||||||
|
{
|
||||||
|
options.IncludeXmlComments(xmlFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.DocumentFilter<EnvelopeGenerator.API.Documentation.AuthProxyDocumentFilter>();
|
||||||
|
});
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
builder.Services.AddOpenApi();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
//Add EF Core dbcontext
|
||||||
|
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
|
||||||
|
var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default";
|
||||||
|
var connStr = config.GetConnectionString(cnnStrName)
|
||||||
|
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
|
||||||
|
|
||||||
|
builder.Services.Configure<ConnectionString>(cs => cs.Value = connStr);
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
|
||||||
|
|
||||||
|
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"));
|
||||||
|
|
||||||
|
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddJwtBearer(AuthScheme.Sender, opt =>
|
||||||
|
{
|
||||||
|
opt.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
||||||
|
{
|
||||||
|
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
||||||
|
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
||||||
|
return [publicKey.SecurityKey];
|
||||||
|
},
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = authTokenKeys.Issuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = authTokenKeys.Audience,
|
||||||
|
};
|
||||||
|
|
||||||
|
opt.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
// if there is no token read related cookie or query string
|
||||||
|
if (context.Token is null) // if there is no token
|
||||||
|
{
|
||||||
|
if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null)
|
||||||
|
context.Token = cookieToken;
|
||||||
|
else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken))
|
||||||
|
context.Token = queryStrToken;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// Per-envelope receiver scheme: reads the JWT from the cookie named
|
||||||
|
// AuthTokenSignFLOWReceiver.{envelope_key} where envelope_key is the
|
||||||
|
// last path segment of the request URL.
|
||||||
|
// This enables simultaneous authentication for multiple envelopes
|
||||||
|
// within the same browser session.
|
||||||
|
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
||||||
|
{
|
||||||
|
opt.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
||||||
|
{
|
||||||
|
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
||||||
|
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
||||||
|
return [publicKey.SecurityKey];
|
||||||
|
},
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = authTokenKeys.Issuer,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = authTokenKeys.Audience,
|
||||||
|
};
|
||||||
|
|
||||||
|
opt.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
// Derive the envelope key from the last route segment: /{envelope_key}
|
||||||
|
var envelopeKey = paths?.LastOrDefault();
|
||||||
|
|
||||||
|
if (envelopeKey is not null)
|
||||||
|
{
|
||||||
|
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||||
|
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null)
|
||||||
|
context.Token = cookieToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
OnTokenValidated = context =>
|
||||||
|
{
|
||||||
|
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var envelopeKey = paths?.LastOrDefault();
|
||||||
|
|
||||||
|
var sub = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
||||||
|
?? context.Principal?.FindFirst("sub")?.Value;
|
||||||
|
|
||||||
|
if (envelopeKey is null || sub != envelopeKey)
|
||||||
|
context.Fail("Envelope key in the path does not match the token subject.");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
|
.AddCookie(options =>
|
||||||
|
{
|
||||||
|
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
|
||||||
|
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
|
||||||
|
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
|
||||||
|
options.LoginPath = "/api/auth/login";
|
||||||
|
options.LogoutPath = "/api/auth/logout";
|
||||||
|
options.SlidingExpiration = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorizationBuilder()
|
||||||
|
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
|
||||||
|
.RequireRole(Role.Sender, Role.Receiver.Full)
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
||||||
|
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||||
|
.RequireRole(Role.Sender)
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||||
|
.AddPolicy(AuthPolicy.Receiver, policy => policy
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Receiver)
|
||||||
|
.RequireAuthenticatedUser()
|
||||||
|
.RequireRole(Role.Receiver.Full, "receiver"))
|
||||||
|
.AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA));
|
||||||
|
|
||||||
|
// User manager
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
builder.Services.AddUserManager<EGDbContext>();
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
|
// LDAP
|
||||||
|
builder.ConfigureBySection<DirectorySearchOptions>();
|
||||||
|
builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions"));
|
||||||
|
|
||||||
|
// Localizer
|
||||||
|
builder.Services.AddCookieBasedLocalizer();
|
||||||
|
|
||||||
|
// Cache options
|
||||||
|
builder.Services.Configure<CacheOptions>(config.GetSection(CacheOptions.SectionName));
|
||||||
|
|
||||||
|
// Distributed Cache - SQL Server
|
||||||
|
builder.Services.AddDistributedSqlServerCache(options =>
|
||||||
|
{
|
||||||
|
config.GetSection("Cache:SqlServer").Bind(options);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(options.ConnectionString))
|
||||||
|
{
|
||||||
|
options.ConnectionString = connStr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Envelope generator serives
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
builder.Services
|
||||||
|
.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
||||||
|
{
|
||||||
|
opt.AddDbTriggerParams(config);
|
||||||
|
opt.AddDbContext((provider, options) =>
|
||||||
|
{
|
||||||
|
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
|
||||||
|
options.UseSqlServer(connStr)
|
||||||
|
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
|
||||||
|
.EnableSensitiveDataLogging()
|
||||||
|
.EnableDetailedErrors();
|
||||||
|
});
|
||||||
|
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
|
||||||
|
})
|
||||||
|
.AddEnvelopeGeneratorServices(config);
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
deferredProvider.Factory = () => app.Services;
|
||||||
|
|
||||||
|
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
app.MapOpenApi();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment() || (app.IsDevOrDiP() && config.GetValue<bool>("UseSwagger")))
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
app.MapScalarApiReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set CORS policy
|
||||||
|
app.UseCors("AllowSpecificOriginsPolicy");
|
||||||
|
|
||||||
|
// Localizer
|
||||||
|
string[] supportedCultureNames = ["de-DE", "en-US"];
|
||||||
|
IList<CultureInfo> list = [.. supportedCultureNames.Select(cn => new CultureInfo(cn))];
|
||||||
|
var cultureInfo = list.FirstOrDefault() ?? throw new InvalidOperationException("There is no supported culture.");
|
||||||
|
var requestLocalizationOptions = new RequestLocalizationOptions
|
||||||
|
{
|
||||||
|
SupportedCultures = list,
|
||||||
|
SupportedUICultures = list
|
||||||
|
};
|
||||||
|
requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
|
||||||
|
app.UseRequestLocalization(requestLocalizationOptions);
|
||||||
|
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseDefaultFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Catch-all YARP proxy — only forward requests that are not swagger/scalar/openapi paths.
|
||||||
|
app.MapReverseProxy();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Stopped program because of exception");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?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>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
||||||
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
51
EnvelopeGenerator.API/Properties/launchSettings.json
Normal file
51
EnvelopeGenerator.API/Properties/launchSettings.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:4176",
|
||||||
|
"sslPort": 44300
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5131",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "sender",
|
||||||
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"angular": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "",
|
||||||
|
"applicationUrl": "https://localhost:7174;http://localhost:5131",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
EnvelopeGenerator.API/appsettings.Development.json
Normal file
29
EnvelopeGenerator.API/appsettings.Development.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReverseProxy": {
|
||||||
|
"Clusters": {
|
||||||
|
"receiver-ui": {
|
||||||
|
"Destinations": {
|
||||||
|
"primary": {
|
||||||
|
"Address": "https://localhost:52936"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AuthClientParams": {
|
||||||
|
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||||
|
"PublicKeys": [
|
||||||
|
{
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "sign-flow.digitaldata.works"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RetryDelay": "00:00:05"
|
||||||
|
}
|
||||||
|
}
|
||||||
256
EnvelopeGenerator.API/appsettings.json
Normal file
256
EnvelopeGenerator.API/appsettings.json
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
{
|
||||||
|
"UseSwagger": true,
|
||||||
|
"UseDbMigration": false,
|
||||||
|
"DiPMode": true,
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"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;"
|
||||||
|
},
|
||||||
|
"DirectorySearchOptions": {
|
||||||
|
"ServerName": "DD-VMP01-DC01",
|
||||||
|
"Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works",
|
||||||
|
"UserCacheExpirationDays": 1,
|
||||||
|
"CustomSearchFilters": {
|
||||||
|
"User": "(&(objectClass=user)(sAMAccountName=*))",
|
||||||
|
"Group": "(&(objectClass=group)(samAccountName=*))"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AuthClientParams": {
|
||||||
|
"Url": "http://172.24.12.39:9090/auth-hub",
|
||||||
|
"PublicKeys": [
|
||||||
|
{
|
||||||
|
"Issuer": "auth.digitaldata.works",
|
||||||
|
"Audience": "sign-flow.digitaldata.works"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"RetryDelay": "00:00:05"
|
||||||
|
},
|
||||||
|
"AuthTokenKeys": {
|
||||||
|
"Cookie": "AuthToken",
|
||||||
|
"QueryString": "AuthToken",
|
||||||
|
"Issuer": "auth.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.
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self' 'nonce-{0}' 'unsafe-eval'",
|
||||||
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com:*",
|
||||||
|
"img-src 'self' data: https: blob:",
|
||||||
|
"font-src 'self' https://fonts.gstatic.com:*",
|
||||||
|
"connect-src 'self' https://nominatim.openstreetmap.org:* http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* blob:",
|
||||||
|
"frame-src 'self'",
|
||||||
|
"media-src 'self'",
|
||||||
|
"object-src 'self'"
|
||||||
|
],
|
||||||
|
"NLog": {
|
||||||
|
"throwConfigExceptions": true,
|
||||||
|
"variables": {
|
||||||
|
"logDirectory": "E:\\LogFiles\\Digital Data\\signFlow",
|
||||||
|
"logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Web"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"infoLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
},
|
||||||
|
"errorLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
},
|
||||||
|
"criticalLogs": {
|
||||||
|
"type": "File",
|
||||||
|
"fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log",
|
||||||
|
"maxArchiveDays": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Trace, Debug, Info, Warn, Error and *Fatal*
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"minLevel": "Info",
|
||||||
|
"maxLevel": "Warn",
|
||||||
|
"writeTo": "infoLogs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Error",
|
||||||
|
"writeTo": "errorLogs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"logger": "*",
|
||||||
|
"level": "Fatal",
|
||||||
|
"writeTo": "criticalLogs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ContactLink": {
|
||||||
|
"Label": "Kontakt",
|
||||||
|
"Href": "https://digitaldata.works/",
|
||||||
|
"HrefLang": "de",
|
||||||
|
"Target": "_blank",
|
||||||
|
"Title": "Digital Data GmbH"
|
||||||
|
},
|
||||||
|
/* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx).
|
||||||
|
To add a new language, first you should write the required resx file.
|
||||||
|
first is the default culture name. */
|
||||||
|
"Cultures": [
|
||||||
|
{
|
||||||
|
"Language": "de-DE",
|
||||||
|
"FIClass": "fi-de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Language": "en-US",
|
||||||
|
"FIClass": "fi-us"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"DisableMultiLanguage": false,
|
||||||
|
"Regexes": [
|
||||||
|
{
|
||||||
|
"Pattern": "/^\\p{L}+(?:([\\ \\-\\']|(\\.\\ ))\\p{L}+)*$/u",
|
||||||
|
"Name": "City",
|
||||||
|
"Platforms": [ ".NET" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Pattern": "/^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$/",
|
||||||
|
"Name": "City",
|
||||||
|
"Platforms": [ "javascript" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"CustomImages": {
|
||||||
|
"App": {
|
||||||
|
"Src": "/img/DD_signFLOW_LOGO.png",
|
||||||
|
"Classes": {
|
||||||
|
"Main": "signFlow-logo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Company": {
|
||||||
|
"Src": "/img/digital_data.svg",
|
||||||
|
"Classes": {
|
||||||
|
"Show": "dd-show-logo",
|
||||||
|
"Locked": "dd-locked-logo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DispatcherParams": {
|
||||||
|
"SendingProfile": 1,
|
||||||
|
"AddedWho": "DDEnvelopGenerator",
|
||||||
|
"ReminderTypeId": 202377,
|
||||||
|
"EmailAttmt1": ""
|
||||||
|
},
|
||||||
|
"MailParams": {
|
||||||
|
"Placeholders": {
|
||||||
|
"[NAME_PORTAL]": "signFlow",
|
||||||
|
"[SIGNATURE_TYPE]": "signieren",
|
||||||
|
"[REASON]": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GtxMessagingParams": {
|
||||||
|
"Uri": "https://rest.gtx-messaging.net",
|
||||||
|
"Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json",
|
||||||
|
"Headers": {},
|
||||||
|
"QueryParams": {
|
||||||
|
"from": "signFlow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TFARegParams": {
|
||||||
|
"TimeLimit": "00:30:00"
|
||||||
|
},
|
||||||
|
"DbTriggerParams": {
|
||||||
|
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
|
"EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
|
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
||||||
|
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
||||||
|
"Receiver": [],
|
||||||
|
"EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ]
|
||||||
|
},
|
||||||
|
"Cache": {
|
||||||
|
"SignatureCacheExpiration": null,
|
||||||
|
"SqlServer": {
|
||||||
|
"ConnectionString": null,
|
||||||
|
"SchemaName": "dbo",
|
||||||
|
"TableName": "TBDD_CACHE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MainPageTitle": null,
|
||||||
|
"AnnotationParams": {
|
||||||
|
"Background": {
|
||||||
|
"Margin": 0.20,
|
||||||
|
"BackgroundColor": {
|
||||||
|
"R": 222,
|
||||||
|
"G": 220,
|
||||||
|
"B": 215
|
||||||
|
},
|
||||||
|
"BorderColor": {
|
||||||
|
"R": 204,
|
||||||
|
"G": 202,
|
||||||
|
"B": 198
|
||||||
|
},
|
||||||
|
"BorderStyle": "underline",
|
||||||
|
"BorderWidth": 4
|
||||||
|
},
|
||||||
|
"DefaultAnnotation": {
|
||||||
|
"Width": 1,
|
||||||
|
"Height": 0.5,
|
||||||
|
"MarginTop": 1
|
||||||
|
},
|
||||||
|
"Annotations": [
|
||||||
|
{
|
||||||
|
"Name": "Signature",
|
||||||
|
"MarginTop": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "PositionLabel",
|
||||||
|
"VerBoundAnnotName": "Signature",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": 0.22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Position",
|
||||||
|
"VerBoundAnnotName": "PositionLabel",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": -0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "CityLabel",
|
||||||
|
"VerBoundAnnotName": "Position",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "City",
|
||||||
|
"VerBoundAnnotName": "CityLabel",
|
||||||
|
"WidthRatio": 1.2,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": -0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "DateLabel",
|
||||||
|
"VerBoundAnnotName": "City",
|
||||||
|
"WidthRatio": 1.55,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Date",
|
||||||
|
"VerBoundAnnotName": "DateLabel",
|
||||||
|
"WidthRatio": 1.55,
|
||||||
|
"HeightRatio": 0.5,
|
||||||
|
"MarginTopRatio": -0.1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
185
EnvelopeGenerator.API/yarp.json
Normal file
185
EnvelopeGenerator.API/yarp.json
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"ReverseProxy": {
|
||||||
|
"Routes": {
|
||||||
|
"receiver-ui-root": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 300,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-sender": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 100,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/sender/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-envelope": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 100,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/envelope/{EnvelopeKey}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-envelope-dxreportviewer": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 90,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/envelope/{EnvelopeKey}/DxReportViewer",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/index.html" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-blazor-framework": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/_framework/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-blazor-content": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/_content/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-static-css": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/css/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{
|
||||||
|
"ResponseHeader": "Cache-Control",
|
||||||
|
"Set": "no-cache, no-store, must-revalidate",
|
||||||
|
"When": "Always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-static-js": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/js/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{
|
||||||
|
"ResponseHeader": "Cache-Control",
|
||||||
|
"Set": "no-cache, no-store, must-revalidate",
|
||||||
|
"When": "Always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"receiver-ui-fake-data": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/fake-data/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-appsettings": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/appsettings.json",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-appsettings-dev": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/appsettings.Development.json",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-styles": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 50,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/EnvelopeGenerator.ReceiverUI.styles.css",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-fonts": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/fonts/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receiver-ui-images": {
|
||||||
|
"ClusterId": "receiver-ui",
|
||||||
|
"Order": 200,
|
||||||
|
"Match": {
|
||||||
|
"Path": "/images/{**catch-all}",
|
||||||
|
"Methods": [ "GET", "HEAD" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth-login": {
|
||||||
|
"ClusterId": "auth-hub",
|
||||||
|
"Match": {
|
||||||
|
"Path": "/api/auth",
|
||||||
|
"Methods": [ "POST" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathSet": "/api/auth/sign-flow" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"auth-envelope-receiver-login": {
|
||||||
|
"ClusterId": "auth-hub",
|
||||||
|
"Match": {
|
||||||
|
"Path": "/api/Auth/envelope-receiver/{key}",
|
||||||
|
"Methods": [ "POST" ]
|
||||||
|
},
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
|
||||||
|
{
|
||||||
|
"QueryValueParameter": "cookie",
|
||||||
|
"Set": "true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Clusters": {
|
||||||
|
"receiver-ui": {
|
||||||
|
"Destinations": {
|
||||||
|
"primary": {
|
||||||
|
"Address": "https://localhost:52936"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth-hub": {
|
||||||
|
"Destinations": {
|
||||||
|
"primary": {
|
||||||
|
"Address": "http://172.24.12.39:9090"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ public sealed record Signature
|
|||||||
public string? Position
|
public string? Position
|
||||||
{
|
{
|
||||||
get => _position;
|
get => _position;
|
||||||
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,4 +62,4 @@ public sealed record Signature
|
|||||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Place { get; init; }
|
public required string Place { get; init; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
|
||||||
|
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||||
|
{
|
||||||
|
public required Envelope Envelope { get; init; }
|
||||||
|
|
||||||
|
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||||
|
|
||||||
|
public required string Reason { get; init; }
|
||||||
|
|
||||||
|
public required string SenderReference { get; init; }
|
||||||
|
|
||||||
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||||
|
|
||||||
|
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||||
|
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||||
|
|
||||||
|
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||||
|
{
|
||||||
|
public SendDeletedMailHandler(
|
||||||
|
IRepository<EmailTemplate> tempRepo,
|
||||||
|
IRepository<EmailOut> emailOutRepo,
|
||||||
|
IOptions<MailParams> mailParamsOptions,
|
||||||
|
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||||
|
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||||
|
{
|
||||||
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
|
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||||
|
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||||
|
{
|
||||||
|
var senderName = notification.Envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
|
senderName = notification.SenderReference;
|
||||||
|
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||||
|
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||||
|
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||||
|
["[REASON]"] = notification.Reason
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,21 +65,18 @@ public static class DependencyInjection
|
|||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
|
|
||||||
// 1. AnnotationBehavior - Saves annotations (executes second)
|
// 1. AnnotationBehavior - Saves annotations (executes second)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
|
|
||||||
|
|
||||||
// 2. AnnotationBehavior - Saves annotations for legacy web flow
|
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||||
|
|
||||||
// 3. DocStatusBehavior - Creates document status
|
// 2. DocStatusBehavior - Creates document status (executes third)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||||
|
|
||||||
// 4. HistoryBehavior - Records history
|
// 3. HistoryBehavior - Records history (executes fourth)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||||
|
|
||||||
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
|
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,11 +43,7 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
|
|||||||
// If EnvelopeReceiver is not provided, query it from database
|
// If EnvelopeReceiver is not provided, query it from database
|
||||||
if (request.EnvelopeReceiver is null)
|
if (request.EnvelopeReceiver is null)
|
||||||
{
|
{
|
||||||
var er = await _erRepo.Query
|
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
||||||
.Where(request, notnull: true)
|
|
||||||
.Include(x => x.Envelope)
|
|
||||||
.Include(x => x.Receiver)
|
|
||||||
.SingleOrDefaultAsync(cancellationToken)
|
|
||||||
?? throw new NotFoundException("EnvelopeReceiver not found");
|
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||||
|
|
||||||
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
|||||||
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||||
|
|
||||||
var elements = await _elementRepo
|
var elements = await _elementRepo
|
||||||
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
||||||
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
.Where(e => e.ReceiverId == request.Receiver.Id)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
foreach (var element in elements)
|
foreach (var element in elements)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
|
||||||
|
|
||||||
|
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MinLength(1)]
|
||||||
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
internal int UserId { get; private set; }
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommand WithAuth(int userId)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Envelope> _envelopeRepository;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommandHandler(
|
||||||
|
IRepository<Envelope> envelopeRepository,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_envelopeRepository = envelopeRepository;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var reason = request.Reason?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
throw new BadRequestException("Please provide a reason.");
|
||||||
|
|
||||||
|
var envelope = await _envelopeRepository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.Include(e => e.EnvelopeReceivers)
|
||||||
|
.ThenInclude(er => er.Receiver)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
||||||
|
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
||||||
|
|
||||||
|
if (envelope.UserId != request.UserId)
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
||||||
|
|
||||||
|
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
||||||
|
? EnvelopeStatus.EnvelopeWithdrawn
|
||||||
|
: EnvelopeStatus.EnvelopeDeleted;
|
||||||
|
|
||||||
|
envelope.Status = targetStatus;
|
||||||
|
envelope.Comment = reason;
|
||||||
|
envelope.ChangedWhen = DateTime.Now;
|
||||||
|
|
||||||
|
await _envelopeRepository.UpdateAsync(envelope, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
var userReference = envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(userReference))
|
||||||
|
throw new BadRequestException("Envelope sender email is missing.");
|
||||||
|
|
||||||
|
await _mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = userReference,
|
||||||
|
Status = targetStatus,
|
||||||
|
Comment = reason
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||||
|
{
|
||||||
|
if (envelopeReceiver.Receiver is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await _mediator.Publish(new EnvelopeDeletedNotification
|
||||||
|
{
|
||||||
|
Envelope = envelope,
|
||||||
|
EnvelopeReceiver = envelopeReceiver,
|
||||||
|
Reason = reason,
|
||||||
|
SenderReference = userReference
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -204,24 +204,16 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#region Trigger
|
#region Trigger
|
||||||
// Configure entities to handle database triggers
|
// Configure entities to handle database triggers
|
||||||
void AddTrigger<T>() where T : class
|
void AddTrigger<T>() where T : class => _triggers
|
||||||
{
|
.Where(t => t.Key == typeof(T).Name)
|
||||||
var entityName = typeof(T).Name;
|
.SelectMany(t => t.Value)
|
||||||
|
.ForEach(tName =>
|
||||||
var configuredTriggerNames = _triggers
|
|
||||||
.Where(t => t.Key == entityName)
|
|
||||||
.SelectMany(t => t.Value)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var triggerName in configuredTriggerNames)
|
|
||||||
{
|
{
|
||||||
#if NET
|
#if NET
|
||||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(triggerName));
|
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(tName));
|
||||||
#endif
|
#endif
|
||||||
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", triggerName, entityName);
|
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", tName, typeof(T).Name);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: call add trigger methods with attributes and reflection
|
// TODO: call add trigger methods with attributes and reflection
|
||||||
AddTrigger<Config>();
|
AddTrigger<Config>();
|
||||||
@@ -251,4 +243,4 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.Server.Client.Models;
|
|
||||||
|
|
||||||
public sealed record SignatureSubmitDto
|
|
||||||
{
|
|
||||||
public required int Id { get; init; }
|
|
||||||
public required string DataUrl { get; init; }
|
|
||||||
public required string FullName { get; init; }
|
|
||||||
public string? Position { get; init; }
|
|
||||||
public required string Place { get; init; }
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,8 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
|||||||
public class EnvelopeService(IHttpClientFactory clientFactory)
|
public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private sealed record DeleteEnvelopeRequest(string Reason);
|
||||||
|
private sealed record ErrorResponse(string? Message);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches envelopes from the API with optional filters.
|
/// Fetches envelopes from the API with optional filters.
|
||||||
@@ -61,4 +63,35 @@ public class EnvelopeService(IHttpClientFactory clientFactory)
|
|||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
|
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(int envelopeId, string reason, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/Envelope/{envelopeId}")
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(new DeleteEnvelopeRequest(reason), options: _jsonOptions)
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await httpClient.SendAsync(request, cancel);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? message = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>(_jsonOptions, cancel);
|
||||||
|
message = error?.Message;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to delete envelope. Status: {statusCode} ({reasonPhrase}){(string.IsNullOrWhiteSpace(message) ? string.Empty : $" - {message}")}",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Server.Client.Models;
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Server.Client.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
@@ -22,32 +21,4 @@ public class SignatureService(IHttpClientFactory httpClientFactory)
|
|||||||
var result = await response.Content.ReadFromJsonAsync<List<SignatureDto>>(_jsonOptions, cancel);
|
var result = await response.Content.ReadFromJsonAsync<List<SignatureDto>>(_jsonOptions, cancel);
|
||||||
return result ?? [];
|
return result ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SubmitAsync(string envelopeKey, IEnumerable<SignatureSubmitDto> signatures, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
|
||||||
|
|
||||||
var payload = signatures
|
|
||||||
.Select(signature => new Signature
|
|
||||||
{
|
|
||||||
Id = signature.Id,
|
|
||||||
DataUrl = signature.DataUrl,
|
|
||||||
FullName = signature.FullName,
|
|
||||||
Position = signature.Position,
|
|
||||||
Place = signature.Place
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var response = await http.PostAsJsonAsync(
|
|
||||||
$"/api/Signature/submit/{Uri.EscapeDataString(envelopeKey)}",
|
|
||||||
payload,
|
|
||||||
cancel);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var body = await response.Content.ReadAsStringAsync(cancel);
|
|
||||||
throw new HttpRequestException(
|
|
||||||
$"Failed to submit signature for envelope {envelopeKey}: {(int)response.StatusCode} {response.StatusCode} - {body}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -521,6 +521,54 @@
|
|||||||
</BodyContentTemplate>
|
</BodyContentTemplate>
|
||||||
</DxPopup>
|
</DxPopup>
|
||||||
|
|
||||||
|
<DxPopup @bind-Visible="@_deletePopupVisible"
|
||||||
|
HeaderText="Umschlag löschen"
|
||||||
|
Width="560px"
|
||||||
|
MaxWidth="95vw"
|
||||||
|
ShowFooter="true"
|
||||||
|
CloseOnOutsideClick="false"
|
||||||
|
ShowCloseButton="true"
|
||||||
|
CloseOnEscape="true"
|
||||||
|
ShowHeader="true"
|
||||||
|
CssClass="sender-delete-popup">
|
||||||
|
<BodyContentTemplate>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:0.75rem;">
|
||||||
|
<p style="margin:0;color:#374151;font-size:0.9rem;line-height:1.45;">
|
||||||
|
Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.
|
||||||
|
</p>
|
||||||
|
@if (_selectedEnvelope is not null)
|
||||||
|
{
|
||||||
|
<p style="margin:0;color:#6b7280;font-size:0.825rem;">
|
||||||
|
Umschlag: <strong>#@_selectedEnvelope.Id - @_selectedEnvelope.Title</strong>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<DxMemo @bind-Text="_deleteReason"
|
||||||
|
NullText="Begründung eingeben..."
|
||||||
|
Rows="5"
|
||||||
|
MaxLength="2000"
|
||||||
|
CssClass="w-100" />
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_deleteErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger" style="margin:0;padding:0.5rem 0.75rem;font-size:0.85rem;">
|
||||||
|
@_deleteErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</BodyContentTemplate>
|
||||||
|
<FooterContentTemplate>
|
||||||
|
<div style="display:flex;justify-content:flex-end;gap:0.5rem;width:100%;">
|
||||||
|
<DxButton Text="Abbrechen"
|
||||||
|
RenderStyle="ButtonRenderStyle.Secondary"
|
||||||
|
Click="CloseDeletePopup"
|
||||||
|
Enabled="@(!_isDeletingEnvelope)" />
|
||||||
|
<DxButton Text="Löschen"
|
||||||
|
RenderStyle="ButtonRenderStyle.Danger"
|
||||||
|
Click="ConfirmDeleteEnvelopeAsync"
|
||||||
|
Enabled="@(!_isDeletingEnvelope)" />
|
||||||
|
</div>
|
||||||
|
</FooterContentTemplate>
|
||||||
|
</DxPopup>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private IEnumerable<EnvelopeDto>? _allEnvelopes;
|
private IEnumerable<EnvelopeDto>? _allEnvelopes;
|
||||||
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
||||||
@@ -549,6 +597,11 @@
|
|||||||
private bool _exportSelectedRowsOnly = false;
|
private bool _exportSelectedRowsOnly = false;
|
||||||
private bool _isExporting = false;
|
private bool _isExporting = false;
|
||||||
|
|
||||||
|
private bool _deletePopupVisible = false;
|
||||||
|
private bool _isDeletingEnvelope = false;
|
||||||
|
private string _deleteReason = string.Empty;
|
||||||
|
private string? _deleteErrorMessage;
|
||||||
|
|
||||||
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
// Stable keys used to store/restore each grid's layout in UserPreferences.GridLayouts
|
||||||
private const string GridKeyActive = "sender.active-envelopes";
|
private const string GridKeyActive = "sender.active-envelopes";
|
||||||
private const string GridKeyCompleted = "sender.completed-envelopes";
|
private const string GridKeyCompleted = "sender.completed-envelopes";
|
||||||
@@ -681,7 +734,51 @@
|
|||||||
void DeleteEnvelope()
|
void DeleteEnvelope()
|
||||||
{
|
{
|
||||||
if (_selectedEnvelope == null) return;
|
if (_selectedEnvelope == null) return;
|
||||||
// TODO: Show delete confirmation dialog
|
_deleteReason = string.Empty;
|
||||||
|
_deleteErrorMessage = null;
|
||||||
|
_deletePopupVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseDeletePopup()
|
||||||
|
{
|
||||||
|
if (_isDeletingEnvelope)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_deletePopupVisible = false;
|
||||||
|
_deleteErrorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task ConfirmDeleteEnvelopeAsync()
|
||||||
|
{
|
||||||
|
if (_selectedEnvelope is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_deleteReason))
|
||||||
|
{
|
||||||
|
_deleteErrorMessage = "Bitte geben Sie einen Löschgrund an.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDeletingEnvelope = true;
|
||||||
|
_deleteErrorMessage = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnvelopeService.DeleteAsync(_selectedEnvelope.Id, _deleteReason.Trim());
|
||||||
|
_deletePopupVisible = false;
|
||||||
|
_selectedEnvelope = null;
|
||||||
|
await LoadEnvelopesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_deleteErrorMessage = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isDeletingEnvelope = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task LogoutAsync()
|
async Task LogoutAsync()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
|
||||||
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
|
||||||
@inject SignatureService SignatureService
|
|
||||||
@inject AppVersionService AppVersion
|
@inject AppVersionService AppVersion
|
||||||
@inject IMemoryCache MemoryCache
|
@inject IMemoryCache MemoryCache
|
||||||
@inject ILogger<ReceiverSignedPage> Logger
|
@inject ILogger<ReceiverSignedPage> Logger
|
||||||
@@ -123,12 +122,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(_submitErrorMessage))
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger mb-0" role="alert" style="font-size: 0.85rem;">
|
|
||||||
@_submitErrorMessage
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</BodyContentTemplate>
|
</BodyContentTemplate>
|
||||||
<FooterContentTemplate>
|
<FooterContentTemplate>
|
||||||
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
|
||||||
@@ -168,48 +161,17 @@
|
|||||||
// ----- Submit / logout state -----
|
// ----- Submit / logout state -----
|
||||||
bool _isLoggingOut = false;
|
bool _isLoggingOut = false;
|
||||||
bool _submitConfirmVisible = false;
|
bool _submitConfirmVisible = false;
|
||||||
string? _submitErrorMessage;
|
|
||||||
|
|
||||||
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
|
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
|
||||||
|
|
||||||
async Task SubmitAndLogoutAsync()
|
async Task SubmitAndLogoutAsync()
|
||||||
{
|
{
|
||||||
if (_isLoggingOut) return;
|
if (_isLoggingOut) return;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _sig is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_isLoggingOut = true;
|
_isLoggingOut = true;
|
||||||
_submitConfirmVisible = false;
|
_submitConfirmVisible = false;
|
||||||
_submitErrorMessage = null;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!);
|
||||||
try
|
Navigation.NavigateTo("/", forceLoad: true);
|
||||||
{
|
|
||||||
if (_signatures.Count == 0)
|
|
||||||
throw new InvalidOperationException("No signature fields found for receiver.");
|
|
||||||
|
|
||||||
var payload = _signatures.Select(field => new SignatureSubmitDto
|
|
||||||
{
|
|
||||||
Id = field.Id,
|
|
||||||
DataUrl = _sig.DataUrl,
|
|
||||||
FullName = _sig.FullName,
|
|
||||||
Position = string.IsNullOrWhiteSpace(_sig.Position) ? null : _sig.Position,
|
|
||||||
Place = _sig.Place
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
await SignatureService.SubmitAsync(EnvelopeKey, payload);
|
|
||||||
|
|
||||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
|
||||||
Navigation.NavigateTo("/", forceLoad: true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Failed to finalize signature for {EnvelopeKey}", EnvelopeKey);
|
|
||||||
_submitErrorMessage = "Die Unterschrift konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.";
|
|
||||||
_isLoggingOut = false;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class EnvelopeController : ControllerBase
|
public class EnvelopeController : ControllerBase
|
||||||
{
|
{
|
||||||
|
public record DeleteEnvelopeRequest(string Reason);
|
||||||
|
|
||||||
private readonly ILogger<EnvelopeController> _logger;
|
private readonly ILogger<EnvelopeController> _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
@@ -108,4 +110,17 @@ public class EnvelopeController : ControllerBase
|
|||||||
else
|
else
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = id,
|
||||||
|
Reason = request?.Reason ?? string.Empty
|
||||||
|
}.WithAuth(User.GetId()));
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
using EnvelopeGenerator.Server.Extensions;
|
using EnvelopeGenerator.Server.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
|
||||||
using EnvelopeGenerator.Application.Documents.Queries;
|
using EnvelopeGenerator.Application.Documents.Queries;
|
||||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
||||||
using EnvelopeGenerator.Application.Histories.Queries;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -21,15 +18,13 @@ namespace EnvelopeGenerator.Server.Controllers;
|
|||||||
public class SignatureController : ControllerBase
|
public class SignatureController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ILogger<SignatureController> _logger;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
/// Initializes a new instance of <see cref="SignatureController"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SignatureController(IMediator mediator, ILogger<SignatureController> logger)
|
public SignatureController(IMediator mediator)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: update to use signature query
|
//TODO: update to use signature query
|
||||||
@@ -59,46 +54,4 @@ public class SignatureController : ControllerBase
|
|||||||
else
|
else
|
||||||
return Ok(rcvSignatures);
|
return Ok(rcvSignatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finalizes the receiver signing flow and persists signature data to TBSIG_DOCUMENT_RECEIVER_ELEMENT.
|
|
||||||
/// Also triggers status/history/mail side effects through SigningCommand pipeline.
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpPost("submit/{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> Submit(
|
|
||||||
[FromRoute] string envelopeKey,
|
|
||||||
[FromBody] IEnumerable<Signature>? signatures,
|
|
||||||
CancellationToken cancel)
|
|
||||||
{
|
|
||||||
if (signatures is null)
|
|
||||||
return BadRequest("Signature payload is required.");
|
|
||||||
|
|
||||||
var uuid = User.EnvelopeUuid();
|
|
||||||
var receiverSignature = User.ReceiverSignature();
|
|
||||||
|
|
||||||
if (await _mediator.IsSignedAsync(uuid, receiverSignature, cancel))
|
|
||||||
return Problem(statusCode: StatusCodes.Status409Conflict);
|
|
||||||
|
|
||||||
if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel))
|
|
||||||
return Problem(statusCode: StatusCodes.Status423Locked);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _mediator.Send(new SigningCommand
|
|
||||||
{
|
|
||||||
Envelope = new() { Uuid = uuid },
|
|
||||||
Receiver = new() { Signature = receiverSignature },
|
|
||||||
ReceiverAppType = ReceiverAppType.ReceiverUI,
|
|
||||||
Signatures = signatures.ToList()
|
|
||||||
}, cancel);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to submit signatures for envelope {EnvelopeKey}", envelopeKey);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ public class ExceptionHandlingMiddleware
|
|||||||
message = notFoundEx.Message;
|
message = notFoundEx.Message;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ForbiddenException forbiddenEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||||
|
message = forbiddenEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.LogError(exception, "Unhandled exception occurred.");
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"applicationUrl": "https://localhost:8089;http://localhost:5132",
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -185,9 +185,7 @@
|
|||||||
"TimeLimit": "00:30:00"
|
"TimeLimit": "00:30:00"
|
||||||
},
|
},
|
||||||
"DbTriggerParams": {
|
"DbTriggerParams": {
|
||||||
"DocReceiverElement": [ "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_INS", "TBSIG_DOCUMENT_RECEIVER_ELEMENT_AFT_UPD" ],
|
|
||||||
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
"Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
"EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
|
||||||
"History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
"History": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ],
|
||||||
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
"EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ],
|
||||||
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
"EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ],
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Tests.Application;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class DeleteEnvelopeCommandTests : TestBase
|
||||||
|
{
|
||||||
|
protected override void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task DeleteEnvelope_DraftEnvelope_ShouldSetDeleted_CreateHistory_AndQueueDeletionEmail()
|
||||||
|
{
|
||||||
|
CancellationToken cancel = default;
|
||||||
|
var reason = "Invalid receiver data";
|
||||||
|
|
||||||
|
var envelope = this.CreateEnvelope(User.Id);
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeCreated;
|
||||||
|
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
|
||||||
|
|
||||||
|
var createReceiverCmd = this.CreateReceiverCommand();
|
||||||
|
(var receiver, _) = await Mediator.Send(createReceiverCmd, cancel);
|
||||||
|
|
||||||
|
var envReceiver = this.CreateEnvelopeReceiver(envelope.Id, receiver.Id);
|
||||||
|
await GetRepository<EnvelopeReceiver>().CreateAsync(envReceiver, cancel);
|
||||||
|
|
||||||
|
var result = await Mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
Reason = reason
|
||||||
|
}.WithAuth(User.Id), cancel);
|
||||||
|
|
||||||
|
Assert.That(result.EnvelopeId, Is.EqualTo(envelope.Id));
|
||||||
|
Assert.That(result.AppliedStatus, Is.EqualTo(EnvelopeStatus.EnvelopeDeleted));
|
||||||
|
Assert.That(result.IsNoOp, Is.False);
|
||||||
|
|
||||||
|
var histories = await GetRepository<History>().Query
|
||||||
|
.Where(h => h.EnvelopeId == envelope.Id && h.Status == EnvelopeStatus.EnvelopeDeleted)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
Assert.That(histories, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(histories[0].Comment, Is.EqualTo(reason));
|
||||||
|
|
||||||
|
var emails = await GetRepository<EmailOut>().Query
|
||||||
|
.Where(e => e.WfId == (int)EnvelopeStatus.MessageDeletionSent && e.ReferenceId == receiver.Id)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
Assert.That(emails, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(emails[0].EmailAddress, Is.EqualTo(receiver.EmailAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task DeleteEnvelope_SentEnvelope_ShouldSetWithdrawn()
|
||||||
|
{
|
||||||
|
CancellationToken cancel = default;
|
||||||
|
|
||||||
|
var envelope = this.CreateEnvelope(User.Id);
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
||||||
|
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
|
||||||
|
|
||||||
|
var result = await Mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
Reason = "Sender requested cancellation"
|
||||||
|
}.WithAuth(User.Id), cancel);
|
||||||
|
|
||||||
|
Assert.That(result.AppliedStatus, Is.EqualTo(EnvelopeStatus.EnvelopeWithdrawn));
|
||||||
|
|
||||||
|
var histories = await GetRepository<History>().Query
|
||||||
|
.Where(h => h.EnvelopeId == envelope.Id && h.Status == EnvelopeStatus.EnvelopeWithdrawn)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
Assert.That(histories, Has.Count.EqualTo(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeleteEnvelope_MissingReason_ShouldThrowBadRequest()
|
||||||
|
{
|
||||||
|
Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||||
|
await Mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = 123,
|
||||||
|
Reason = " "
|
||||||
|
}.WithAuth(User.Id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task DeleteEnvelope_WrongOwner_ShouldThrowForbidden()
|
||||||
|
{
|
||||||
|
CancellationToken cancel = default;
|
||||||
|
|
||||||
|
var envelope = this.CreateEnvelope(User.Id);
|
||||||
|
envelope = await GetRepository<Envelope>().CreateAsync(envelope, cancel);
|
||||||
|
|
||||||
|
var otherUserCmd = this.CreateUserCommand();
|
||||||
|
var otherUser = await GetRepository<DigitalData.UserManager.Domain.Entities.User>().CreateAsync(otherUserCmd, cancel);
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<ForbiddenException>(async () =>
|
||||||
|
await Mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
Reason = "Not allowed"
|
||||||
|
}.WithAuth(otherUser.Id), cancel));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ public class Fake
|
|||||||
|
|
||||||
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
services.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
||||||
{
|
{
|
||||||
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase("EnvelopeGeneratorTestDb"));
|
opt.AddDbContext(dbCtxOpt => dbCtxOpt.UseInMemoryDatabase($"EnvelopeGeneratorTestDb_{Guid.NewGuid():N}"));
|
||||||
|
|
||||||
opt.AddDbTriggerParams(context.Configuration);
|
opt.AddDbTriggerParams(context.Configuration);
|
||||||
|
|
||||||
@@ -308,4 +308,4 @@ public static class Extensions
|
|||||||
.Select(_ => fake.CreateUserCommand())
|
.Select(_ => fake.CreateUserCommand())
|
||||||
.ToList();
|
.ToList();
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ public abstract class TestBase : Faker
|
|||||||
|
|
||||||
// Add seed email templates
|
// Add seed email templates
|
||||||
foreach (var temp in SeedEmailTemplates)
|
foreach (var temp in SeedEmailTemplates)
|
||||||
|
{
|
||||||
|
temp.LangCode ??= "de";
|
||||||
await repo.CreateAsync(temp);
|
await repo.CreateAsync(temp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[TearDown]
|
[TearDown]
|
||||||
@@ -140,4 +143,4 @@ public abstract class TestBase : Faker
|
|||||||
ChangedWhen = DateTime.Parse("2025-05-12 10:43:44.290")
|
ChangedWhen = DateTime.Parse("2025-05-12 10:43:44.290")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.PdfEditor
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Tests", "EnvelopeGenerator.Tests\EnvelopeGenerator.Tests.csproj", "{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.API", "EnvelopeGenerator.API\EnvelopeGenerator.API.csproj", "{EC768913-6270-14F4-1DD3-69C87A659462}"
|
||||||
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.DependencyInjection", "EnvelopeGenerator.DependencyInjection\EnvelopeGenerator.DependencyInjection.csproj", "{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.DependencyInjection", "EnvelopeGenerator.DependencyInjection\EnvelopeGenerator.DependencyInjection.csproj", "{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
|
||||||
@@ -93,6 +95,10 @@ Global
|
|||||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8}.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
|
||||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -130,6 +136,7 @@ Global
|
|||||||
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {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}
|
{211619F5-AE25-4BA5-A552-BACAFE0632D3} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
|
||||||
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
|
{224C4845-1CDE-22B7-F3A9-1FF9297F70E8} = {0CBC2432-A561-4440-89BC-671B66A24146}
|
||||||
|
{EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||||
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
{5DCCF9A1-C03F-90E6-87D3-E96DB25250C2} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||||
{BF1700D5-592E-4FFA-84E8-5480E289A1F0} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
{BF1700D5-592E-4FFA-84E8-5480E289A1F0} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
|
||||||
{4E6C54DA-576D-0955-2564-9EC890BB8279} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0}
|
{4E6C54DA-576D-0955-2564-9EC890BB8279} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0}
|
||||||
|
|||||||
Reference in New Issue
Block a user