using FastEndpoints;
namespace KArtSell.Modules.ModelOperations.Compliance;
///
/// Query audit events with filters (compliance officer access).
/// GET /audit/events?entityId=uuid&eventType=MODEL_ACTIVATED&dateFrom=2026-01-01&dateTo=2026-12-31&actorEmail=user@company.com
///
public class QueryAuditEventsRequest
{
public Guid? EntityId { get; set; }
public string? EventType { get; set; }
public DateTime? DateFrom { get; set; }
public DateTime? DateTo { get; set; }
public string? ActorEmail { get; set; }
public int Skip { get; set; } = 0;
public int Take { get; set; } = 50;
}
public class AuditEventDto
{
public Guid Id { get; set; }
public string EventType { get; set; } = string.Empty;
public string EntityType { get; set; } = string.Empty;
public Guid EntityId { get; set; }
public string ActorEmail { get; set; } = string.Empty;
public string? ActorRole { get; set; }
public DateTime EventAt { get; set; }
public string Result { get; set; } = string.Empty;
public Dictionary? Details { get; set; }
public string[]? EvidenceLinks { get; set; }
public DateTime PublishedAt { get; set; }
public Guid CorrelationId { get; set; }
}
public class QueryAuditEventsResponse
{
public List Items { get; set; } = new();
public int Total { get; set; }
public int Skip { get; set; }
public int Take { get; set; }
public int Pages => (Total + Take - 1) / Take;
}
public class QueryAuditEventsEndpoint : Endpoint
{
private readonly AuditSql _sql;
private readonly ILogger _logger;
public QueryAuditEventsEndpoint(AuditSql sql, ILogger logger)
{
_sql = sql;
_logger = logger;
}
public override void Configure()
{
Get("/audit/events");
AllowAnonymous(); // RBAC enforced at handler level (Compliance Officer role)
Description(d => d
.WithName("Query Audit Events")
.WithDescription("Query immutable audit trail with optional filters")
.WithOpenApi());
}
public override async Task HandleAsync(QueryAuditEventsRequest req, CancellationToken ct)
{
try
{
using var db = new NpgsqlConnection(Environment.GetEnvironmentVariable("KARTSELL_POSTGRES"));
db.Open();
var (events, total) = await _sql.QueryAuditEventsAsync(
db,
req.EntityId,
req.EventType,
req.DateFrom,
req.DateTo,
req.ActorEmail,
req.Skip,
req.Take,
ct);
var response = new QueryAuditEventsResponse
{
Items = events.Select(e => new AuditEventDto
{
Id = e.Id,
EventType = e.EventType,
EntityType = e.EntityType,
EntityId = e.EntityId,
ActorEmail = e.ActorEmail,
ActorRole = e.ActorRole,
EventAt = e.EventAt,
Result = e.Result,
Details = e.Details,
EvidenceLinks = e.EvidenceLinks,
PublishedAt = e.PublishedAt,
CorrelationId = e.CorrelationId
}).ToList(),
Total = total,
Skip = req.Skip,
Take = req.Take
};
await SendOkAsync(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to query audit events");
await SendInternalErrorResponse();
}
}
private async Task SendInternalErrorResponse()
{
await SendAsync(
new QueryAuditEventsResponse(),
statusCode: StatusCodes.Status500InternalServerError);
}
}