| | | 1 | | using System.Diagnostics; |
| | | 2 | | using System.Net.Http.Headers; |
| | | 3 | | using System.Text; |
| | | 4 | | using Microsoft.Extensions.Options; |
| | | 5 | | using Newtonsoft.Json.Linq; |
| | | 6 | | using Services.ApiServices.ApiSupplierRequestLog; |
| | | 7 | | |
| | | 8 | | namespace Services.ApiServices.NikeiApi; |
| | | 9 | | |
| | | 10 | | /// <summary>Транспорт Nikei: Basic Auth остаётся только в заголовке и никогда не попадает в аудит.</summary> |
| | | 11 | | public sealed class NikeiHttpClient |
| | | 12 | | { |
| | | 13 | | private const string Provider = "Nikei"; |
| | | 14 | | |
| | | 15 | | private readonly IHttpClientFactory _factory; |
| | | 16 | | private readonly IOptionsMonitor<NikeiConfig> _options; |
| | | 17 | | private readonly ApiSupplierRequestLogStore? _logs; |
| | | 18 | | |
| | 58 | 19 | | public NikeiHttpClient(IHttpClientFactory factory, IOptionsMonitor<NikeiConfig> options, |
| | 58 | 20 | | ApiSupplierRequestLogStore? logs = null) |
| | | 21 | | { |
| | 58 | 22 | | _factory = factory; |
| | 58 | 23 | | _options = options; |
| | 58 | 24 | | _logs = logs; |
| | 58 | 25 | | } |
| | | 26 | | |
| | | 27 | | public async Task<JToken> GetAsync(string path, CancellationToken cancellationToken = default) |
| | | 28 | | { |
| | 4 | 29 | | var config = _options.CurrentValue; |
| | 4 | 30 | | if (string.IsNullOrWhiteSpace(config.Login) || string.IsNullOrWhiteSpace(config.Password)) |
| | 0 | 31 | | throw new NikeiApiException("Не настроены Nikei:Login и Nikei:Password.", requestPath: path); |
| | | 32 | | |
| | 4 | 33 | | var endpoint = "/" + path.TrimStart('/'); |
| | 4 | 34 | | var timer = Stopwatch.StartNew(); |
| | | 35 | | HttpResponseMessage response; |
| | | 36 | | try |
| | | 37 | | { |
| | 4 | 38 | | using var request = new HttpRequestMessage(HttpMethod.Get, BuildUri(config.BaseUrl, endpoint)); |
| | 4 | 39 | | var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{config.Login}:{config.Password}")); |
| | 4 | 40 | | request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); |
| | 4 | 41 | | response = await _factory.CreateClient("NikeiApi").SendAsync(request, cancellationToken); |
| | 4 | 42 | | } |
| | 0 | 43 | | catch (Exception ex) when (ex is not NikeiApiException) |
| | | 44 | | { |
| | 0 | 45 | | _logs?.Add(Provider, HttpMethod.Get.Method, endpoint, null, timer, null, ex.Message); |
| | 0 | 46 | | throw new NikeiApiException("Nikei недоступен.", requestPath: endpoint, innerException: ex); |
| | | 47 | | } |
| | | 48 | | |
| | 4 | 49 | | using (response) |
| | | 50 | | { |
| | 4 | 51 | | var text = await response.Content.ReadAsStringAsync(cancellationToken); |
| | 4 | 52 | | _logs?.Add(Provider, HttpMethod.Get.Method, endpoint, (int)response.StatusCode, timer, null, text); |
| | 4 | 53 | | if (!response.IsSuccessStatusCode) |
| | 0 | 54 | | throw new NikeiApiException($"Nikei вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode, |
| | | 55 | | |
| | 4 | 56 | | try { return JToken.Parse(text); } |
| | 0 | 57 | | catch (Exception ex) |
| | | 58 | | { |
| | 0 | 59 | | throw new NikeiApiException("Nikei вернул невалидный JSON.", (int)response.StatusCode, endpoint, ex); |
| | | 60 | | } |
| | | 61 | | } |
| | 4 | 62 | | } |
| | | 63 | | |
| | | 64 | | /// <summary>POST формы Nikei. Токены <c>to_cart</c> передаются в теле, а Basic Auth остаётся в заголовке.</summary> |
| | | 65 | | public async Task<JToken> PostFormAsync(string path, IReadOnlyDictionary<string, string> values, |
| | | 66 | | CancellationToken cancellationToken = default) |
| | | 67 | | { |
| | 29 | 68 | | var config = _options.CurrentValue; |
| | 29 | 69 | | if (string.IsNullOrWhiteSpace(config.Login) || string.IsNullOrWhiteSpace(config.Password)) |
| | 0 | 70 | | throw new NikeiApiException("Не настроены Nikei:Login и Nikei:Password.", requestPath: path); |
| | | 71 | | |
| | 29 | 72 | | var endpoint = "/" + path.TrimStart('/'); |
| | 29 | 73 | | var timer = Stopwatch.StartNew(); |
| | | 74 | | HttpResponseMessage response; |
| | | 75 | | try |
| | | 76 | | { |
| | 29 | 77 | | using var request = new HttpRequestMessage(HttpMethod.Post, BuildUri(config.BaseUrl, endpoint)) |
| | 29 | 78 | | { |
| | 29 | 79 | | Content = new FormUrlEncodedContent(values) |
| | 29 | 80 | | }; |
| | 29 | 81 | | var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{config.Login}:{config.Password}")); |
| | 29 | 82 | | request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); |
| | 29 | 83 | | response = await _factory.CreateClient("NikeiApi").SendAsync(request, cancellationToken); |
| | 29 | 84 | | } |
| | 0 | 85 | | catch (Exception ex) when (ex is not NikeiApiException) |
| | | 86 | | { |
| | 0 | 87 | | _logs?.Add(Provider, HttpMethod.Post.Method, endpoint, null, timer, null, ex.Message); |
| | 0 | 88 | | throw new NikeiApiException("Nikei недоступен.", requestPath: endpoint, innerException: ex); |
| | | 89 | | } |
| | | 90 | | |
| | 29 | 91 | | using (response) |
| | | 92 | | { |
| | 29 | 93 | | var text = await response.Content.ReadAsStringAsync(cancellationToken); |
| | 29 | 94 | | _logs?.Add(Provider, HttpMethod.Post.Method, endpoint, (int)response.StatusCode, timer, null, text); |
| | 29 | 95 | | if (!response.IsSuccessStatusCode) |
| | 2 | 96 | | throw new NikeiApiException($"Nikei вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode, |
| | 27 | 97 | | try { return JToken.Parse(text); } |
| | 0 | 98 | | catch (Exception ex) { throw new NikeiApiException("Nikei вернул невалидный JSON.", (int)response.StatusCode |
| | | 99 | | } |
| | 27 | 100 | | } |
| | | 101 | | |
| | | 102 | | /// <summary>Проверяет бизнес-ошибку, которую Nikei может вернуть с HTTP 200.</summary> |
| | | 103 | | public static void ThrowIfError(JToken token, string path) |
| | | 104 | | { |
| | 15 | 105 | | if (token is not JObject payload || !string.Equals(payload.Value<string>("status"), "error", StringComparison.Or |
| | 1 | 106 | | var message = payload.Value<string>("message") ?? payload.Value<string>("text") ?? "Запрос отклонён."; |
| | 1 | 107 | | throw new NikeiApiException($"Nikei отклонил запрос. {message}", requestPath: path); |
| | | 108 | | } |
| | | 109 | | |
| | | 110 | | private static Uri BuildUri(string baseUrl, string path) |
| | | 111 | | { |
| | 33 | 112 | | if (!Uri.TryCreate(baseUrl.TrimEnd('/') + path, UriKind.Absolute, out var uri)) |
| | 0 | 113 | | throw new NikeiApiException("Некорректный Nikei:BaseUrl.", requestPath: path); |
| | 33 | 114 | | return uri; |
| | | 115 | | } |
| | | 116 | | } |