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