using DigitalData.Core.Abstraction.Application.DTO; using DigitalData.Core.Exceptions; 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.EnvelopeReceivers.Queries; using EnvelopeGenerator.Application.Histories.Queries; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.API.Extensions; using EnvelopeGenerator.API.Models; using MediatR; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Dynamic; 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.GetReceiverSignatureOfReceiver(); var uuid = User.GetEnvelopeUuidOfReceiver(); 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 docSignedNotification = await _mediator .ReadEnvelopeReceiverAsync(uuid, signature, cancel) .ToDocSignedNotification(psPdfKitAnnotation) ?? throw new NotFoundException("Envelope receiver is not found."); await _mediator.PublishSafely(docSignedNotification, cancel); 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.GetReceiverSignatureOfReceiver(); var uuid = User.GetEnvelopeUuidOfReceiver(); var mail = User.GetReceiverMailOfReceiver(); 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); } /// /// Returns the signature placeholders (id, page, x, y, width, height) /// the authenticated receiver must sign on the current envelope. /// Used by the Blazor receiver UI to render a custom signature overlay /// on top of the DevExpress PDF viewer. /// [Authorize(Policy = AuthPolicy.Receiver)] [HttpGet("elements")] public async Task GetElements(CancellationToken cancel) { var signature = User.GetReceiverSignatureOfReceiver(); var uuid = User.GetEnvelopeUuidOfReceiver(); var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel) ?? throw new NotFoundException("Envelope receiver is not found."); var elements = envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements ?? Enumerable.Empty(); // Only expose what the overlay needs (no internal columns). var payload = elements .Where(e => e.ReceiverId == envelopeReceiver.ReceiverId) .Select(e => new { id = e.Id, page = e.Page, x = e.X, y = e.Y, width = e.Width, height = e.Height, required = e.Required, tooltip = e.Tooltip, }); return Ok(payload); } /// /// Signs the document for the authenticated receiver using a Blazor / /// DevExpress friendly payload (one PNG image per placeholder, plus /// optional position / city per placeholder). /// /// Internally produces a wrapping a /// list of entries so that the /// existing notification pipeline (mail / status / annotation persistence) /// is fully reused. The Instant JSON is left blank because the Blazor /// pipeline does not depend on PSPDFKit-side rendering. /// [Authorize(Policy = AuthPolicy.Receiver)] [HttpPost("blazor")] public async Task SignBlazor([FromBody] BlazorSignaturePayload payload, CancellationToken cancel) { if (payload is null || payload.Signatures.Count == 0) return BadRequest(); var signature = User.GetReceiverSignatureOfReceiver(); var uuid = User.GetEnvelopeUuidOfReceiver(); var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel) ?? throw new NotFoundException("Envelope receiver is not found."); if (await _mediator.IsSignedAsync(uuid, signature, cancel)) return Problem(statusCode: StatusCodes.Status409Conflict); if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel)) return Problem(statusCode: StatusCodes.Status423Locked); // Build a structured AnnotationCreateDto list out of the lightweight // payload. One DTO per (element, kind) tuple, mirroring how the legacy // PSPDFKit pipeline produced multiple form fields per placeholder. var structured = new List(); foreach (var entry in payload.Signatures) { structured.Add(new AnnotationCreateDto { ElementId = entry.ElementId, Name = $"{entry.ElementId}#signature", Type = "signature", Value = entry.SignatureDataUrl, }); if (!string.IsNullOrWhiteSpace(entry.Position)) { structured.Add(new AnnotationCreateDto { ElementId = entry.ElementId, Name = $"{entry.ElementId}#position", Type = "position", Value = entry.Position!, }); } if (!string.IsNullOrWhiteSpace(entry.City)) { structured.Add(new AnnotationCreateDto { ElementId = entry.ElementId, Name = $"{entry.ElementId}#city", Type = "city", Value = entry.City!, }); } structured.Add(new AnnotationCreateDto { ElementId = entry.ElementId, Name = $"{entry.ElementId}#date", Type = "date", Value = entry.SignedAt.ToString("o"), }); } // No PSPDFKit Instant JSON for the Blazor flow — pass an empty object. var psPdfKitAnnotation = new PsPdfKitAnnotation(new ExpandoObject(), structured); var docSignedNotification = await _mediator .ReadEnvelopeReceiverAsync(uuid, signature, cancel) .ToDocSignedNotification(psPdfKitAnnotation) ?? throw new NotFoundException("Envelope receiver is not found."); await _mediator.PublishSafely(docSignedNotification, cancel); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Ok(); } }