From e11bc9df8ea9ef92dcaae85f9f3310a4c4bbc1ec Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 22 Jun 2026 14:57:26 +0200 Subject: [PATCH] Add new controllers for envelope management Introduced multiple controllers to enhance application functionality: - `AnnotationController`: Manages annotations and signature lifecycle. - `AuthController`: Handles user authentication and session management. - `CacheController`: Manages cached data for receivers. - `ConfigController`: Exposes client configuration data. - `DocumentController`: Provides access to envelope documents. - `EmailTemplateController`: Manages email templates. - `EnvelopeController`: Manages envelope operations. - `EnvelopeReceiverController`: Handles envelope receiver data. - `EnvelopeTypeController`: Retrieves envelope types. - `HistoryController`: Accesses envelope history. - `IAuthController`: Defines authentication interface. - `LocalizationController`: Manages localization settings. - `ReadOnlyController`: Manages read-only envelope sharing. - `ReceiverController`: Retrieves receiver data. - `SignatureController`: Retrieves document signatures. - `TfaRegistrationController`: Manages two-factor authentication. These changes improve maintainability and scalability by organizing operations into dedicated controllers. --- .../Controllers/AnnotationController.cs | 132 +++++++++ .../Controllers/AuthController.cs | 117 ++++++++ .../Controllers/CacheController.cs | 84 ++++++ .../Controllers/ConfigController.cs | 30 ++ .../Controllers/DocumentController.cs | 84 ++++++ .../Controllers/EmailTemplateController.cs | 69 +++++ .../Controllers/EnvelopeController.cs | 111 +++++++ .../Controllers/EnvelopeReceiverController.cs | 275 ++++++++++++++++++ .../Controllers/EnvelopeTypeController.cs | 39 +++ .../Controllers/HistoryController.cs | 118 ++++++++ .../Controllers/Interfaces/IAuthController.cs | 38 +++ .../Controllers/LocalizationController.cs | 121 ++++++++ .../Controllers/ReadOnlyController.cs | 91 ++++++ .../Controllers/ReceiverController.cs | 47 +++ .../Controllers/SignatureController.cs | 57 ++++ .../Controllers/TfaRegistrationController.cs | 129 ++++++++ 16 files changed, 1542 insertions(+) create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AnnotationController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/AuthController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/CacheController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ConfigController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/DocumentController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EmailTemplateController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeReceiverController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/EnvelopeTypeController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/HistoryController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/Interfaces/IAuthController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/LocalizationController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReadOnlyController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/ReceiverController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/SignatureController.cs create mode 100644 EnvelopeGenerator.WebUI/EnvelopeGenerator.WebUI/Controllers/TfaRegistrationController.cs 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() }); + } + } +}