Files
EFC-DESK-V2/Classes/ApiClient.cs

489 lines
19 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();
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/";
}
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> 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
}
}
}