diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AnnotationController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AnnotationController.cs
new file mode 100644
index 00000000..e0698b84
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AnnotationController.cs
@@ -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;
+
+///
+/// Manages annotations and signature lifecycle for envelopes.
+///
+[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 _logger;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ [Obsolete("Use MediatR")]
+ public AnnotationController(
+ ILogger logger,
+ IEnvelopeHistoryService envelopeHistoryService,
+ IEnvelopeReceiverService envelopeReceiverService,
+ IMediator mediator)
+ {
+ _historyService = envelopeHistoryService;
+ _envelopeReceiverService = envelopeReceiverService;
+ _mediator = mediator;
+ _logger = logger;
+ }
+
+ ///
+ /// Creates or updates annotations for the authenticated envelope receiver.
+ ///
+ /// Annotation payload.
+ /// Cancellation token.
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpPost]
+ [Obsolete("PSPDF Kit will no longer be used.")]
+ public async Task 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();
+ }
+
+ ///
+ /// Rejects the document for the current receiver.
+ ///
+ /// Optional rejection reason.
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpPost("reject")]
+ [Obsolete("Use MediatR")]
+ public async Task 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);
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AuthController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AuthController.cs
new file mode 100644
index 00000000..6a3500c9
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AuthController.cs
@@ -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;
+
+///
+/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus.
+///
+[Route("api/[controller]")]
+[ApiController]
+public partial class AuthController(IOptions authTokenKeyOptions, IAuthorizationService authService) : ControllerBase, IAuthController
+{
+ private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value;
+
+ ///
+ ///
+ ///
+ public IAuthorizationService AuthService { get; } = authService;
+
+ ///
+ /// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie)
+ ///
+ ///
+ /// Gibt eine HTTP 200 oder 401.
+ ///
+ ///
+ /// Sample request:
+ ///
+ /// POST /api/auth/logout
+ ///
+ ///
+ /// Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat.
+ /// Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.
+ [ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
+ [Authorize(Policy = AuthPolicy.SenderOrReceiver)]
+ [HttpPost("logout")]
+ public async Task 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();
+ }
+
+ ///
+ /// Prüft, ob der Benutzer ein autorisiertes Token hat.
+ ///
+ /// Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401
+ ///
+ /// Sample request:
+ ///
+ /// GET /api/auth
+ ///
+ ///
+ /// Wenn es einen autorisierten Cookie gibt.
+ /// Wenn kein Cookie vorhanden ist oder nicht autorisierte.
+ [ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
+ [HttpGet("check")]
+ [Authorize]
+ public IActionResult Check(string? role = null)
+ => role is not null && !User.IsInRole(role)
+ ? Unauthorized()
+ : Ok();
+
+ ///
+ /// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key.
+ /// The request must carry a cookie named AuthTokenSignFLOWReceiver.{envelopeKey}.
+ ///
+ /// The unique envelope key extracted from the route.
+ /// Valid per-envelope token found.
+ /// Token is missing, expired or invalid.
+ [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();
+
+ ///
+ /// Removes the per-envelope receiver cookie for the given envelope key.
+ ///
+ /// The unique envelope key whose cookie should be deleted.
+ /// Cookie successfully deleted.
+ [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();
+ }
+
+ ///
+ /// Removes all per-envelope receiver cookies from the current request.
+ ///
+ /// All envelope receiver cookies successfully deleted.
+ [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();
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/CacheController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/CacheController.cs
new file mode 100644
index 00000000..8379d4eb
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/CacheController.cs
@@ -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;
+
+///
+/// Manages cached data for receivers using distributed cache.
+///
+[ApiController]
+[Route("api/[controller]")]
+[Authorize(Policy = AuthPolicy.Receiver)]
+public class CacheController(
+ IDistributedCache cache,
+ IOptions cacheOptions) : ControllerBase
+{
+ private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
+
+ ///
+ /// Stores a receiver's signature in cache for the specified envelope.
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpPost("SignatureCapture/{envelopeKey}")]
+ public async Task 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();
+ }
+
+ ///
+ /// Retrieves a cached signature for the specified envelope.
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpGet("SignatureCapture/{envelopeKey}")]
+ public async Task 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(json);
+ return Ok(signature);
+ }
+
+ ///
+ /// Deletes a cached signature for the specified envelope.
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpDelete("SignatureCapture/{envelopeKey}")]
+ public async Task DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel)
+ {
+ var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}";
+ await cache.RemoveAsync(cacheKey, cancel);
+
+ return Ok();
+ }
+}
+
+///
+/// Request model for caching signature data.
+///
+public sealed record SignatureCacheRequest(
+ string DataUrl,
+ string FullName,
+ string Place,
+ string? Position = null);
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ConfigController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ConfigController.cs
new file mode 100644
index 00000000..81aa23c0
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ConfigController.cs
@@ -0,0 +1,30 @@
+using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+
+namespace EnvelopeGenerator.API.Controllers;
+
+///
+/// Exposes configuration data required by the client applications.
+///
+///
+/// Initializes a new instance of .
+///
+[Route("api/[controller]")]
+[ApiController]
+[Authorize]
+public class ConfigController(IOptionsMonitor annotationParamsOptions) : ControllerBase
+{
+ private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
+
+ ///
+ /// Returns annotation configuration that was previously rendered by MVC.
+ ///
+ [HttpGet("Annotations")]
+ [Obsolete("PSPDF Kit will no longer be used.")]
+ public IActionResult GetAnnotationParams()
+ {
+ return Ok(_annotationParams.AnnotationJSObject);
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/DocumentController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/DocumentController.cs
new file mode 100644
index 00000000..f8468f81
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/DocumentController.cs
@@ -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;
+
+///
+/// Provides access to envelope documents for authenticated receivers.
+///
+///
+/// Initializes a new instance of the class.
+///
+[ApiController]
+[Route("api/[controller]")]
+public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger logger) : ControllerBase, IAuthController
+{
+ ///
+ ///
+ ///
+ public IAuthorizationService AuthService => authService;
+
+ ///
+ /// Returns the document bytes receiver.
+ ///
+ /// Encoded envelope key.
+ /// Cancellation token.
+ [HttpGet]
+ [Authorize(Policy = AuthPolicy.SenderOrReceiver)]
+ public async Task 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();
+ }
+
+ ///
+ /// Gets the document for the specified envelope key.
+ ///
+ ///
+ ///
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpGet("{envelopeKey}")]
+ public async Task 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");
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EmailTemplateController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EmailTemplateController.cs
new file mode 100644
index 00000000..be670e42
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EmailTemplateController.cs
@@ -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;
+
+///
+/// Controller for managing temp templates.
+/// Steuerung zur Verwaltung von E-Mail-Vorlagen.
+///
+///
+/// Initialisiert eine neue Instanz der -Klasse.
+///
+///
+/// Die Mediator-Instanz, die zum Senden von Befehlen und Abfragen verwendet wird.
+///
+[Route("api/[controller]")]
+[ApiController]
+[Authorize(Policy = AuthPolicy.Sender)]
+public class EmailTemplateController(IMediator mediator) : ControllerBase
+{
+ ///
+ /// Ruft E-Mail-Vorlagen basierend auf der angegebenen Abfrage ab.
+ /// Gibt alles zurück, wenn keine Id- oder Typ-Informationen eingegeben wurden.
+ ///
+ /// Die Abfrageparameter zum Abrufen von E-Mail-Vorlagen.
+ ///
+ /// Gibt HTTP-Antwort zurück
+ ///
+ /// Sample request:
+ /// GET /api/EmailTemplate?emailTemplateId=123
+ ///
+ /// Wenn die E-Mail-Vorlagen erfolgreich abgerufen werden.
+ /// Wenn die Abfrageparameter ungültig sind.
+ /// Wenn der Benutzer nicht authentifiziert ist.
+ /// Wenn die gesuchte Abfrage nicht gefunden wird.
+ [HttpGet]
+ public async Task Get([FromQuery] ReadEmailTemplateQuery emailTemplate, CancellationToken cancel)
+ {
+ var result = await mediator.Send(emailTemplate, cancel);
+ return Ok(result);
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ ///
+ /// Wenn die E-Mail-Vorlage erfolgreich aktualisiert oder zurückgesetzt wird.
+ /// Wenn die Abfrage ohne einen String gesendet wird.
+ /// Wenn der Benutzer nicht authentifiziert ist.
+ /// Wenn die gesuchte Abfrage nicht gefunden wird.
+ [HttpPut]
+ public async Task Update([FromBody] UpdateEmailTemplateCommand update, CancellationToken cancel)
+ {
+ await mediator.Send(update, cancel);
+ return Ok();
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeController.cs
new file mode 100644
index 00000000..c1bb54d2
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeController.cs
@@ -0,0 +1,111 @@
+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;
+
+///
+/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit.
+///
+///
+/// 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.
+///
+[Route("api/[controller]")]
+[ApiController]
+[Authorize]
+public class EnvelopeController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly IMediator _mediator;
+
+ ///
+ /// Erstellt eine neue Instanz des EnvelopeControllers.
+ ///
+ /// Der Logger, der für das Protokollieren von Informationen verwendet wird.
+ ///
+ public EnvelopeController(ILogger logger, IMediator mediator)
+ {
+ _logger = logger;
+ _mediator = mediator;
+ }
+
+ ///
+ /// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab.
+ ///
+ ///
+ /// Eine IActionResult-Instanz, die die abgerufenen Umschläge oder einen Fehlerstatus enthält.
+ /// Die Anfrage war erfolgreich, und die Umschläge werden zurückgegeben.
+ /// Die Anfrage war fehlerhaft oder unvollständig.
+ /// Der Benutzer ist nicht authentifiziert.
+ /// Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.
+ /// Ein unerwarteter Fehler ist aufgetreten.
+ [Authorize(AuthenticationSchemes = AuthScheme.Sender)]
+ [HttpGet]
+ public async Task GetAsync([FromQuery] ReadEnvelopeQuery envelope)
+ {
+ var result = await _mediator.Send(envelope.Authorize(User.GetId()));
+ return result.Any() ? Ok(result) : NotFound();
+ }
+
+ ///
+ /// Ruft das Ergebnis eines Dokuments basierend auf der ID ab.
+ ///
+ ///
+ /// Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false).
+ /// Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält.
+ /// Das Dokument wurde erfolgreich abgerufen.
+ /// Das Dokument wurde nicht gefunden oder ist nicht verfügbar.
+ /// Ein unerwarteter Fehler ist aufgetreten.
+ [HttpGet("doc-result")]
+ public async Task GetDocResultAsync([FromQuery] 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");
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ [NonAction]
+ [Authorize]
+ [HttpPost]
+ public async Task 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);
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeReceiverController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeReceiverController.cs
new file mode 100644
index 00000000..80472540
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeReceiverController.cs
@@ -0,0 +1,275 @@
+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;
+
+///
+/// Controller für die Verwaltung von Umschlagempfängern.
+///
+///
+/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten.
+///
+[Route("api/[controller]")]
+[Authorize]
+[ApiController]
+public class EnvelopeReceiverController : ControllerBase
+{
+ private readonly ILogger _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;
+
+ ///
+ /// Konstruktor für den EnvelopeReceiverController.
+ ///
+ public EnvelopeReceiverController(ILogger logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions csOpt)
+ {
+ _logger = logger;
+ _mediator = mediator;
+ _mapper = mapper;
+ _envelopeExecutor = envelopeExecutor;
+ _erExecutor = erExecutor;
+ _documentExecutor = documentExecutor;
+ _cnnStr = csOpt.Value.Value;
+ }
+
+ ///
+ /// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab.
+ ///
+ /// Die Abfrageparameter für die Filterung von Umschlagempfängern.
+ /// Eine HTTP-Antwort mit der Liste der gefundenen Umschlagempfänger oder einem Fehlerstatus.
+ ///
+ /// 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.
+ ///
+ /// Die Liste der Umschlagempfänger wurde erfolgreich abgerufen.
+ /// Wenn kein autorisierter Token vorhanden ist
+ /// Ein unerwarteter Fehler ist aufgetreten.
+ [Authorize]
+ [HttpGet]
+ public async Task GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver)
+ {
+ envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() };
+
+ var result = await _mediator.Send(envelopeReceiver);
+
+ return Ok(result);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpGet("{envelopeKey}")]
+ public async Task GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel)
+ {
+ var er = await _mediator.Send(new ReadEnvelopeReceiverQuery()
+ {
+ Key = envelopeKey
+ }, cancel);
+
+ return Ok(er.SingleOrDefault());
+ }
+
+ ///
+ /// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab.
+ ///
+ /// Abfrage, bei der nur eine der Angaben ID, Signatur oder E-Mail-Adresse des Empfängers eingegeben werden muss.
+ /// Eine HTTP-Antwort mit dem Namen des Empfängers oder einem Fehlerstatus.
+ ///
+ /// Dieser Endpunkt ermöglicht es, den Namen des zuletzt verwendeten Empfängers basierend auf der E-Mail-Adresse abzurufen.
+ ///
+ /// Der Name des Empfängers wurde erfolgreich abgerufen.
+ /// Wenn kein autorisierter Token vorhanden ist
+ /// Kein Empfänger gefunden.
+ /// Ein unerwarteter Fehler ist aufgetreten.
+ [Authorize]
+ [HttpGet("salute")]
+ public async Task GetReceiverName([FromQuery] ReadReceiverNameQuery receiver)
+ {
+ var name = await _mediator.Send(receiver);
+ return name is null ? NotFound() : Ok(name);
+ }
+
+ ///
+ /// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften.
+ ///
+ ///
+ ///
+ /// HTTP-Antwort
+ ///
+ /// 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
+ /// }
+ ///
+ ///
+ /// Envelope-Erstellung und Sendeprozessbefehl erfolgreich
+ /// Wenn ein Fehler im HTTP-Body auftritt
+ /// Wenn kein autorisierter Token vorhanden ist
+ /// Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.
+ [Authorize]
+ [HttpPost]
+ public async Task 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 sentReceivers = new();
+ List unsentReceivers = new();
+
+ foreach (var receiver in request.Receivers)
+ {
+ var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel);
+
+ if (envelopeReceiver is null)
+ unsentReceivers.Add(receiver);
+ else
+ sentReceivers.Add(envelopeReceiver);
+ }
+
+ var res = _mapper.Map(envelope);
+ res.UnsentReceivers = unsentReceivers;
+ res.SentReceiver = _mapper.Map>(sentReceivers.Select(er => er.Receiver));
+ #endregion
+
+ #region Add document
+ var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel);
+
+ if (document is null)
+ return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed.");
+ #endregion
+
+ #region Add document element
+ // @DOC_ID, @RECEIVER_ID, @POSITION_X, @POSITION_Y, @PAGE
+ string sql = @"
+ DECLARE @OUT_SUCCESS bit;
+
+ EXEC [dbo].[PRSIG_API_ADD_DOC_RECEIVER_ELEM]
+ {0},
+ {1},
+ {2},
+ {3},
+ {4},
+ @OUT_SUCCESS OUTPUT;
+
+ SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
+
+ foreach (var rcv in res.SentReceiver)
+ foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty())
+ {
+ using SqlConnection conn = new(_cnnStr);
+ conn.Open();
+
+ var formattedSQL = string.Format(sql, document.Id.ToSqlParam(), rcv.Id.ToSqlParam(), sign.X.ToSqlParam(), sign.Y.ToSqlParam(), sign.Page.ToSqlParam());
+
+ using SqlCommand cmd = new(formattedSQL, conn);
+ cmd.CommandType = CommandType.Text;
+
+ using SqlDataReader reader = cmd.ExecuteReader();
+ if (reader.Read())
+ {
+ bool outSuccess = reader.GetBoolean(0);
+ }
+ }
+ #endregion
+
+ #region Create history
+ // ENV_UID, STATUS_ID, USER_ID,
+ string sql_hist = @"
+ USE [DD_ECM]
+
+ DECLARE @OUT_SUCCESS bit;
+
+ EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
+ {0},
+ {1},
+ {2},
+ @OUT_SUCCESS OUTPUT;
+
+ SELECT @OUT_SUCCESS as [@OUT_SUCCESS];";
+
+ using (SqlConnection conn = new(_cnnStr))
+ {
+ conn.Open();
+ var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), 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);
+ }
+ }
+ #endregion
+
+ return Ok(res);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static bool IsBase64String(string input)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ return false;
+
+ try
+ {
+ Convert.FromBase64String(input);
+ return true;
+ }
+ catch (FormatException)
+ {
+ return false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeTypeController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeTypeController.cs
new file mode 100644
index 00000000..e6fc6095
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeTypeController.cs
@@ -0,0 +1,39 @@
+using MediatR;
+using EnvelopeGenerator.Application.EnvelopeTypes.Queries;
+using Microsoft.AspNetCore.Mvc;
+
+namespace EnvelopeGenerator.GeneratorAPI.Controllers;
+
+///
+///
+///
+[ApiExplorerSettings(IgnoreApi = true)]
+[Route("api/[controller]")]
+[ApiController]
+public class EnvelopeTypeController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly IMediator _mediator;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public EnvelopeTypeController(ILogger logger, IMediator mediator)
+ {
+ _logger = logger;
+ _mediator = mediator;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ [HttpGet]
+ public async Task GetAllAsync()
+ {
+ var result = await _mediator.Send(new ReadEnvelopeTypesQuery());
+ return Ok(result);
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/HistoryController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/HistoryController.cs
new file mode 100644
index 00000000..16418fa3
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/HistoryController.cs
@@ -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;
+
+///
+/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit.
+///
+[Route("api/[controller]")]
+[ApiController]
+[Authorize]
+public class HistoryController : ControllerBase
+{
+ private readonly IMemoryCache _memoryCache;
+
+ private readonly IMediator _mediator;
+
+ ///
+ /// Konstruktor für den HistoryController.
+ ///
+ ///
+ ///
+ public HistoryController(IMemoryCache memoryCache, IMediator mediator)
+ {
+ _memoryCache = memoryCache;
+ _mediator = mediator;
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ [HttpGet("related")]
+ [Authorize]
+ public IActionResult GetReferenceTypes(ReferenceType? referenceType = null)
+ {
+ return referenceType is null
+ ? Ok(_memoryCache.GetEnumAsDictionary("gen.api", ReferenceType.Unknown))
+ : Ok(referenceType.ToString());
+ }
+
+ ///
+ /// 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
+ ///
+ ///
+ /// 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.
+ ///
+ /// Gibt die HTTP-Antwort zurück.
+ ///
+ [HttpGet("status")]
+ [Authorize]
+ public IActionResult GetEnvelopeStatus([FromQuery] EnvelopeStatus? status = null)
+ {
+ return status is null
+ ? Ok(_memoryCache.GetEnumAsDictionary("gen.api", Status.NonHist, Status.RelatedToFormApp))
+ : Ok(status.ToString());
+ }
+
+ ///
+ /// Ruft die gesamte Umschlaghistorie basierend auf den angegebenen Abfrageparametern ab.
+ ///
+ /// Die Abfrageparameter, die die Filterkriterien für die Umschlaghistorie definieren.
+ ///
+ /// Eine Liste von Historieneinträgen, die den angegebenen Kriterien entsprechen, oder nur der letzte Eintrag.
+ /// Die Anfrage war erfolgreich, und die Umschlaghistorie wird zurückgegeben.
+ /// Die Anfrage war ungültig oder unvollständig.
+ /// Der Benutzer ist nicht authentifiziert.
+ /// Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen.
+ /// Ein unerwarteter Fehler ist aufgetreten.
+ [HttpGet]
+ [Authorize]
+ public async Task 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);
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/Interfaces/IAuthController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/Interfaces/IAuthController.cs
new file mode 100644
index 00000000..cf31d972
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/Interfaces/IAuthController.cs
@@ -0,0 +1,38 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authorization;
+
+namespace EnvelopeGenerator.API.Controllers.Interfaces;
+
+///
+///
+///
+public interface IAuthController
+{
+ ///
+ ///
+ ///
+ IAuthorizationService AuthService { get; }
+
+ ///
+ ///
+ ///
+ ClaimsPrincipal User { get; }
+}
+
+///
+///
+///
+public static class AuthControllerExtensions
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async Task IsUserInPolicyAsync(this IAuthController controller, string policyName)
+ {
+ var result = await controller.AuthService.AuthorizeAsync(controller.User, policyName);
+ return result.Succeeded;
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/LocalizationController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/LocalizationController.cs
new file mode 100644
index 00000000..e2e02de2
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/LocalizationController.cs
@@ -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;
+
+///
+/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen.
+///
+[ApiExplorerSettings(IgnoreApi = true)]
+[Route("api/[controller]")]
+[ApiController]
+public class LocalizationController : ControllerBase
+{
+ private static readonly Guid L_KEY = Guid.NewGuid();
+
+ private readonly ILogger _logger;
+ private readonly IStringLocalizer _mLocalizer;
+ private readonly IStringLocalizer _localizer;
+ private readonly IMemoryCache _cache;
+
+ ///
+ /// Konstruktor für den .
+ ///
+ /// Logger für die Protokollierung.
+ /// Lokalisierungsdienst für Ressourcen.
+ /// Speicher-Cache für die Zwischenspeicherung von Daten.
+ /// Lokalisierungsdienst für Modelle.
+ public LocalizationController(
+ ILogger logger,
+ IStringLocalizer localizer,
+ IMemoryCache memoryCache,
+ IStringLocalizer _modelLocalizer)
+ {
+ _logger = logger;
+ _localizer = localizer;
+ _cache = memoryCache;
+ _mLocalizer = _modelLocalizer;
+ }
+
+ ///
+ /// Ruft alle lokalisierten Daten ab.
+ ///
+ /// Eine Liste aller lokalisierten Daten.
+ [HttpGet]
+ public IActionResult GetAll() => Ok(_cache.GetOrCreate(Language ?? string.Empty + L_KEY, _ => _mLocalizer.ToDictionary()));
+
+ ///
+ /// Ruft die aktuelle Sprache ab.
+ ///
+ /// Die aktuelle Sprache oder ein NotFound-Ergebnis, wenn keine Sprache gesetzt ist.
+ [HttpGet("lang")]
+ public IActionResult GetLanguage() => Language is null ? NotFound() : Ok(Language);
+
+ ///
+ /// Setzt die Sprache.
+ ///
+ /// Die zu setzende Sprache.
+ /// Ein Ok-Ergebnis, wenn die Sprache erfolgreich gesetzt wurde, oder ein BadRequest-Ergebnis, wenn die Eingabe ungültig ist.
+ [HttpPost("lang")]
+ public IActionResult SetLanguage([FromQuery] string language)
+ {
+ if (string.IsNullOrEmpty(language))
+ return BadRequest();
+
+ Language = language;
+ return Ok();
+ }
+
+ ///
+ /// Löscht die aktuelle Sprache.
+ ///
+ /// Ein Ok-Ergebnis, wenn die Sprache erfolgreich gelöscht wurde.
+ [HttpDelete("lang")]
+ public IActionResult DeleteLanguage()
+ {
+ Language = null;
+ return Ok();
+ }
+
+ ///
+ /// Eigenschaft für die Verwaltung der aktuellen Sprache über Cookies.
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReadOnlyController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReadOnlyController.cs
new file mode 100644
index 00000000..db800c22
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReadOnlyController.cs
@@ -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;
+
+///
+/// Manages read-only envelope sharing flows.
+///
+[Route("api/[controller]")]
+[ApiController]
+public class ReadOnlyController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly IEnvelopeReceiverReadOnlyService _readOnlyService;
+ private readonly IEnvelopeMailService _mailService;
+ private readonly IEnvelopeHistoryService _historyService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ReadOnlyController(ILogger logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService)
+ {
+ _logger = logger;
+ _readOnlyService = readOnlyService;
+ _mailService = mailService;
+ _historyService = historyService;
+ }
+
+ ///
+ /// Creates a new read-only receiver for the current envelope.
+ ///
+ /// Creation payload.
+ [HttpPost]
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [Obsolete("Use MediatR")]
+ public async Task 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(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);
+ });
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReceiverController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReceiverController.cs
new file mode 100644
index 00000000..756b5eb3
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReceiverController.cs
@@ -0,0 +1,47 @@
+using MediatR;
+using EnvelopeGenerator.Application.Receivers.Queries;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace EnvelopeGenerator.GeneratorAPI.Controllers;
+
+///
+/// Controller für die Verwaltung von Empfängern.
+///
+///
+/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur.
+///
+[Route("api/[controller]")]
+[ApiController]
+[Authorize]
+public class ReceiverController : ControllerBase
+{
+ private readonly IMediator _mediator;
+
+ ///
+ /// Initialisiert eine neue Instanz des -Controllers.
+ ///
+ /// Mediator für Anfragen.
+ public ReceiverController(IMediator mediator)
+ {
+ _mediator = mediator;
+ }
+
+ ///
+ /// Ruft eine Liste von Empfängern ab, basierend auf den angegebenen Abfrageparametern.
+ ///
+ /// Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.
+ /// Eine Liste von Empfängern oder ein Fehlerstatus.
+ [HttpGet]
+ public async Task 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);
+ }
+}
\ No newline at end of file
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/SignatureController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/SignatureController.cs
new file mode 100644
index 00000000..bd3ff352
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/SignatureController.cs
@@ -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;
+
+///
+///
+///
+[Authorize(Policy = AuthPolicy.Receiver)]
+[ApiController]
+[Route("api/[controller]")]
+public class SignatureController : ControllerBase
+{
+ private readonly IMediator _mediator;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public SignatureController(IMediator mediator)
+ {
+ _mediator = mediator;
+ }
+
+ //TODO: update to use signature query
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpGet("{envelopeKey}")]
+ public async Task 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 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);
+ }
+}
diff --git a/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/TfaRegistrationController.cs b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/TfaRegistrationController.cs
new file mode 100644
index 00000000..f365c0df
--- /dev/null
+++ b/EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/TfaRegistrationController.cs
@@ -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;
+
+///
+/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers.
+///
+[ApiController]
+[Route("api/tfa")]
+public class TfaRegistrationController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly IEnvelopeReceiverService _envelopeReceiverService;
+ private readonly IAuthenticator _authenticator;
+ private readonly IReceiverService _receiverService;
+ private readonly TFARegParams _parameters;
+ private readonly IStringLocalizer _localizer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TfaRegistrationController(
+ ILogger logger,
+ IEnvelopeReceiverService envelopeReceiverService,
+ IAuthenticator authenticator,
+ IReceiverService receiverService,
+ IOptions tfaRegParamsOptions,
+ IStringLocalizer localizer)
+ {
+ _logger = logger;
+ _envelopeReceiverService = envelopeReceiverService;
+ _authenticator = authenticator;
+ _receiverService = receiverService;
+ _parameters = tfaRegParamsOptions.Value;
+ _localizer = localizer;
+ }
+
+ ///
+ /// Generates registration metadata (QR code and deadline) for a receiver.
+ ///
+ /// Encoded envelope receiver id.
+ [Authorize]
+ [HttpGet("{envelopeReceiverId}")]
+ public async Task 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() });
+ }
+ }
+
+ ///
+ /// Logs out the envelope receiver from cookie authentication.
+ ///
+ [Authorize(Policy = AuthPolicy.Receiver)]
+ [HttpPost("auth/logout")]
+ public async Task 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() });
+ }
+ }
+}