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);
}