Вынести коды параметров аудита и ключи JSON в константы

Магические строки кодов деталей аудита заменены каталогом AuditFields (68 мест), ключи элемента изменения и разбора JSON — константами, ключи хранилища ключей Telegram и геометрия маски — именованными константами.
This commit is contained in:
2026-09-13 20:32:11 +03:00
parent 6a88032414
commit 482b345438
18 changed files with 189 additions and 46 deletions
+5 -5
View File
@@ -68,7 +68,7 @@ public static class AuthEndpoints
ActorId: result.UserId, ActorId: result.UserId,
TenantId: result.TenantId, TenantId: result.TenantId,
Ip: ClientIp(context), 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); return EndpointResults.Forbidden(TenantSuspendedDetail);
} }
@@ -84,7 +84,7 @@ public static class AuthEndpoints
ActorId: null, ActorId: null,
TenantId: null, TenantId: null,
Ip: ClientIp(context), Ip: ClientIp(context),
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", attemptedLogin)])), ct); DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, attemptedLogin)])), ct);
} }
return EndpointResults.Unauthorized(InvalidCredentialsDetail); return EndpointResults.Unauthorized(InvalidCredentialsDetail);
@@ -98,7 +98,7 @@ public static class AuthEndpoints
ActorId: result.UserId, ActorId: result.UserId,
TenantId: result.TenantId, TenantId: result.TenantId,
Ip: ClientIp(context), 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); SessionCookieWriter.Append(context, cookieOptions.Value, result.Token);
return Results.Ok(new { ok = true, login = result.Login }); return Results.Ok(new { ok = true, login = result.Login });
@@ -125,12 +125,12 @@ public static class AuthEndpoints
ActorId: logout.OperatorId, ActorId: logout.OperatorId,
TenantId: logout.TenantId, TenantId: logout.TenantId,
Ip: ClientIp(context), 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) 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); context.Response.Cookies.Delete(cookieName);
@@ -120,7 +120,7 @@ public static class CardDetailsEndpoints
CardsService service = context.RequestServices.GetRequiredService<CardsService>(); CardsService service = context.RequestServices.GetRequiredService<CardsService>();
CardDto created = await service.CreateLocalCardAsync(ToCreateLocalDto(body), ct); 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); return await ReadCardAsync(context, created.Id, ct);
} }
@@ -182,7 +182,7 @@ public static class CardsEndpoints
return EndpointResults.BadRequest(outcome.Error); 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>(); CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
CardDto unified = await cardsService.GetCardAsync(cardId, ct); CardDto unified = await cardsService.GetCardAsync(cardId, ct);
@@ -202,7 +202,7 @@ public static class CardsEndpoints
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>(); CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
await cardsService.TrashCardAsync(cardId, ct); 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 }); return Results.Ok(new { ok = true });
} }
@@ -219,7 +219,7 @@ public static class CardsEndpoints
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>(); CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
string col = await cardsService.RestoreCardAsync(cardId, ct); 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 }); return Results.Ok(new { ok = true, col });
} }
@@ -240,7 +240,7 @@ public static class CardsEndpoints
return EndpointResults.NotFound(CardNotFoundDetail); 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 }); return Results.Ok(new { ok = true });
} }
@@ -284,7 +284,7 @@ public static class CardsEndpoints
return EndpointResults.NotFound(CardNotFoundDetail); 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 }); return Results.Ok(new { comments = result.Comments });
} }
@@ -402,10 +402,10 @@ public static class CardsEndpoints
context, context,
AuditEvents.CardReclassified, AuditEvents.CardReclassified,
[ [
AuditDetails.Set("attempted", result.Attempted), AuditDetails.Set(AuditFields.Attempted, result.Attempted),
AuditDetails.Set("reclassified", result.Reclassified), AuditDetails.Set(AuditFields.Reclassified, result.Reclassified),
AuditDetails.Set("moved", result.Moved), AuditDetails.Set(AuditFields.Moved, result.Moved),
AuditDetails.Set("trashed", result.Trashed), AuditDetails.Set(AuditFields.Trashed, result.Trashed),
], ],
ct); ct);
} }
@@ -94,7 +94,7 @@ public static class ContainersEndpoints
Note: body.Note ?? string.Empty), Note: body.Note ?? string.Empty),
ct); 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 }); return Results.Ok(new { id = created.Id });
} }
@@ -153,7 +153,7 @@ public static class ContainersEndpoints
patchBody.Policy), patchBody.Policy),
ct); 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 }); return Results.Ok(new { id = updated.Id });
} }
@@ -171,7 +171,7 @@ public static class ContainersEndpoints
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>(); ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
ContainerDto accepted = await containers.AcceptSuggestedAsync(containerId, ct); 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); return Results.Ok(accepted);
} }
@@ -189,7 +189,7 @@ public static class ContainersEndpoints
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>(); ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
int moved = await containers.DeleteAsync(containerId, ct); 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 }); return Results.Ok(new { ok = true, movedToInbox = moved });
} }
+1 -1
View File
@@ -70,7 +70,7 @@ public static class JoinEndpoint
Ip: ClientIp(context), Ip: ClientIp(context),
// Код инвайта — capability-токен: в аудит пишется только SHA-256-хэш (Security review). // Код инвайта — capability-токен: в аудит пишется только SHA-256-хэш (Security review).
DetailJson: AuditService.ToDetailJson( 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 }); return Results.Ok(new { ok = true, login = result.Login });
} }
@@ -64,7 +64,7 @@ public static class OperatorAuthEndpoints
ActorId: null, ActorId: null,
TenantId: null, TenantId: null,
Ip: ClientIp(context), Ip: ClientIp(context),
DetailJson: AuditService.ToDetailJson([AuditDetails.Set("login", attemptedLogin)])), ct); DetailJson: AuditService.ToDetailJson([AuditDetails.Set(AuditFields.Login, attemptedLogin)])), ct);
} }
return EndpointResults.Unauthorized(InvalidCredentialsDetail); return EndpointResults.Unauthorized(InvalidCredentialsDetail);
@@ -78,7 +78,7 @@ public static class OperatorAuthEndpoints
ActorId: result.OperatorId, ActorId: result.OperatorId,
TenantId: null, TenantId: null,
Ip: ClientIp(context), 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); SetOperatorSessionCookie(context, cookieOptions.Value, result.Token);
return Results.Ok(new { ok = true, login = result.Login }); return Results.Ok(new { ok = true, login = result.Login });
@@ -100,7 +100,7 @@ public static class OperatorAuthEndpoints
if (operatorIdentity is not null) 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 }); return Results.Ok(new { ok = true });
@@ -93,7 +93,7 @@ public static class OperatorInvitesEndpoints
// Код инвайта — capability-токен (по нему активируется приглашение): в аудит пишется // Код инвайта — capability-токен (по нему активируется приглашение): в аудит пишется
// только его SHA-256-хэш, чтобы утечка ленты не давала рабочие коды (Security review). // только его SHA-256-хэш, чтобы утечка ленты не давала рабочие коды (Security review).
DetailJson: AuditService.ToDetailJson( 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; InviteDto invite = result.Invite;
return Results.Ok(new { invite.Code, invite.Email, invite.TenantId, invite.ExpiresAt, invite.Status }); return Results.Ok(new { invite.Code, invite.Email, invite.TenantId, invite.ExpiresAt, invite.Status });
@@ -128,7 +128,7 @@ public static class OperatorInvitesEndpoints
TenantId: null, TenantId: null,
Ip: ClientIp(context), Ip: ClientIp(context),
DetailJson: AuditService.ToDetailJson( 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 }); return Results.Ok(new { ok = true });
} }
@@ -175,7 +175,7 @@ public static class OperatorLimitsEndpoints
TenantId: id, TenantId: id,
Ip: ClientIp(context), Ip: ClientIp(context),
DetailJson: AuditService.ToDetailJson( 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)); return Results.Ok(BuildDetailDto(tenant.Name, ownerLogin, updated));
} }
@@ -129,7 +129,7 @@ public static class OperatorSettingsEndpoints
TenantId: null, TenantId: null,
Ip: ClientIp(context), Ip: ClientIp(context),
DetailJson: AuditService.ToDetailJson( 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); TelegramKeysMaskedDto snapshot = await keys.GetMaskedAsync(ct);
return Results.Ok(snapshot); return Results.Ok(snapshot);
@@ -114,7 +114,7 @@ public static class OperatorTenantsEndpoints
TenantId: createdTenant.Id, TenantId: createdTenant.Id,
Ip: ClientIp(context), Ip: ClientIp(context),
DetailJson: AuditService.ToDetailJson( 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) if (result.OwnerLogin is not null)
{ {
@@ -204,7 +204,7 @@ public static class OperatorTenantsEndpoints
TenantId: result.Tenant.Id, TenantId: result.Tenant.Id,
Ip: ClientIp(context), Ip: ClientIp(context),
DetailJson: AuditService.ToDetailJson( 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 }); return Results.Ok(new { ok = true, status = result.Tenant.Status });
@@ -244,7 +244,7 @@ public static class OperatorTenantsEndpoints
ActorId: operatorIdentity.OperatorId, ActorId: operatorIdentity.OperatorId,
TenantId: result.TenantId, TenantId: result.TenantId,
Ip: ClientIp(context), 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-сессия (как после /api/auth/login): СТАВИМ ту же httpOnly-куку deal_session
// на ответ, чтобы браузер оператора сразу получил tenant-сессию (JS не может записать httpOnly-куку). // на ответ, чтобы браузер оператора сразу получил tenant-сессию (JS не может записать httpOnly-куку).
@@ -74,7 +74,7 @@ public static class SettingsEndpoints
SettingsService settingsService = context.RequestServices.GetRequiredService<SettingsService>(); SettingsService settingsService = context.RequestServices.GetRequiredService<SettingsService>();
PublicSettingsDto result = await settingsService.ApplyPatchAsync(body, ct); 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)) if (ShouldScheduleRatesRefresh(body))
{ {
@@ -170,7 +170,7 @@ public static class TelegramEndpoints
TelegramAuthResultDto result = await gateway.StartQrAsync(apiId, keys.ApiHash, ct); TelegramAuthResultDto result = await gateway.StartQrAsync(apiId, keys.ApiHash, ct);
if (result.Phase == ReadyPhase) 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 }); 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); string phase = await gateway.SendCodeAsync((body.Code ?? string.Empty).Trim(), ct);
if (phase == ReadyPhase) 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 }); return Results.Ok(new { phase });
@@ -224,7 +224,7 @@ public static class TelegramEndpoints
string phase = await gateway.SendPasswordAsync(body.Password ?? string.Empty, ct); string phase = await gateway.SendPasswordAsync(body.Password ?? string.Empty, ct);
if (phase == ReadyPhase) 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 }); return Results.Ok(new { phase });
@@ -343,7 +343,7 @@ public static class TelegramEndpoints
if (body.Enabled) 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 }); return Results.Ok(new { ok = true, count = result.Count, enabled = body.Enabled });
@@ -393,7 +393,7 @@ public static class TelegramEndpoints
if (result.Enabled) 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 }); return Results.Ok(new { ok = true, enabled = result.Enabled });
@@ -62,7 +62,7 @@ public static class AuditViewFactory
return owner; 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:"; 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) + терпимость регистра. // Опции JSON значения telegramKeys: camelCase (как пишет SaveAsync) + терпимость регистра.
private static readonly JsonSerializerOptions KeysJsonOptions = new() private static readonly JsonSerializerOptions KeysJsonOptions = new()
{ {
@@ -49,8 +58,8 @@ public sealed class TelegramKeysService(IGlobalSettingsStore store, ISecretCiphe
{ {
using JsonDocument document = JsonDocument.Parse(row.ValueJson); using JsonDocument document = JsonDocument.Parse(row.ValueJson);
JsonElement root = document.RootElement; JsonElement root = document.RootElement;
string apiId = ReadString(root, "apiId"); string apiId = ReadString(root, ApiIdProperty);
string apiHash = ReadString(root, "apiHash"); string apiHash = ReadString(root, ApiHashProperty);
return new TgKeysSnapshot(apiId, cipher.Decrypt(apiHash)); return new TgKeysSnapshot(apiId, cipher.Decrypt(apiHash));
} }
catch (JsonException) catch (JsonException)
@@ -143,8 +152,11 @@ public sealed class TelegramKeysService(IGlobalSettingsStore store, ISecretCiphe
return value.Length switch return value.Length switch
{ {
0 => string.Empty, 0 => string.Empty,
<= 8 => string.Concat(value.AsSpan(0, 1), MaskEllipsis), <= MaskShortMaxLength => string.Concat(value.AsSpan(0, MaskShortVisibleChars), MaskEllipsis),
_ => string.Concat(value.AsSpan(0, 4), MaskEllipsis, value.AsSpan(value.Length - 4)), _ => 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 ChangesProperty = "changes";
// Ключи элемента изменения нового формата.
private const string FieldProperty = "field";
private const string FromProperty = "from";
private const string ToProperty = "to";
// Параметр события, дублирующий TenantId самой записи, — в изменения не попадает. // Параметр события, дублирующий TenantId самой записи, — в изменения не попадает.
private const string TenantIdProperty = "tenantId"; private const string TenantIdProperty = "tenantId";
@@ -105,14 +110,14 @@ public static class AuditRecordDtoExtensions
foreach (JsonElement item in changes.EnumerateArray()) foreach (JsonElement item in changes.EnumerateArray())
{ {
if (item.ValueKind != JsonValueKind.Object if (item.ValueKind != JsonValueKind.Object
|| !item.TryGetProperty("field", out JsonElement field) || !item.TryGetProperty(FieldProperty, out JsonElement field)
|| field.ValueKind != JsonValueKind.String) || field.ValueKind != JsonValueKind.String)
{ {
continue; continue;
} }
string? from = item.TryGetProperty("from", out JsonElement fromElement) ? Stringify(fromElement) : null; string? from = item.TryGetProperty(FromProperty, out JsonElement fromElement) ? Stringify(fromElement) : null;
string? to = item.TryGetProperty("to", out JsonElement toElement) ? Stringify(toElement) : null; string? to = item.TryGetProperty(ToProperty, out JsonElement toElement) ? Stringify(toElement) : null;
result.Add(new AuditChangeDto(field.GetString()!, from, to)); result.Add(new AuditChangeDto(field.GetString()!, from, to));
} }
@@ -7,6 +7,10 @@ namespace Deal.Modules.Tenants.Application.Models;
/// </summary> /// </summary>
public static class AuditDetails public static class AuditDetails
{ {
// Строковые представления булевых значений (как в JSON).
private const string TrueLiteral = "true";
private const string FalseLiteral = "false";
/// <summary> /// <summary>
/// Задаёт параметр события /// Задаёт параметр события
/// </summary> /// </summary>
@@ -33,7 +37,7 @@ public static class AuditDetails
{ {
null => null, null => null,
string text => text, string text => text,
bool flag => flag ? "true" : "false", bool flag => flag ? TrueLiteral : FalseLiteral,
DateTimeOffset moment => moment.ToString("O", CultureInfo.InvariantCulture), DateTimeOffset moment => moment.ToString("O", CultureInfo.InvariantCulture),
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString(), _ => 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. // Извлекает логин из деталей записи (параметр login); повреждённые детали — null.
// record: Запись аудита. // record: Запись аудита.
// Возвращает: Логин либо null (деталей нет/параметр не задан). // Возвращает: Логин либо null (деталей нет/параметр не задан).
private static string? ExtractLogin(AuditRecordDto record) => record.DetailValue("login"); private static string? ExtractLogin(AuditRecordDto record) => record.DetailValue(AuditFields.Login);
// Уровень находки по превышению порога (≥2× порог — high, иначе medium). // Уровень находки по превышению порога (≥2× порог — high, иначе medium).
// count: Фактическое значение правила. // count: Фактическое значение правила.