From 482b3454389b7a7aa59a1f2ebdfcd2a5472fd4e9 Mon Sep 17 00:00:00 2001 From: stepan Date: Sun, 13 Sep 2026 20:32:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D1=8B=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B2=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=20=D0=B8=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B8=20JSON=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Магические строки кодов деталей аудита заменены каталогом AuditFields (68 мест), ключи элемента изменения и разбора JSON — константами, ключи хранилища ключей Telegram и геометрия маски — именованными константами. --- src/core/Deal.Api/Endpoints/AuthEndpoints.cs | 10 +- .../Endpoints/CardDetailsEndpoints.cs | 2 +- src/core/Deal.Api/Endpoints/CardsEndpoints.cs | 18 +-- .../Deal.Api/Endpoints/ContainersEndpoints.cs | 8 +- src/core/Deal.Api/Endpoints/JoinEndpoint.cs | 2 +- .../Endpoints/OperatorAuthEndpoints.cs | 6 +- .../Endpoints/OperatorInvitesEndpoints.cs | 4 +- .../Endpoints/OperatorLimitsEndpoints.cs | 2 +- .../Endpoints/OperatorSettingsEndpoints.cs | 2 +- .../Endpoints/OperatorTenantsEndpoints.cs | 6 +- .../Deal.Api/Endpoints/SettingsEndpoints.cs | 2 +- .../Deal.Api/Endpoints/TelegramEndpoints.cs | 10 +- .../Deal.Api/Services/AuditViewFactory.cs | 2 +- .../Deal.Api/Telegram/TelegramKeysService.cs | 20 ++- .../Extensions/AuditRecordDtoExtensions.cs | 11 +- .../Application/Models/AuditDetails.cs | 6 +- .../Application/Models/AuditFields.cs | 122 ++++++++++++++++++ .../Services/SuspiciousActivityService.cs | 2 +- 18 files changed, 189 insertions(+), 46 deletions(-) create mode 100644 src/core/Deal.Modules.Tenants/Application/Models/AuditFields.cs diff --git a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs index 45a9396..306f64b 100644 --- a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs @@ -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); diff --git a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs index 44791e5..e3ec24a 100644 --- a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs @@ -120,7 +120,7 @@ public static class CardDetailsEndpoints CardsService service = context.RequestServices.GetRequiredService(); 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); } diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs index e90d8ac..d27dd52 100644 --- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs @@ -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(); CardDto unified = await cardsService.GetCardAsync(cardId, ct); @@ -202,7 +202,7 @@ public static class CardsEndpoints CardsService cardsService = context.RequestServices.GetRequiredService(); 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(); 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); } diff --git a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs index 34e50a6..aff2392 100644 --- a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs @@ -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(); 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(); 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 }); } diff --git a/src/core/Deal.Api/Endpoints/JoinEndpoint.cs b/src/core/Deal.Api/Endpoints/JoinEndpoint.cs index 065b981..8a642d0 100644 --- a/src/core/Deal.Api/Endpoints/JoinEndpoint.cs +++ b/src/core/Deal.Api/Endpoints/JoinEndpoint.cs @@ -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 }); } diff --git a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs index 7f5d8c4..aac7ad1 100644 --- a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs @@ -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 }); diff --git a/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs index 83e58a4..dead97d 100644 --- a/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorInvitesEndpoints.cs @@ -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 }); } diff --git a/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs index ee6b15e..3e72c97 100644 --- a/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorLimitsEndpoints.cs @@ -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)); } diff --git a/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs index e0ae264..8a324f4 100644 --- a/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorSettingsEndpoints.cs @@ -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); diff --git a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs index 794d17a..d757d2f 100644 --- a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs @@ -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-куку). diff --git a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs index 7a0c23d..7944ea9 100644 --- a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs @@ -74,7 +74,7 @@ public static class SettingsEndpoints SettingsService settingsService = context.RequestServices.GetRequiredService(); 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)) { diff --git a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs index 7ea2670..d81a796 100644 --- a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs @@ -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 }); diff --git a/src/core/Deal.Api/Services/AuditViewFactory.cs b/src/core/Deal.Api/Services/AuditViewFactory.cs index 5e8919e..ca547e8 100644 --- a/src/core/Deal.Api/Services/AuditViewFactory.cs +++ b/src/core/Deal.Api/Services/AuditViewFactory.cs @@ -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); } // Уникальные идентификаторы пространств выборки. diff --git a/src/core/Deal.Api/Telegram/TelegramKeysService.cs b/src/core/Deal.Api/Telegram/TelegramKeysService.cs index 6e2725e..862945f 100644 --- a/src/core/Deal.Api/Telegram/TelegramKeysService.cs +++ b/src/core/Deal.Api/Telegram/TelegramKeysService.cs @@ -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)), }; } } diff --git a/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs b/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs index 5427526..445bb56 100644 --- a/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs +++ b/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs @@ -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)); } diff --git a/src/core/Deal.Modules.Tenants/Application/Models/AuditDetails.cs b/src/core/Deal.Modules.Tenants/Application/Models/AuditDetails.cs index 2940030..dcbaba7 100644 --- a/src/core/Deal.Modules.Tenants/Application/Models/AuditDetails.cs +++ b/src/core/Deal.Modules.Tenants/Application/Models/AuditDetails.cs @@ -7,6 +7,10 @@ namespace Deal.Modules.Tenants.Application.Models; /// public static class AuditDetails { + // Строковые представления булевых значений (как в JSON). + private const string TrueLiteral = "true"; + private const string FalseLiteral = "false"; + /// /// Задаёт параметр события /// @@ -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(), diff --git a/src/core/Deal.Modules.Tenants/Application/Models/AuditFields.cs b/src/core/Deal.Modules.Tenants/Application/Models/AuditFields.cs new file mode 100644 index 0000000..8503abb --- /dev/null +++ b/src/core/Deal.Modules.Tenants/Application/Models/AuditFields.cs @@ -0,0 +1,122 @@ +namespace Deal.Modules.Tenants.Application.Models; + +/// +/// Каталог кодов параметров в деталях события аудита +/// +public static class AuditFields +{ + /// + /// Логин пользователя + /// + public const string Login = "login"; + + /// + /// Email + /// + public const string Email = "email"; + + /// + /// Отпечаток (хэш) кода приглашения + /// + public const string CodeHash = "codeHash"; + + /// + /// Имя сущности (пользователь/колонка) + /// + public const string Name = "name"; + + /// + /// Статус + /// + public const string Status = "status"; + + /// + /// Бюджет токенов + /// + public const string Budget = "budget"; + + /// + /// Период лимита + /// + public const string Period = "period"; + + /// + /// Идентификатор приложения Telegram + /// + public const string ApiId = "apiId"; + + /// + /// Признак заданного секрета приложения Telegram + /// + public const string ApiHashSet = "apiHashSet"; + + /// + /// Логин, под которым выполнен вход от имени пользователя + /// + public const string TargetLogin = "targetLogin"; + + /// + /// Карточка + /// + public const string CardId = "cardId"; + + /// + /// Контейнер назначения карточки + /// + public const string Destination = "to"; + + /// + /// Колонка карточки + /// + public const string Column = "col"; + + /// + /// Контейнер (колонка) + /// + public const string ContainerId = "containerId"; + + /// + /// Число обработанных карточек + /// + public const string Attempted = "attempted"; + + /// + /// Число переклассифицированных карточек + /// + public const string Reclassified = "reclassified"; + + /// + /// Число перемещённых карточек + /// + public const string Moved = "moved"; + + /// + /// Число карточек, отправленных в корзину + /// + public const string Trashed = "trashed"; + + /// + /// Изменённые поля настроек + /// + public const string Fields = "fields"; + + /// + /// Признак действия «все каналы» + /// + public const string All = "all"; + + /// + /// Число каналов + /// + public const string Count = "count"; + + /// + /// Этап подключения Telegram + /// + public const string Phase = "phase"; + + /// + /// Диалог Telegram + /// + public const string DialogId = "dialogId"; +} diff --git a/src/core/Deal.Modules.Tenants/Application/Services/SuspiciousActivityService.cs b/src/core/Deal.Modules.Tenants/Application/Services/SuspiciousActivityService.cs index 2502f99..d51f8bd 100644 --- a/src/core/Deal.Modules.Tenants/Application/Services/SuspiciousActivityService.cs +++ b/src/core/Deal.Modules.Tenants/Application/Services/SuspiciousActivityService.cs @@ -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: Фактическое значение правила.