Вынести коды параметров аудита и ключи JSON в константы
Магические строки кодов деталей аудита заменены каталогом AuditFields (68 мест), ключи элемента изменения и разбора JSON — константами, ключи хранилища ключей Telegram и геометрия маски — именованными константами.
This commit is contained in:
@@ -68,7 +68,7 @@ public static class AuthEndpoints
|
||||
ActorId: result.UserId,
|
||||
TenantId: result.TenantId,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", NormalizeLogin(body.Login))])), ct);
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, NormalizeLogin(body.Login))])), ct);
|
||||
|
||||
return EndpointResults.Forbidden(TenantSuspendedDetail);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public static class AuthEndpoints
|
||||
ActorId: null,
|
||||
TenantId: null,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", attemptedLogin)])), ct);
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, attemptedLogin)])), ct);
|
||||
}
|
||||
|
||||
return EndpointResults.Unauthorized(InvalidCredentialsDetail);
|
||||
@@ -98,7 +98,7 @@ public static class AuthEndpoints
|
||||
ActorId: result.UserId,
|
||||
TenantId: result.TenantId,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", result.Login)])), ct);
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, result.Login)])), ct);
|
||||
|
||||
SessionCookieWriter.Append(context, cookieOptions.Value, result.Token);
|
||||
return Results.Ok(new { ok = true, login = result.Login });
|
||||
@@ -125,12 +125,12 @@ public static class AuthEndpoints
|
||||
ActorId: logout.OperatorId,
|
||||
TenantId: logout.TenantId,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", logout.Login)])), ct);
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, logout.Login)])), ct);
|
||||
}
|
||||
|
||||
if (user is not null)
|
||||
{
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TenantLogout, [AuditDetails.Set("login", user.Login)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TenantLogout, [AuditDetails.Set(AuditFields.Login, user.Login)], ct);
|
||||
}
|
||||
|
||||
context.Response.Cookies.Delete(cookieName);
|
||||
|
||||
@@ -120,7 +120,7 @@ public static class CardDetailsEndpoints
|
||||
CardsService service = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto created = await service.CreateLocalCardAsync(ToCreateLocalDto(body), ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardCreated, [AuditDetails.Set("cardId", created.Id)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardCreated, [AuditDetails.Set(AuditFields.CardId, created.Id)], ct);
|
||||
return await ReadCardAsync(context, created.Id, ct);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ public static class CardsEndpoints
|
||||
return EndpointResults.BadRequest(outcome.Error);
|
||||
}
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardMoved, [AuditDetails.Set("cardId", cardId), AuditDetails.Set("to", body.To)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardMoved, [AuditDetails.Set(AuditFields.CardId, cardId), AuditDetails.Set(AuditFields.Destination, body.To)], ct);
|
||||
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto unified = await cardsService.GetCardAsync(cardId, ct);
|
||||
@@ -202,7 +202,7 @@ public static class CardsEndpoints
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
await cardsService.TrashCardAsync(cardId, ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardTrashed, [AuditDetails.Set("cardId", cardId)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardTrashed, [AuditDetails.Set(AuditFields.CardId, cardId)], ct);
|
||||
return Results.Ok(new { ok = true });
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ public static class CardsEndpoints
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
string col = await cardsService.RestoreCardAsync(cardId, ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardRestored, [AuditDetails.Set("cardId", cardId), AuditDetails.Set("col", col)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardRestored, [AuditDetails.Set(AuditFields.CardId, cardId), AuditDetails.Set(AuditFields.Column, col)], ct);
|
||||
return Results.Ok(new { ok = true, col });
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ public static class CardsEndpoints
|
||||
return EndpointResults.NotFound(CardNotFoundDetail);
|
||||
}
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardDeleted, [AuditDetails.Set("cardId", cardId)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardDeleted, [AuditDetails.Set(AuditFields.CardId, cardId)], ct);
|
||||
return Results.Ok(new { ok = true });
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ public static class CardsEndpoints
|
||||
return EndpointResults.NotFound(CardNotFoundDetail);
|
||||
}
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardCommentAdded, [AuditDetails.Set("cardId", cardId)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardCommentAdded, [AuditDetails.Set(AuditFields.CardId, cardId)], ct);
|
||||
return Results.Ok(new { comments = result.Comments });
|
||||
}
|
||||
|
||||
@@ -402,10 +402,10 @@ public static class CardsEndpoints
|
||||
context,
|
||||
AuditEvents.CardReclassified,
|
||||
[
|
||||
AuditDetails.Set("attempted", result.Attempted),
|
||||
AuditDetails.Set("reclassified", result.Reclassified),
|
||||
AuditDetails.Set("moved", result.Moved),
|
||||
AuditDetails.Set("trashed", result.Trashed),
|
||||
AuditDetails.Set(AuditFields.Attempted, result.Attempted),
|
||||
AuditDetails.Set(AuditFields.Reclassified, result.Reclassified),
|
||||
AuditDetails.Set(AuditFields.Moved, result.Moved),
|
||||
AuditDetails.Set(AuditFields.Trashed, result.Trashed),
|
||||
],
|
||||
ct);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public static class ContainersEndpoints
|
||||
Note: body.Note ?? string.Empty),
|
||||
ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerCreated, [AuditDetails.Set("containerId", created.Id), AuditDetails.Set("name", created.Name)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerCreated, [AuditDetails.Set(AuditFields.ContainerId, created.Id), AuditDetails.Set(AuditFields.Name, created.Name)], ct);
|
||||
return Results.Ok(new { id = created.Id });
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ public static class ContainersEndpoints
|
||||
patchBody.Policy),
|
||||
ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, [AuditDetails.Set("containerId", updated.Id)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, [AuditDetails.Set(AuditFields.ContainerId, updated.Id)], ct);
|
||||
return Results.Ok(new { id = updated.Id });
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ public static class ContainersEndpoints
|
||||
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
|
||||
ContainerDto accepted = await containers.AcceptSuggestedAsync(containerId, ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, [AuditDetails.Set("containerId", accepted.Id)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, [AuditDetails.Set(AuditFields.ContainerId, accepted.Id)], ct);
|
||||
return Results.Ok(accepted);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ public static class ContainersEndpoints
|
||||
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
|
||||
int moved = await containers.DeleteAsync(containerId, ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerDeleted, [AuditDetails.Set("containerId", containerId)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerDeleted, [AuditDetails.Set(AuditFields.ContainerId, containerId)], ct);
|
||||
return Results.Ok(new { ok = true, movedToInbox = moved });
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ public static class JoinEndpoint
|
||||
Ip: ClientIp(context),
|
||||
// Код инвайта — capability-токен: в аудит пишется только SHA-256-хэш (Security review).
|
||||
DetailJson: AuditService.ToDetailJson(
|
||||
[AuditDetails.Set("email", result.Login), AuditDetails.Set("codeHash", SessionTokens.HashToken(body.Code?.Trim() ?? string.Empty))])), ct);
|
||||
[AuditDetails.Set(AuditFields.Email, result.Login), AuditDetails.Set(AuditFields.CodeHash, SessionTokens.HashToken(body.Code?.Trim() ?? string.Empty))])), ct);
|
||||
|
||||
return Results.Ok(new { ok = true, login = result.Login });
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public static class OperatorAuthEndpoints
|
||||
ActorId: null,
|
||||
TenantId: null,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", attemptedLogin)])), ct);
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, attemptedLogin)])), ct);
|
||||
}
|
||||
|
||||
return EndpointResults.Unauthorized(InvalidCredentialsDetail);
|
||||
@@ -78,7 +78,7 @@ public static class OperatorAuthEndpoints
|
||||
ActorId: result.OperatorId,
|
||||
TenantId: null,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", result.Login)])), ct);
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, result.Login)])), ct);
|
||||
|
||||
SetOperatorSessionCookie(context, cookieOptions.Value, result.Token);
|
||||
return Results.Ok(new { ok = true, login = result.Login });
|
||||
@@ -100,7 +100,7 @@ public static class OperatorAuthEndpoints
|
||||
|
||||
if (operatorIdentity is not null)
|
||||
{
|
||||
await AuditAppender.AppendOperatorAsync(context, AuditEvents.OperatorLogout, [AuditDetails.Set("login", operatorIdentity.Login)], ct);
|
||||
await AuditAppender.AppendOperatorAsync(context, AuditEvents.OperatorLogout, [AuditDetails.Set(AuditFields.Login, operatorIdentity.Login)], ct);
|
||||
}
|
||||
|
||||
return Results.Ok(new { ok = true });
|
||||
|
||||
@@ -93,7 +93,7 @@ public static class OperatorInvitesEndpoints
|
||||
// Код инвайта — capability-токен (по нему активируется приглашение): в аудит пишется
|
||||
// только его SHA-256-хэш, чтобы утечка ленты не давала рабочие коды (Security review).
|
||||
DetailJson: AuditService.ToDetailJson(
|
||||
[AuditDetails.Set("email", result.Invite.Email), AuditDetails.Set("codeHash", SessionTokens.HashToken(result.Invite.Code))])), ct);
|
||||
[AuditDetails.Set(AuditFields.Email, result.Invite.Email), AuditDetails.Set(AuditFields.CodeHash, SessionTokens.HashToken(result.Invite.Code))])), ct);
|
||||
|
||||
InviteDto invite = result.Invite;
|
||||
return Results.Ok(new { invite.Code, invite.Email, invite.TenantId, invite.ExpiresAt, invite.Status });
|
||||
@@ -128,7 +128,7 @@ public static class OperatorInvitesEndpoints
|
||||
TenantId: null,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson(
|
||||
[AuditDetails.Set("email", result.Invite!.Email), AuditDetails.Set("codeHash", SessionTokens.HashToken(result.Invite.Code))])), ct);
|
||||
[AuditDetails.Set(AuditFields.Email, result.Invite!.Email), AuditDetails.Set(AuditFields.CodeHash, SessionTokens.HashToken(result.Invite.Code))])), ct);
|
||||
|
||||
return Results.Ok(new { ok = true });
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public static class OperatorLimitsEndpoints
|
||||
TenantId: id,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson(
|
||||
[AuditDetails.Change("budget", current.BudgetTokens, newBudget), AuditDetails.Change("period", current.Period, newPeriod)])), ct);
|
||||
[AuditDetails.Change(AuditFields.Budget, current.BudgetTokens, newBudget), AuditDetails.Change(AuditFields.Period, current.Period, newPeriod)])), ct);
|
||||
|
||||
return Results.Ok(BuildDetailDto(tenant.Name, ownerLogin, updated));
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ public static class OperatorSettingsEndpoints
|
||||
TenantId: null,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson(
|
||||
[AuditDetails.Set("apiId", effectiveApiId), AuditDetails.Set("apiHashSet", true)])), ct);
|
||||
[AuditDetails.Set(AuditFields.ApiId, effectiveApiId), AuditDetails.Set(AuditFields.ApiHashSet, true)])), ct);
|
||||
|
||||
TelegramKeysMaskedDto snapshot = await keys.GetMaskedAsync(ct);
|
||||
return Results.Ok(snapshot);
|
||||
|
||||
@@ -114,7 +114,7 @@ public static class OperatorTenantsEndpoints
|
||||
TenantId: createdTenant.Id,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson(
|
||||
[AuditDetails.Set("name", createdTenant.Name), AuditDetails.Set("email", result.OwnerLogin)])), ct);
|
||||
[AuditDetails.Set(AuditFields.Name, createdTenant.Name), AuditDetails.Set(AuditFields.Email, result.OwnerLogin)])), ct);
|
||||
|
||||
if (result.OwnerLogin is not null)
|
||||
{
|
||||
@@ -204,7 +204,7 @@ public static class OperatorTenantsEndpoints
|
||||
TenantId: result.Tenant.Id,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson(
|
||||
[AuditDetails.Change("status", result.PreviousStatus, result.Tenant.Status)])), ct);
|
||||
[AuditDetails.Change(AuditFields.Status, result.PreviousStatus, result.Tenant.Status)])), ct);
|
||||
}
|
||||
|
||||
return Results.Ok(new { ok = true, status = result.Tenant.Status });
|
||||
@@ -244,7 +244,7 @@ public static class OperatorTenantsEndpoints
|
||||
ActorId: operatorIdentity.OperatorId,
|
||||
TenantId: result.TenantId,
|
||||
Ip: ClientIp(context),
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("targetLogin", result.Login)])), ct);
|
||||
DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.TargetLogin, result.Login)])), ct);
|
||||
|
||||
// Токен — это tenant-сессия (как после /api/auth/login): СТАВИМ ту же httpOnly-куку deal_session
|
||||
// на ответ, чтобы браузер оператора сразу получил tenant-сессию (JS не может записать httpOnly-куку).
|
||||
|
||||
@@ -74,7 +74,7 @@ public static class SettingsEndpoints
|
||||
SettingsService settingsService = context.RequestServices.GetRequiredService<SettingsService>();
|
||||
PublicSettingsDto result = await settingsService.ApplyPatchAsync(body, ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.SettingsUpdated, [AuditDetails.Set("fields", string.Join(", ", body.Keys))], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.SettingsUpdated, [AuditDetails.Set(AuditFields.Fields, string.Join(", ", body.Keys))], ct);
|
||||
|
||||
if (ShouldScheduleRatesRefresh(body))
|
||||
{
|
||||
|
||||
@@ -170,7 +170,7 @@ public static class TelegramEndpoints
|
||||
TelegramAuthResultDto result = await gateway.StartQrAsync(apiId, keys.ApiHash, ct);
|
||||
if (result.Phase == ReadyPhase)
|
||||
{
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TelegramLinked, [AuditDetails.Set("phase", result.Phase)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TelegramLinked, [AuditDetails.Set(AuditFields.Phase, result.Phase)], ct);
|
||||
}
|
||||
|
||||
return Results.Ok(new { phase = result.Phase, qrUrl = result.QrUrl ?? string.Empty });
|
||||
@@ -197,7 +197,7 @@ public static class TelegramEndpoints
|
||||
string phase = await gateway.SendCodeAsync((body.Code ?? string.Empty).Trim(), ct);
|
||||
if (phase == ReadyPhase)
|
||||
{
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TelegramLinked, [AuditDetails.Set("phase", phase)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TelegramLinked, [AuditDetails.Set(AuditFields.Phase, phase)], ct);
|
||||
}
|
||||
|
||||
return Results.Ok(new { phase });
|
||||
@@ -224,7 +224,7 @@ public static class TelegramEndpoints
|
||||
string phase = await gateway.SendPasswordAsync(body.Password ?? string.Empty, ct);
|
||||
if (phase == ReadyPhase)
|
||||
{
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TelegramLinked, [AuditDetails.Set("phase", phase)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.TelegramLinked, [AuditDetails.Set(AuditFields.Phase, phase)], ct);
|
||||
}
|
||||
|
||||
return Results.Ok(new { phase });
|
||||
@@ -343,7 +343,7 @@ public static class TelegramEndpoints
|
||||
|
||||
if (body.Enabled)
|
||||
{
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ChannelEnabled, [AuditDetails.Set("all", true), AuditDetails.Set("count", result.Count)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ChannelEnabled, [AuditDetails.Set(AuditFields.All, true), AuditDetails.Set(AuditFields.Count, result.Count)], ct);
|
||||
}
|
||||
|
||||
return Results.Ok(new { ok = true, count = result.Count, enabled = body.Enabled });
|
||||
@@ -393,7 +393,7 @@ public static class TelegramEndpoints
|
||||
|
||||
if (result.Enabled)
|
||||
{
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ChannelEnabled, [AuditDetails.Set("dialogId", dialog_id)], ct);
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ChannelEnabled, [AuditDetails.Set(AuditFields.DialogId, dialog_id)], ct);
|
||||
}
|
||||
|
||||
return Results.Ok(new { ok = true, enabled = result.Enabled });
|
||||
|
||||
@@ -62,7 +62,7 @@ public static class AuditViewFactory
|
||||
return owner;
|
||||
}
|
||||
|
||||
return record.DetailValue("login") ?? record.DetailValue("email");
|
||||
return record.DetailValue(AuditFields.Login) ?? record.DetailValue(AuditFields.Email);
|
||||
}
|
||||
|
||||
// Уникальные идентификаторы пространств выборки.
|
||||
|
||||
@@ -26,6 +26,15 @@ public sealed class TelegramKeysService(IGlobalSettingsStore store, ISecretCiphe
|
||||
|
||||
private const string EncryptedPrefix = "enc:";
|
||||
|
||||
// Ключи JSON значения telegramKeys (camelCase, как пишет SaveAsync).
|
||||
private const string ApiIdProperty = "apiId";
|
||||
private const string ApiHashProperty = "apiHash";
|
||||
|
||||
// Геометрия маски секрета: короткий (≤8) → «x…»; иначе «1234…5678».
|
||||
private const int MaskShortMaxLength = 8;
|
||||
private const int MaskShortVisibleChars = 1;
|
||||
private const int MaskEdgeVisibleChars = 4;
|
||||
|
||||
// Опции JSON значения telegramKeys: camelCase (как пишет SaveAsync) + терпимость регистра.
|
||||
private static readonly JsonSerializerOptions KeysJsonOptions = new()
|
||||
{
|
||||
@@ -49,8 +58,8 @@ public sealed class TelegramKeysService(IGlobalSettingsStore store, ISecretCiphe
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
|
||||
JsonElement root = document.RootElement;
|
||||
string apiId = ReadString(root, "apiId");
|
||||
string apiHash = ReadString(root, "apiHash");
|
||||
string apiId = ReadString(root, ApiIdProperty);
|
||||
string apiHash = ReadString(root, ApiHashProperty);
|
||||
return new TgKeysSnapshot(apiId, cipher.Decrypt(apiHash));
|
||||
}
|
||||
catch (JsonException)
|
||||
@@ -143,8 +152,11 @@ public sealed class TelegramKeysService(IGlobalSettingsStore store, ISecretCiphe
|
||||
return value.Length switch
|
||||
{
|
||||
0 => string.Empty,
|
||||
<= 8 => string.Concat(value.AsSpan(0, 1), MaskEllipsis),
|
||||
_ => string.Concat(value.AsSpan(0, 4), MaskEllipsis, value.AsSpan(value.Length - 4)),
|
||||
<= MaskShortMaxLength => string.Concat(value.AsSpan(0, MaskShortVisibleChars), MaskEllipsis),
|
||||
_ => string.Concat(
|
||||
value.AsSpan(0, MaskEdgeVisibleChars),
|
||||
MaskEllipsis,
|
||||
value.AsSpan(value.Length - MaskEdgeVisibleChars)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ public static class AuditRecordDtoExtensions
|
||||
// Ключ служебного поля-контейнера изменений в деталях события.
|
||||
private const string ChangesProperty = "changes";
|
||||
|
||||
// Ключи элемента изменения нового формата.
|
||||
private const string FieldProperty = "field";
|
||||
private const string FromProperty = "from";
|
||||
private const string ToProperty = "to";
|
||||
|
||||
// Параметр события, дублирующий TenantId самой записи, — в изменения не попадает.
|
||||
private const string TenantIdProperty = "tenantId";
|
||||
|
||||
@@ -105,14 +110,14 @@ public static class AuditRecordDtoExtensions
|
||||
foreach (JsonElement item in changes.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object
|
||||
|| !item.TryGetProperty("field", out JsonElement field)
|
||||
|| !item.TryGetProperty(FieldProperty, out JsonElement field)
|
||||
|| field.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? from = item.TryGetProperty("from", out JsonElement fromElement) ? Stringify(fromElement) : null;
|
||||
string? to = item.TryGetProperty("to", out JsonElement toElement) ? Stringify(toElement) : null;
|
||||
string? from = item.TryGetProperty(FromProperty, out JsonElement fromElement) ? Stringify(fromElement) : null;
|
||||
string? to = item.TryGetProperty(ToProperty, out JsonElement toElement) ? Stringify(toElement) : null;
|
||||
result.Add(new AuditChangeDto(field.GetString()!, from, to));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ namespace Deal.Modules.Tenants.Application.Models;
|
||||
/// </summary>
|
||||
public static class AuditDetails
|
||||
{
|
||||
// Строковые представления булевых значений (как в JSON).
|
||||
private const string TrueLiteral = "true";
|
||||
private const string FalseLiteral = "false";
|
||||
|
||||
/// <summary>
|
||||
/// Задаёт параметр события
|
||||
/// </summary>
|
||||
@@ -33,7 +37,7 @@ public static class AuditDetails
|
||||
{
|
||||
null => null,
|
||||
string text => text,
|
||||
bool flag => flag ? "true" : "false",
|
||||
bool flag => flag ? TrueLiteral : FalseLiteral,
|
||||
DateTimeOffset moment => moment.ToString("O", CultureInfo.InvariantCulture),
|
||||
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
|
||||
_ => value.ToString(),
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
namespace Deal.Modules.Tenants.Application.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Каталог кодов параметров в деталях события аудита
|
||||
/// </summary>
|
||||
public static class AuditFields
|
||||
{
|
||||
/// <summary>
|
||||
/// Логин пользователя
|
||||
/// </summary>
|
||||
public const string Login = "login";
|
||||
|
||||
/// <summary>
|
||||
/// Email
|
||||
/// </summary>
|
||||
public const string Email = "email";
|
||||
|
||||
/// <summary>
|
||||
/// Отпечаток (хэш) кода приглашения
|
||||
/// </summary>
|
||||
public const string CodeHash = "codeHash";
|
||||
|
||||
/// <summary>
|
||||
/// Имя сущности (пользователь/колонка)
|
||||
/// </summary>
|
||||
public const string Name = "name";
|
||||
|
||||
/// <summary>
|
||||
/// Статус
|
||||
/// </summary>
|
||||
public const string Status = "status";
|
||||
|
||||
/// <summary>
|
||||
/// Бюджет токенов
|
||||
/// </summary>
|
||||
public const string Budget = "budget";
|
||||
|
||||
/// <summary>
|
||||
/// Период лимита
|
||||
/// </summary>
|
||||
public const string Period = "period";
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор приложения Telegram
|
||||
/// </summary>
|
||||
public const string ApiId = "apiId";
|
||||
|
||||
/// <summary>
|
||||
/// Признак заданного секрета приложения Telegram
|
||||
/// </summary>
|
||||
public const string ApiHashSet = "apiHashSet";
|
||||
|
||||
/// <summary>
|
||||
/// Логин, под которым выполнен вход от имени пользователя
|
||||
/// </summary>
|
||||
public const string TargetLogin = "targetLogin";
|
||||
|
||||
/// <summary>
|
||||
/// Карточка
|
||||
/// </summary>
|
||||
public const string CardId = "cardId";
|
||||
|
||||
/// <summary>
|
||||
/// Контейнер назначения карточки
|
||||
/// </summary>
|
||||
public const string Destination = "to";
|
||||
|
||||
/// <summary>
|
||||
/// Колонка карточки
|
||||
/// </summary>
|
||||
public const string Column = "col";
|
||||
|
||||
/// <summary>
|
||||
/// Контейнер (колонка)
|
||||
/// </summary>
|
||||
public const string ContainerId = "containerId";
|
||||
|
||||
/// <summary>
|
||||
/// Число обработанных карточек
|
||||
/// </summary>
|
||||
public const string Attempted = "attempted";
|
||||
|
||||
/// <summary>
|
||||
/// Число переклассифицированных карточек
|
||||
/// </summary>
|
||||
public const string Reclassified = "reclassified";
|
||||
|
||||
/// <summary>
|
||||
/// Число перемещённых карточек
|
||||
/// </summary>
|
||||
public const string Moved = "moved";
|
||||
|
||||
/// <summary>
|
||||
/// Число карточек, отправленных в корзину
|
||||
/// </summary>
|
||||
public const string Trashed = "trashed";
|
||||
|
||||
/// <summary>
|
||||
/// Изменённые поля настроек
|
||||
/// </summary>
|
||||
public const string Fields = "fields";
|
||||
|
||||
/// <summary>
|
||||
/// Признак действия «все каналы»
|
||||
/// </summary>
|
||||
public const string All = "all";
|
||||
|
||||
/// <summary>
|
||||
/// Число каналов
|
||||
/// </summary>
|
||||
public const string Count = "count";
|
||||
|
||||
/// <summary>
|
||||
/// Этап подключения Telegram
|
||||
/// </summary>
|
||||
public const string Phase = "phase";
|
||||
|
||||
/// <summary>
|
||||
/// Диалог Telegram
|
||||
/// </summary>
|
||||
public const string DialogId = "dialogId";
|
||||
}
|
||||
@@ -340,7 +340,7 @@ public sealed class SuspiciousActivityService
|
||||
// Извлекает логин из деталей записи (параметр login); повреждённые детали — null.
|
||||
// record: Запись аудита.
|
||||
// Возвращает: Логин либо null (деталей нет/параметр не задан).
|
||||
private static string? ExtractLogin(AuditRecordDto record) => record.DetailValue("login");
|
||||
private static string? ExtractLogin(AuditRecordDto record) => record.DetailValue(AuditFields.Login);
|
||||
|
||||
// Уровень находки по превышению порога (≥2× порог — high, иначе medium).
|
||||
// count: Фактическое значение правила.
|
||||
|
||||
Reference in New Issue
Block a user