using System; using System.Threading; using System.Threading.Tasks; using FastEndpoints; using GB5Shared.DirectAction; using GB5Shared.DTO.DirectAction; using FrameworkBLL.DirectAction; using FrameworkDAL.CustomCode.DirectAction; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; namespace FrameworkSL.Endpoints.Action { /// /// POST /Action/ConversationReply — form fields: Token, Reply. /// /// Submits one reply into the DirectAction-driven EIP conversation started by /// GET /Action/Conversation, advances the flow, and re-renders the next step /// (or the final success page once the flow completes and DIRECTACTION_EXECUTE /// has run). /// public class PostActionConversationReply : Endpoint { private readonly IDirectActionTokenService _tokenService; private readonly IDirectActionTokenDAL _tokenDAL; private readonly IDirectActionConversationBLL _conversationBLL; private readonly ILogger _logger; public PostActionConversationReply( IDirectActionTokenService tokenService, IDirectActionTokenDAL tokenDAL, IDirectActionConversationBLL conversationBLL, ILogger logger) { _tokenService = tokenService; _tokenDAL = tokenDAL; _conversationBLL = conversationBLL; _logger = logger; } public override void Configure() { Post("/Action/ConversationReply"); AllowAnonymous(); Description(b => b.WithTags("DirectAction")); } public override async Task HandleAsync(ActionConversationReplyRequest req, CancellationToken ct) { try { if (string.IsNullOrWhiteSpace(req.Token)) { await SendHtmlAsync(ActionHtmlHelpers.ErrorPage("Invalid Link", "This action link is missing a token."), ct); return; } if (!_tokenService.TryValidate(req.Token, out var actionCode, out var contextId, out var tenantId, out var assigneeUserId, out var databaseName, out var tokenExpiresAt)) { await SendHtmlAsync(ActionHtmlHelpers.ErrorPage("Link Expired or Invalid", "This action link has expired or is not valid."), ct); return; } var login = new LoginDTO { ClientId = tenantId, UserId = assigneeUserId, DatabaseName = databaseName, ConnectionDatabaseName = databaseName }; var tokenHash = ActionHtmlHelpers.ComputeHash(req.Token); var existing = await _tokenDAL.GetByHashAsync(tokenHash, login, ct); if (existing != null) { var msg = existing.UsedAt.HasValue ? $"This link was already used on {existing.UsedAt:yyyy-MM-dd HH:mm} UTC." : "This link has been revoked."; await SendHtmlAsync(ActionHtmlHelpers.ErrorPage("Already Actioned", msg), ct); return; } var frameworkBaseUrl = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}"; var result = await _conversationBLL.ProcessAsync( actionCode, contextId, assigneeUserId, tenantId, tokenHash, tokenExpiresAt, frameworkBaseUrl, userMessage: req.Reply ?? string.Empty, login, ct); await SendHtmlAsync(DirectActionConversationHtml.Page(req.Token, result), ct); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { _logger.LogInformation("PostActionConversationReply: request cancelled by client"); if (!HttpContext.Response.HasStarted) HttpContext.Response.StatusCode = 499; } catch (Exception ex) { _logger.LogError(ex, "PostActionConversationReply: unhandled error"); if (!HttpContext.Response.HasStarted) await SendHtmlAsync(ActionHtmlHelpers.ErrorPage("System Error", "An unexpected error occurred. Please contact your administrator."), ct); } } private async Task SendHtmlAsync(string html, CancellationToken ct) { HttpContext.Response.ContentType = "text/html; charset=utf-8"; await HttpContext.Response.WriteAsync(html, ct); } } public record ActionConversationReplyRequest(string? Token, string? Reply); }