first commit
This commit is contained in:
466
Classes/ApiClient.cs
Normal file
466
Classes/ApiClient.cs
Normal file
@@ -0,0 +1,466 @@
|
||||
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 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 = new HttpClient();
|
||||
|
||||
if (timeout.HasValue)
|
||||
_httpClient.Timeout = timeout.Value;
|
||||
else
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
_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);
|
||||
|
||||
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<Task<HttpResponseMessage>> sendFunc, bool requireAuth = true)
|
||||
{
|
||||
int attempts = 0;
|
||||
while (true)
|
||||
{
|
||||
attempts++;
|
||||
try
|
||||
{
|
||||
// Asegurar que tenemos token antes de enviar (solo si requiere auth)
|
||||
if (requireAuth)
|
||||
await EnsureTokenAsync();
|
||||
|
||||
var response = await sendFunc();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
// Si es 401 Unauthorized y requiere auth, intentar refresh token
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && requireAuth)
|
||||
{
|
||||
bool refreshed = await TryRefreshTokenAsync();
|
||||
if (refreshed)
|
||||
{
|
||||
// Reintentar con nuevo token
|
||||
response = await sendFunc();
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
}
|
||||
|
||||
string errorContent = await response.Content.ReadAsStringAsync();
|
||||
// throw new ApiException($"Error {response.StatusCode}: {errorContent}", (int)response.StatusCode);
|
||||
return errorContent;
|
||||
}
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch (TaskCanceledException) when (attempts <= _maxRetries)
|
||||
{
|
||||
// Timeout: reintentar
|
||||
await Task.Delay(_retryDelay);
|
||||
}
|
||||
catch (HttpRequestException) when (attempts <= _maxRetries)
|
||||
{
|
||||
// Error red: reintentar
|
||||
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(() => _httpClient.GetAsync(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(() => _httpClient.PostAsync(url, 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(() => _httpClient.PutAsync(url, content));
|
||||
}
|
||||
|
||||
public async Task<string> DeleteAsync(string url)
|
||||
{
|
||||
return await SendWithRetriesAsync(() => _httpClient.DeleteAsync(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(() => _httpClient.PostAsync(url, 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(() => _httpClient.PostAsync(url, 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(() => _httpClient.PostAsync(url, 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(() => _httpClient.PostAsync(url, 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()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user