502 lines
20 KiB
C#
502 lines
20 KiB
C#
using EFCDesk.Entidades;
|
|
using System;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace EFCDesk.Classes
|
|
{
|
|
|
|
public class ApiException : Exception
|
|
{
|
|
public int StatusCode { get; }
|
|
public ApiException(string message, int statusCode) : base(message)
|
|
{
|
|
StatusCode = statusCode;
|
|
}
|
|
}
|
|
|
|
public class ApiClient : IDisposable
|
|
{
|
|
private static readonly HttpClient _sharedHttpClient = new HttpClient();
|
|
private readonly HttpClient _httpClient;
|
|
private readonly int _maxRetries;
|
|
private readonly TimeSpan _retryDelay;
|
|
private string? _currentToken;
|
|
private string? _currentRefreshToken;
|
|
private readonly object _tokenLock = new object();
|
|
|
|
private string DominioEFC = Helpers.DominioExpedienteElectronico();
|
|
|
|
|
|
public ApiClient(TimeSpan? timeout = null, int maxRetries = 3, TimeSpan? retryDelay = null)
|
|
{
|
|
_httpClient = _sharedHttpClient;
|
|
|
|
if (timeout.HasValue && timeout.Value > _httpClient.Timeout)
|
|
_httpClient.Timeout = timeout.Value;
|
|
|
|
_maxRetries = maxRetries >= 0 ? maxRetries : 3;
|
|
_retryDelay = retryDelay ?? TimeSpan.FromSeconds(2);
|
|
}
|
|
|
|
//private void SetAuthorization(string token)
|
|
//{
|
|
// if (string.IsNullOrWhiteSpace(token))
|
|
// throw new ArgumentException("El token no puede estar vacío.", nameof(token));
|
|
|
|
// _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", token);
|
|
//}
|
|
|
|
public string EndpointLoginToken()
|
|
{
|
|
return @"/api/v1/token/";
|
|
}
|
|
public string EndpointRefreshToken()
|
|
{
|
|
return @"/api/v1/token/refresh/";
|
|
}
|
|
|
|
public string EndpointStatusTask()
|
|
{
|
|
return @"/api/v1/tasks/status/";
|
|
}
|
|
|
|
private void SetAuthorizationBearer(string token, string refresh_token)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
throw new ArgumentException("El token no puede estar vacío.", nameof(token));
|
|
|
|
if (string.IsNullOrWhiteSpace(refresh_token))
|
|
throw new ArgumentException("El refresh token no puede estar vacío.", nameof(refresh_token));
|
|
|
|
lock (_tokenLock)
|
|
{
|
|
_currentToken = token;
|
|
_currentRefreshToken = refresh_token;
|
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
}
|
|
}
|
|
|
|
private async Task<bool> TryRefreshTokenAsync()
|
|
{
|
|
try
|
|
{
|
|
Globales.configJson = ConfiguracionJSON.LoadFromJson();
|
|
|
|
// Obtener el refresh token actual guardado
|
|
string? savedToken = Globales.refresToken;
|
|
if (string.IsNullOrEmpty(savedToken))
|
|
return false;
|
|
|
|
// Usar el endpoint de refresh token
|
|
string dominio = Globales.configJson.DominioExp ?? "";
|
|
if (string.IsNullOrEmpty(dominio))
|
|
return false;
|
|
|
|
string refreshUrl = dominio + EndpointRefreshToken();
|
|
|
|
// Crear solicitud de refresh con el token actual
|
|
var refreshData = new { refresh = savedToken };
|
|
var jsonContent = JsonSerializer.Serialize(refreshData);
|
|
|
|
using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
|
|
|
|
// Limpiar headers para esta solicitud
|
|
// _httpClient.DefaultRequestHeaders.Authorization = null;
|
|
// var response = await _httpClient.PostAsync(refreshUrl, content);
|
|
|
|
using var request = new HttpRequestMessage(HttpMethod.Post, refreshUrl) { Content = content };
|
|
var response = await _httpClient.SendAsync(request);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
string responseContent = await response.Content.ReadAsStringAsync();
|
|
|
|
if (IsJson(responseContent) && HasJsonData(responseContent))
|
|
{
|
|
using var doc = JsonDocument.Parse(responseContent);
|
|
var root = doc.RootElement;
|
|
|
|
// Buscar nuevo token en la respuesta (ajustar según tu API)
|
|
string newToken = "";
|
|
if (root.TryGetProperty("access", out var accessProp))
|
|
{
|
|
newToken = accessProp.GetString() ?? "";
|
|
}
|
|
else if (root.TryGetProperty("token", out var tokenProp))
|
|
{
|
|
newToken = tokenProp.GetString() ?? "";
|
|
}
|
|
|
|
// Buscar nuevo refresh token en la respuesta (ajustar según tu API)
|
|
string newRefreshToken = "";
|
|
if (root.TryGetProperty("refresh", out var refreshProp))
|
|
{
|
|
newRefreshToken = refreshProp.GetString() ?? "";
|
|
}
|
|
else if (root.TryGetProperty("refresh_token", out var refreshtokenProp))
|
|
{
|
|
newRefreshToken = refreshtokenProp.GetString() ?? "";
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(newToken) && !string.IsNullOrEmpty(newRefreshToken))
|
|
{
|
|
// Guardar nuevo token
|
|
Globales.accesToken = newToken;
|
|
|
|
// Guardar nuevo refresh token
|
|
Globales.refresToken = newRefreshToken;
|
|
|
|
// Actualizar headers
|
|
SetAuthorizationBearer(newToken, newRefreshToken);
|
|
return true;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Globales.logger.LogError("Error al intentar refresh token", ex);
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
// Restaurar el token actual si el refresh falló
|
|
if (!string.IsNullOrEmpty(_currentToken) && !string.IsNullOrEmpty(_currentRefreshToken))
|
|
{
|
|
SetAuthorizationBearer(_currentToken, _currentRefreshToken);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task EnsureTokenAsync()
|
|
{
|
|
// Si ya tenemos token en memoria, usarlo
|
|
if (!string.IsNullOrEmpty(_currentToken) && !string.IsNullOrEmpty(_currentRefreshToken))
|
|
{
|
|
SetAuthorizationBearer(_currentToken, _currentRefreshToken);
|
|
return;
|
|
}
|
|
|
|
// Intentar obtener token guardado
|
|
string? savedToken = Globales.accesToken;
|
|
string? savedRefreshToken = Globales.refresToken;
|
|
|
|
if (!string.IsNullOrEmpty(savedToken) && !string.IsNullOrEmpty(savedRefreshToken))
|
|
{
|
|
SetAuthorizationBearer(savedToken, savedRefreshToken);
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException("No hay token disponible. Inicie sesión primero.");
|
|
}
|
|
|
|
|
|
private async Task<string> SendWithRetriesAsync(Func<HttpRequestMessage> requestFunc, bool requireAuth = true)
|
|
{
|
|
int attempts = 0;
|
|
while (true)
|
|
{
|
|
attempts++;
|
|
try
|
|
{
|
|
string? token = null;
|
|
|
|
if (requireAuth)
|
|
{
|
|
await EnsureTokenAsync();
|
|
lock (_tokenLock)
|
|
{
|
|
token = _currentToken;
|
|
}
|
|
}
|
|
|
|
var request = requestFunc();
|
|
|
|
if (requireAuth && token != null)
|
|
{
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
}
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && requireAuth)
|
|
{
|
|
bool refreshed = await TryRefreshTokenAsync();
|
|
if (refreshed)
|
|
{
|
|
string? newToken = null;
|
|
lock (_tokenLock)
|
|
{
|
|
newToken = _currentToken;
|
|
}
|
|
|
|
var newRequest = requestFunc();
|
|
if (newToken != null)
|
|
{
|
|
newRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
|
|
}
|
|
|
|
response = await _httpClient.SendAsync(newRequest);
|
|
if (response.IsSuccessStatusCode)
|
|
return await response.Content.ReadAsStringAsync();
|
|
}
|
|
}
|
|
|
|
string errorContent = await response.Content.ReadAsStringAsync();
|
|
return errorContent;
|
|
}
|
|
return await response.Content.ReadAsStringAsync();
|
|
}
|
|
catch (TaskCanceledException) when (attempts <= _maxRetries)
|
|
{
|
|
await Task.Delay(_retryDelay);
|
|
}
|
|
catch (HttpRequestException) when (attempts <= _maxRetries)
|
|
{
|
|
await Task.Delay(_retryDelay);
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
return CrearJsonError(408, "Tiempo de espera agotado");
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return CrearJsonError(503, $"Error de red: {ex.Message}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return CrearJsonError(500, $"Error inesperado: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task<string> GetAsync(string url)
|
|
{
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Get, url));
|
|
}
|
|
|
|
public async Task<string> GetStatusTaskAsync(string taskId)
|
|
{
|
|
var apiClient = Globales.ApiClient;
|
|
string url = DominioEFC + EndpointStatusTask() + $"{taskId}/";
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Get, url));
|
|
}
|
|
|
|
public async Task<string> PostJsonAsync(string url, string jsonContent)
|
|
{
|
|
using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Post, url) { Content = content });
|
|
}
|
|
|
|
public async Task<string> PutJsonAsync(string url, string jsonContent)
|
|
{
|
|
using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Put, url) { Content = content });
|
|
}
|
|
|
|
public async Task<string> DeleteAsync(string url)
|
|
{
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Delete, url));
|
|
}
|
|
|
|
// Método para enviar archivo y JSON juntos en multipart/form-data
|
|
public async Task<string> PostMultipartAsync(string url, string filePath, object jsonData, string fileFieldName = "archivo", string jsonFieldName = "data")
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
|
throw new ArgumentException("El archivo no existe o la ruta es inválida.", nameof(filePath));
|
|
|
|
using var form = new MultipartFormDataContent();
|
|
|
|
// Archivo
|
|
var fileStream = File.OpenRead(filePath);
|
|
var fileContent = new StreamContent(fileStream);
|
|
form.Add(fileContent, fileFieldName, Path.GetFileName(filePath));
|
|
|
|
// JSON
|
|
var jsonString = JsonSerializer.Serialize(jsonData);
|
|
var jsonContent = new StringContent(jsonString, System.Text.Encoding.UTF8, "application/json");
|
|
form.Add(jsonContent, jsonFieldName);
|
|
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Post, url) { Content = form });
|
|
}
|
|
|
|
public async Task<string> PostMultipartAsync(string url, string filePath, string organizacion, string pedimento,
|
|
string documentType,
|
|
string fuente,
|
|
string fileFieldName = "archivo"
|
|
)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
|
throw new ArgumentException("El archivo no existe o la ruta es inválida.", nameof(filePath));
|
|
|
|
using var form = new MultipartFormDataContent();
|
|
|
|
// Archivo
|
|
var fileStream = File.OpenRead(filePath);
|
|
var fileContent = new StreamContent(fileStream);
|
|
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
|
form.Add(fileContent, fileFieldName, Path.GetFileName(filePath));
|
|
|
|
// Campos de texto
|
|
form.Add(new StringContent(organizacion), "organizacion");
|
|
form.Add(new StringContent(pedimento), "pedimento");
|
|
form.Add(new StringContent(documentType), "document_type");
|
|
form.Add(new StringContent(fuente), "fuente");
|
|
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Post, url) { Content = form });
|
|
}
|
|
|
|
//public async Task<string> PostMultipartZipAsync(string url, string token, byte[] zipBytes, string zipFileName, string fileFieldName = "archivos")
|
|
//{
|
|
// if (zipBytes == null || zipBytes.Length == 0)
|
|
// throw new ArgumentException("El ZIP está vacío o no es válido.", nameof(zipBytes));
|
|
|
|
// SetAuthorization(token);
|
|
|
|
// using var form = new MultipartFormDataContent();
|
|
|
|
// // Convertimos el byte[] a contenido del archivo
|
|
// var fileContent = new ByteArrayContent(zipBytes);
|
|
// fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/zip");
|
|
|
|
// // Nombre del archivo en el multipart
|
|
// form.Add(fileContent, fileFieldName, zipFileName);
|
|
|
|
// // Campos de texto
|
|
// //form.Add(new StringContent(pedimento), "pedimento");
|
|
|
|
// return await SendWithRetriesAsync(() => _httpClient.PostAsync(url, form));
|
|
|
|
//}
|
|
|
|
public async Task<string> PostMultipartZipAsync(string url, string filePath, string zipFileName, Dictionary<string, string> metadatos, string fileFieldName = "archivos")
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
|
throw new ArgumentException("El archivo no existe o la ruta es inválida.", nameof(filePath));
|
|
|
|
using var form = new MultipartFormDataContent();
|
|
|
|
// Archivo
|
|
var fileStream = File.OpenRead(filePath);
|
|
var fileContent = new StreamContent(fileStream);
|
|
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
|
form.Add(fileContent, fileFieldName, Path.GetFileName(filePath));
|
|
|
|
if (metadatos != null)
|
|
{
|
|
foreach (var kvp in metadatos)
|
|
{
|
|
form.Add(new StringContent(kvp.Value), kvp.Key);
|
|
}
|
|
}
|
|
|
|
// Campos de texto
|
|
//form.Add(new StringContent(organizacion), "organizacion");
|
|
//form.Add(new StringContent(pedimento), "pedimento");
|
|
//form.Add(new StringContent(documentType), "document_type");
|
|
//form.Add(new StringContent(fuente), "fuente");
|
|
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Post, url) { Content = form });
|
|
}
|
|
|
|
// Método existente que acepta string JSON
|
|
public async Task<string> PostJsonWithoutAuthAsync(string url, string jsonContent)
|
|
{
|
|
using var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
|
|
return await SendWithRetriesAsync(() => new HttpRequestMessage(HttpMethod.Post, url) { Content = content }, requireAuth: false);
|
|
}
|
|
|
|
// Nueva sobrecarga que acepta cualquier objeto
|
|
public async Task<string> PostJsonWithoutAuthAsync(string url, object data)
|
|
{
|
|
var jsonContent = JsonSerializer.Serialize(data);
|
|
return await PostJsonWithoutAuthAsync(url, jsonContent);
|
|
}
|
|
|
|
public bool IsJson(string response)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(response)) return false;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(response);
|
|
return true;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool HasJsonData(string response)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(response)) return false;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(response);
|
|
var root = doc.RootElement;
|
|
|
|
if (root.ValueKind == JsonValueKind.Object)
|
|
{
|
|
return root.EnumerateObject().Any();
|
|
}
|
|
else if (root.ValueKind == JsonValueKind.Array)
|
|
{
|
|
return root.GetArrayLength() > 0;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Método para deserializar el JSON a un objeto genérico
|
|
public T? TryParseJson<T>(string response) where T : class
|
|
{
|
|
if (string.IsNullOrWhiteSpace(response)) return null;
|
|
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<T>(response);
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
Globales.logger.LogError($"Error al deserializar JSON: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string CrearJsonError(int status, string message)
|
|
{
|
|
return $$"""
|
|
{
|
|
"success": false,
|
|
"status": {{status}},
|
|
"message": "{{message.Replace("\"", "\\\"")}}"
|
|
}
|
|
""";
|
|
}
|
|
|
|
|
|
|
|
public void Dispose()
|
|
{
|
|
// No disposing _httpClient because it's a shared singleton
|
|
}
|
|
}
|
|
}
|