Аудит: читаемые имена и «было → стало» вместо сырых данных #28
@@ -182,7 +182,14 @@ public static class CardsEndpoints
|
|||||||
return EndpointResults.BadRequest(outcome.Error);
|
return EndpointResults.BadRequest(outcome.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardMoved, [AuditDetails.Set(AuditFields.CardId, cardId), AuditDetails.Set(AuditFields.Destination, body.To)], ct);
|
await AuditAppender.AppendTenantAsync(
|
||||||
|
context,
|
||||||
|
AuditEvents.CardMoved,
|
||||||
|
[
|
||||||
|
AuditDetails.Set(AuditFields.CardId, cardId),
|
||||||
|
AuditDetails.Change(AuditFields.ContainerId, outcome.From, outcome.To),
|
||||||
|
],
|
||||||
|
ct);
|
||||||
|
|
||||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||||
CardDto unified = await cardsService.GetCardAsync(cardId, ct);
|
CardDto unified = await cardsService.GetCardAsync(cardId, ct);
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ public static class ContainersEndpoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
|
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
|
||||||
|
ContainerDto before = await containers.GetAsync(containerId, ct);
|
||||||
ContainerDto updated = await containers.PatchAsync(
|
ContainerDto updated = await containers.PatchAsync(
|
||||||
containerId,
|
containerId,
|
||||||
new ContainerPatchDto(
|
new ContainerPatchDto(
|
||||||
@@ -153,7 +154,13 @@ public static class ContainersEndpoints
|
|||||||
patchBody.Policy),
|
patchBody.Policy),
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, [AuditDetails.Set(AuditFields.ContainerId, updated.Id)], ct);
|
await AuditAppender.AppendTenantAsync(
|
||||||
|
context,
|
||||||
|
AuditEvents.ContainerUpdated,
|
||||||
|
string.Equals(before.Name, updated.Name, StringComparison.Ordinal)
|
||||||
|
? [AuditDetails.Set(AuditFields.ContainerId, updated.Id)]
|
||||||
|
: [AuditDetails.Change(AuditFields.Name, before.Name, updated.Name)],
|
||||||
|
ct);
|
||||||
return Results.Ok(new { id = updated.Id });
|
return Results.Ok(new { id = updated.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ public sealed class CardMover(CardsService cardsService) : ICardMover
|
|||||||
CardResultDto result = CardsDefaultContainers.Contains(toContainerId)
|
CardResultDto result = CardsDefaultContainers.Contains(toContainerId)
|
||||||
? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct)
|
? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct)
|
||||||
: await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct);
|
: await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct);
|
||||||
return new CardMoveResultDto(result.Error);
|
return new CardMoveResultDto(result.Error, From: result.Card?.PrevCol, To: result.Card?.Col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ namespace Deal.Modules.Cards.Application.Dtos;
|
|||||||
/// Результат перехода карточки единым механизмом <see cref="ICardMover"/>.
|
/// Результат перехода карточки единым механизмом <see cref="ICardMover"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Error">Текст 400-ошибки либо null (успех).</param>
|
/// <param name="Error">Текст 400-ошибки либо null (успех).</param>
|
||||||
public sealed record CardMoveResultDto(string? Error);
|
/// <param name="From">Контейнер-источник после перехода; null — переход не выполнен.</param>
|
||||||
|
/// <param name="To">Контейнер-назначение после перехода; null — переход не выполнен.</param>
|
||||||
|
public sealed record CardMoveResultDto(string? Error, string? From = null, string? To = null);
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ public static class AuditRecordDtoExtensions
|
|||||||
private const string OldPrefix = "old";
|
private const string OldPrefix = "old";
|
||||||
private const string NewPrefix = "new";
|
private const string NewPrefix = "new";
|
||||||
|
|
||||||
|
// Соответствие суффикса «old<X>» имени ключа нового значения в устаревшем формате (имена не совпадают).
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> LegacyNewNameByOldSuffix =
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["Budget"] = "budgetTokens",
|
||||||
|
["Period"] = "period",
|
||||||
|
};
|
||||||
|
|
||||||
// События аудита «неудачный вход» (тенант/оператор).
|
// События аудита «неудачный вход» (тенант/оператор).
|
||||||
private static readonly string[] FailedLoginEvents =
|
private static readonly string[] FailedLoginEvents =
|
||||||
{
|
{
|
||||||
@@ -140,10 +148,23 @@ public static class AuditRecordDtoExtensions
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string? pairedNewName = property.Name.StartsWith(OldPrefix, StringComparison.Ordinal)
|
string? pairedNewName = null;
|
||||||
&& property.Name.Length > OldPrefix.Length
|
if (property.Name.StartsWith(OldPrefix, StringComparison.Ordinal)
|
||||||
? NewPrefix + property.Name[OldPrefix.Length..]
|
&& property.Name.Length > OldPrefix.Length)
|
||||||
: null;
|
{
|
||||||
|
string suffix = property.Name[OldPrefix.Length..];
|
||||||
|
string defaultNewName = NewPrefix + suffix;
|
||||||
|
if (LegacyNewNameByOldSuffix.TryGetValue(suffix, out string? mapped)
|
||||||
|
&& root.TryGetProperty(mapped, out JsonElement mappedValue))
|
||||||
|
{
|
||||||
|
consumed.Add(mapped);
|
||||||
|
result.Add(new AuditChangeDto(LowerFirst(suffix), Stringify(property.Value), Stringify(mappedValue)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pairedNewName = root.TryGetProperty(defaultNewName, out _) ? defaultNewName : null;
|
||||||
|
}
|
||||||
|
|
||||||
if (pairedNewName is not null && root.TryGetProperty(pairedNewName, out JsonElement newValue))
|
if (pairedNewName is not null && root.TryGetProperty(pairedNewName, out JsonElement newValue))
|
||||||
{
|
{
|
||||||
consumed.Add(pairedNewName);
|
consumed.Add(pairedNewName);
|
||||||
|
|||||||
@@ -57,6 +57,30 @@ public sealed class AuditRecordDtoExtensionsTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuditChanges_LegacyLimitChange_PairsBudgetAndPeriod()
|
||||||
|
{
|
||||||
|
var record = Record(
|
||||||
|
"""{"tenantId":"6f3c0d1e-2b4a-4c8d-9e0f-1a2b3c4d5e6f","oldBudget":100000,"oldPeriod":"month","budgetTokens":1000000,"period":"month"}""");
|
||||||
|
|
||||||
|
IReadOnlyList<AuditChangeDto> changes = record.AuditChanges();
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
changes,
|
||||||
|
budget =>
|
||||||
|
{
|
||||||
|
Assert.Equal("budget", budget.Field);
|
||||||
|
Assert.Equal("100000", budget.From);
|
||||||
|
Assert.Equal("1000000", budget.To);
|
||||||
|
},
|
||||||
|
period =>
|
||||||
|
{
|
||||||
|
Assert.Equal("period", period.Field);
|
||||||
|
Assert.Equal("month", period.From);
|
||||||
|
Assert.Equal("month", period.To);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AuditChanges_InvalidJson_ReturnsEmpty()
|
public void AuditChanges_InvalidJson_ReturnsEmpty()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user