Files
Deal/src/core/Deal.Modules.Tenants/Application/Services/AuditService.cs
T
Rustam Khalimov cd0b3b606b Разбить модули Deal.Modules.* по назначению
Application проектов Discovery, Kanban, Pipeline, Settings, Tenants
разделён на Abstractions/Exceptions/Extensions/Models/Registrars/Services;
namespace приведён к путям, using потребителей мигрированы и
дедуплицированы (169 файлов), cref/FQN обновлены.
2026-09-11 13:18:14 +03:00

88 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// Прикладной сервис аудита (Ruling 4 этапа 7): append-only запись событий и чтение ленты оператором.
/// </summary>
/// <remarks>
/// Единственная точка записи в public.audit_log: <see cref="AppendAsync"/> сам проставляет At=UTC-now, вызывается
/// из эндпоинтов/сервисов (входы, инвайты, impersonation, действия оператора — задачи 4–10). Update/Delete в
/// порту отсутствуют (append-only на уровне кода); TTL/авто-очистка не делаются (Ruling 4). Чтение — только
/// оператору (GET /api/operator/audit через QueryAsync/CountAsync). Каталог событий — <see cref="AuditEvents"/>,
/// типы акторов — <see cref="AuditActorTypes"/>, JSON деталей — <see cref="ToDetailJson"/> (camelCase, без секретов).
/// </remarks>
public sealed class AuditService(IAuditLogStore store)
{
/// <summary>
/// Верхняя граница выборки аудита (Ruling 4: limit ≤500).
/// </summary>
public const int MaxQueryLimit = 500;
/// <summary>
/// Размер выборки по умолчанию при отсутствии limit в запросе (эталон DiscoveryLogService).
/// </summary>
public const int DefaultQueryLimit = 100;
// Опции JSON деталей: camelCase (конвенция DetailJson/JSON проекта).
private static readonly JsonSerializerOptions DetailJsonOptions = new(JsonSerializerDefaults.Web);
/// <summary>
/// Записывает событие аудита (append-only; At = сейчас, UTC).
/// </summary>
/// <param name="record">Запись события (At и Id игнорируются: At проставляет сервис, Id — БД).</param>
/// <param name="ct">Токен отмены.</param>
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);
}
/// <summary>
/// Записи по фильтру, новые сверху (прокси порта; чтение — операторский эндпоинт).
/// </summary>
/// <param name="filter">Фильтр выборки.</param>
/// <param name="ct">Токен отмены.</param>
/// <returns>Записи от новых к старым.</returns>
public Task<IReadOnlyList<AuditRecordDto>> QueryAsync(AuditQueryDto filter, CancellationToken ct) =>
store.QueryAsync(filter, ct);
/// <summary>
/// Число записей по фильтру (для ответа {items, total}).
/// </summary>
/// <param name="filter">Фильтр выборки.</param>
/// <param name="ct">Токен отмены.</param>
/// <returns>Полное число записей по фильтру.</returns>
public Task<int> CountAsync(AuditQueryDto filter, CancellationToken ct) => store.CountAsync(filter, ct);
/// <summary>
/// Сериализует детали события в JSON (camelCase; секреты в объект не класть — правило Ruling 4).
/// </summary>
/// <param name="details">Объект деталей (обычно анонимный: { login = ... }).</param>
/// <returns>JSON-строка деталей.</returns>
public static string ToDetailJson(object? details) => JsonSerializer.Serialize(details, DetailJsonOptions);
/// <summary>
/// Актор «пользователь тенанта» по разрешённой сессии: (ActorType, ActorId, TenantId).
/// </summary>
/// <param name="user">Идентичность пользователя тенанта.</param>
/// <returns>Кортеж актора для полей записи аудита.</returns>
public static (string ActorType, Guid? ActorId, Guid? TenantId) ActorFromUser(UserIdentityDto user) =>
(AuditActorTypes.Tenant, user.Id, user.TenantId);
/// <summary>
/// Актор «оператор» по разрешённой операторской сессии: (ActorType, ActorId, TenantId=null).
/// </summary>
/// <param name="operatorIdentity">Идентичность оператора.</param>
/// <returns>Кортеж актора для полей записи аудита.</returns>
public static (string ActorType, Guid? ActorId, Guid? TenantId) ActorFromOperator(OperatorIdentityDto operatorIdentity) =>
(AuditActorTypes.Operator, operatorIdentity.Id, null);
}