Application проектов Discovery, Kanban, Pipeline, Settings, Tenants разделён на Abstractions/Exceptions/Extensions/Models/Registrars/Services; namespace приведён к путям, using потребителей мигрированы и дедуплицированы (169 файлов), cref/FQN обновлены.
88 lines
5.3 KiB
C#
88 lines
5.3 KiB
C#
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);
|
||
}
|