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; /// /// Операторские ручки глобальных /// 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"; /// /// Регистрирует группу /api/operator/settings /// /// Построитель маршрутов приложения. /// Построитель маршрутов для цепочки вызовов. 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 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 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(); }