using System.Text.Json; using Deal.Modules.Tenants.Application.Models; using Deal.SharedKernel.Observability; using Deal.Modules.Tenants.Application.Abstractions; using Deal.Modules.Tenants.Application.Extensions; using Deal.Modules.Tenants.Application.Registrars; namespace Deal.Modules.Tenants.Application.Services; /// /// Прикладной сервис аудита (Ruling 4 этапа 7): append-only запись событий и чтение ленты оператором. /// /// /// Единственная точка записи в public.audit_log: сам проставляет At=UTC-now, вызывается /// из эндпоинтов/сервисов (входы, инвайты, impersonation, действия оператора — задачи 4–10). Update/Delete в /// порту отсутствуют (append-only на уровне кода); TTL/авто-очистка не делаются (Ruling 4). Чтение — только /// оператору (GET /api/operator/audit через QueryAsync/CountAsync). Каталог событий — , /// типы акторов — , JSON деталей — (camelCase, без секретов). /// public sealed class AuditService(IAuditLogStore store) { /// /// Верхняя граница выборки аудита (Ruling 4: limit ≤500). /// public const int MaxQueryLimit = 500; /// /// Размер выборки по умолчанию при отсутствии limit в запросе (эталон DiscoveryLogService). /// public const int DefaultQueryLimit = 100; // Опции JSON деталей: camelCase (конвенция DetailJson/JSON проекта). private static readonly JsonSerializerOptions DetailJsonOptions = new(JsonSerializerDefaults.Web); /// /// Записывает событие аудита (append-only; At = сейчас, UTC). /// /// Запись события (At и Id игнорируются: At проставляет сервис, Id — БД). /// Токен отмены. public async Task AppendAsync(AuditRecordDto record, CancellationToken ct) { await store.AppendAsync(record with { At = DateTimeOffset.UtcNow }, ct); // Прикладная метрика (этап 12, пакет A): счётчик событий аудита по типу/актору // (низкокардинальные метки — без tenantId/actorId). DealMetrics.RecordAuditEvent(record.EventType, record.ActorType); } /// /// Записи по фильтру, новые сверху (прокси порта; чтение — операторский эндпоинт). /// /// Фильтр выборки. /// Токен отмены. /// Записи от новых к старым. public Task> QueryAsync(AuditQueryDto filter, CancellationToken ct) => store.QueryAsync(filter, ct); /// /// Число записей по фильтру (для ответа {items, total}). /// /// Фильтр выборки. /// Токен отмены. /// Полное число записей по фильтру. public Task CountAsync(AuditQueryDto filter, CancellationToken ct) => store.CountAsync(filter, ct); /// /// Сериализует детали события в JSON (camelCase; секреты в объект не класть — правило Ruling 4). /// /// Объект деталей (обычно анонимный: { login = ... }). /// JSON-строка деталей. public static string ToDetailJson(object? details) => JsonSerializer.Serialize(details, DetailJsonOptions); /// /// Актор «пользователь тенанта» по разрешённой сессии: (ActorType, ActorId, TenantId). /// /// Идентичность пользователя тенанта. /// Кортеж актора для полей записи аудита. public static (string ActorType, Guid? ActorId, Guid? TenantId) ActorFromUser(UserIdentityDto user) => (AuditActorTypes.Tenant, user.Id, user.TenantId); /// /// Актор «оператор» по разрешённой операторской сессии: (ActorType, ActorId, TenantId=null). /// /// Идентичность оператора. /// Кортеж актора для полей записи аудита. public static (string ActorType, Guid? ActorId, Guid? TenantId) ActorFromOperator(OperatorIdentityDto operatorIdentity) => (AuditActorTypes.Operator, operatorIdentity.Id, null); }