Детали событий описываются списком изменений «параметр: было → стало» (AuditChangeDto, AuditDetails), а не произвольным JSON. Запись везде идёт через конструктор AuditDetails; разбор старого плоского формата сохранён. Операторские ручки аудита отдают AuditRecordViewDto с changes и именем пользователя (батч-резолв через ITenantRepository.FindNamesByIdsAsync). Детектор подозрительной активности читает логин через новый разбор.
143 lines
6.7 KiB
C#
143 lines
6.7 KiB
C#
using Deal.Api.Endpoints.RequestModels;
|
|
using Deal.Api.Extensions;
|
|
using Deal.Api.Services;
|
|
using Deal.Api.Telegram;
|
|
using Deal.Modules.Tenants.Application.Models;
|
|
using Deal.Modules.Tenants.Application.Services;
|
|
|
|
namespace Deal.Api.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Операторские ручки глобальных
|
|
/// </summary>
|
|
public static class OperatorSettingsEndpoints
|
|
{
|
|
// Префикс группы операторских настроек.
|
|
private const string SettingsGroupPrefix = "/api/operator/settings";
|
|
|
|
// OpenAPI-тег группы.
|
|
private const string SettingsOpenApiTag = "operator-settings";
|
|
|
|
// Относительный путь глобальных ключей Telegram (GET/PUT).
|
|
private const string TelegramKeysPath = "/telegram-keys";
|
|
|
|
// Текст 400: пустое тело PUT (ни одного поля).
|
|
private const string EmptyBodyDetail = "Укажите api_id и api_hash";
|
|
|
|
// Текст 400: частичное обновление, но ключей ещё нет — нужны оба поля.
|
|
private const string MissingKeysDetail = "Ключи ещё не заданы — укажите и api_id, и api_hash";
|
|
|
|
// Текст 400: api_id не 5..9 цифр.
|
|
private const string InvalidApiIdDetail = "api_id должен состоять из 5–9 цифр";
|
|
|
|
// Текст 400: api_hash пустой/маска/с префиксом enc:.
|
|
private const string InvalidApiHashDetail = "Укажите непустой api_hash";
|
|
|
|
/// <summary>
|
|
/// Регистрирует группу /api/operator/settings
|
|
/// </summary>
|
|
/// <param name="app">Построитель маршрутов приложения.</param>
|
|
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
|
public static IEndpointRouteBuilder MapOperatorSettingsEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup(SettingsGroupPrefix).WithTags(SettingsOpenApiTag);
|
|
group.MapGet(TelegramKeysPath, GetTelegramKeysAsync);
|
|
group.MapPut(TelegramKeysPath, PutTelegramKeysAsync);
|
|
return app;
|
|
}
|
|
|
|
// GET /api/operator/settings/telegram-keys: маскированные глобальные ключи Telegram.
|
|
// context: Контекст запроса.
|
|
// keys: Сервис глобальных ключей Telegram (scoped).
|
|
// ct: Токен отмены.
|
|
// Возвращает: 200 маскированный снимок или 401 без операторской сессии.
|
|
private static async Task<IResult> GetTelegramKeysAsync(
|
|
HttpContext context,
|
|
TelegramKeysService keys,
|
|
CancellationToken ct)
|
|
{
|
|
if (context.GetCurrentOperator() is null)
|
|
{
|
|
return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail);
|
|
}
|
|
|
|
TelegramKeysMaskedDto snapshot = await keys.GetMaskedAsync(ct);
|
|
return Results.Ok(snapshot);
|
|
}
|
|
|
|
// PUT /api/operator/settings/telegram-keys: частичное сохранение глобальных ключей Telegram
|
|
// оператором.
|
|
// Поля можно передавать по отдельности: непереданное поле (null) сохраняет текущее значение,
|
|
// явное значение (в т.ч. пустая строка) валидируется. Если ключей ещё нет, оба поля обязательны.
|
|
// body: Тело {apiId?, apiHash?} (хотя бы одно поле).
|
|
// context: Контекст запроса.
|
|
// keys: Сервис глобальных ключей Telegram (scoped).
|
|
// auditService: Сервис аудита (событие telegram_keys_changed).
|
|
// ct: Токен отмены.
|
|
// Возвращает: 200 маскированный снимок, 400 при невалидных/недостающих полях или 401 без операторской сессии.
|
|
private static async Task<IResult> PutTelegramKeysAsync(
|
|
OperatorTelegramKeysRequest? body,
|
|
HttpContext context,
|
|
TelegramKeysService keys,
|
|
AuditService auditService,
|
|
CancellationToken ct)
|
|
{
|
|
var operatorIdentity = context.GetCurrentOperator();
|
|
if (operatorIdentity is null)
|
|
{
|
|
return EndpointResults.Unauthorized(AuthHelpers.OperatorUnauthorizedDetail);
|
|
}
|
|
|
|
if (body is null)
|
|
{
|
|
return EndpointResults.BadRequest(EmptyBodyDetail);
|
|
}
|
|
|
|
// null — поле не передано (сохраняем текущее); непустая строка/плейсхолдер — валидируем явно.
|
|
string? apiId = body.ApiId?.Trim();
|
|
string? apiHash = body.ApiHash?.Trim();
|
|
if (apiId is null && apiHash is null)
|
|
{
|
|
return EndpointResults.BadRequest(EmptyBodyDetail);
|
|
}
|
|
|
|
if (apiId is not null && !TelegramKeysService.IsValidApiId(apiId))
|
|
{
|
|
return EndpointResults.BadRequest(InvalidApiIdDetail);
|
|
}
|
|
|
|
if (apiHash is not null && !TelegramKeysService.IsValidApiHash(apiHash))
|
|
{
|
|
return EndpointResults.BadRequest(InvalidApiHashDetail);
|
|
}
|
|
|
|
// Частичное обновление: недостающее поле берём из текущих ключей; если ключей ещё нет — нужны оба.
|
|
TgKeysSnapshot current = await keys.GetAsync(ct);
|
|
string effectiveApiId = apiId ?? current.ApiId;
|
|
string effectiveApiHash = apiHash ?? current.ApiHash;
|
|
if (effectiveApiId.Length == 0 || effectiveApiHash.Length == 0)
|
|
{
|
|
return EndpointResults.BadRequest(MissingKeysDetail);
|
|
}
|
|
|
|
await keys.SaveAsync(effectiveApiId, effectiveApiHash, ct);
|
|
|
|
await auditService.AppendAsync(new AuditRecordDto(
|
|
AuditEvents.TelegramKeysChanged,
|
|
AuditActorTypes.Operator,
|
|
ActorId: operatorIdentity.OperatorId,
|
|
TenantId: null,
|
|
Ip: ClientIp(context),
|
|
DetailJson: AuditService.ToDetailJson(
|
|
[AuditDetails.Set("apiId", effectiveApiId), AuditDetails.Set("apiHashSet", true)])), ct);
|
|
|
|
TelegramKeysMaskedDto snapshot = await keys.GetMaskedAsync(ct);
|
|
return Results.Ok(snapshot);
|
|
}
|
|
|
|
// IP-адрес клиента для аудита (без порта; null, если недоступен).
|
|
// context: Контекст запроса.
|
|
// Возвращает: Строковое представление IP или null.
|
|
private static string? ClientIp(HttpContext context) => context.Connection.RemoteIpAddress?.ToString();
|
|
}
|