using Deal.Api.Extensions; using Deal.Api.Services; using Deal.Modules.Tenants.Application.Models; using Deal.Modules.Tenants.Application.Services; namespace Deal.Api.Endpoints; /// /// Операторский эндпоинт чтения аудита /// public static class OperatorAuditEndpoints { private const string OperatorGroupPrefix = "/api/operator"; // Путь ленты аудита относительно группы. private const string AuditPath = "/audit"; private const string OperatorOpenApiTag = "operator"; /// /// Регистрирует группу /api/operator /// /// Построитель маршрутов приложения. /// Построитель маршрутов для цепочки вызовов. public static IEndpointRouteBuilder MapOperatorAuditEndpoints(this IEndpointRouteBuilder app) { app.MapGroup(OperatorGroupPrefix).WithTags(OperatorOpenApiTag).MapGet(AuditPath, ListAsync); return app; } private static async Task ListAsync( string? eventType, string? actorType, Guid? actorId, Guid? tenantId, DateTimeOffset? from, DateTimeOffset? to, int? limit, int? offset, HttpContext context, AuditService auditService, CancellationToken ct) { var operatorIdentity = context.GetCurrentOperator(); if (operatorIdentity is null) { return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail); } var filter = new AuditQueryDto( eventType, actorType, tenantId, from, to, NormalizeLimit(limit), actorId, NormalizeOffset(offset)); IReadOnlyList items = await auditService.QueryAsync(filter, ct); int total = await auditService.CountAsync(filter, ct); return Results.Ok(new { items, total }); } /// /// Нормализует limit запроса /// /// Запрошенный размер выборки (null — не задан). /// Значение для фильтра. public static int NormalizeLimit(int? limit) => limit is null ? AuditService.DefaultQueryLimit : Math.Max(1, Math.Min(AuditService.MaxQueryLimit, limit.Value)); /// /// Нормализует offset запроса /// /// Запрошенное смещение (null — не задано). /// Неотрицательное смещение. public static int NormalizeOffset(int? offset) => Math.Max(0, offset ?? 0); }