Initial commit

This commit is contained in:
2026-07-13 09:25:40 +07:00
parent c08ff54676
commit bccfb156d7
1938 changed files with 641646 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
using System.Net;
using System.Net.Http.Json;
using System.Text;
using RobotNet10.CustomConfigurationEditor.Models;
namespace RobotNet10.CustomConfigurationEditor.Services.API;
/// <summary>
/// Service cho giao tiếp với Config REST API
/// </summary>
public class ConfigApiService(HttpClient httpClient)
{
private readonly HttpClient _httpClient = httpClient;
private readonly string? _baseUrl = httpClient.BaseAddress?.AbsoluteUri;
private const string ApiPath = "api/configs";
// ==========================================
// CONFIG FILE MANAGEMENT
// ==========================================
/// <summary>
/// Lấy tất cả configs (metadata only)
/// </summary>
public async Task<List<ConfigFileMetadataModel>> GetAllConfigsAsync(string? search = null)
{
var url = $"{_baseUrl}{ApiPath}";
if (!string.IsNullOrEmpty(search))
url += $"?search={Uri.EscapeDataString(search)}";
return await _httpClient.GetFromJsonAsync<List<ConfigFileMetadataModel>>(url) ?? [];
}
/// <summary>
/// Lấy config theo ID
/// </summary>
public async Task<ConfigFileModel?> GetConfigByIdAsync(Guid id)
{
return await _httpClient.GetFromJsonAsync<ConfigFileModel>($"{_baseUrl}{ApiPath}/{id}");
}
/// <summary>
/// Lấy config theo ConfigType
/// </summary>
public async Task<ConfigFileModel?> GetConfigByTypeAsync(string configType)
{
return await _httpClient.GetFromJsonAsync<ConfigFileModel>($"{_baseUrl}{ApiPath}/by-type/{Uri.EscapeDataString(configType)}");
}
/// <summary>
/// Kiểm tra ConfigType có tồn tại không
/// </summary>
public async Task<bool> ConfigTypeExistsAsync(string configType)
{
return await _httpClient.GetFromJsonAsync<bool>($"{_baseUrl}{ApiPath}/exists/{Uri.EscapeDataString(configType)}");
}
/// <summary>
/// Tạo config mới
/// </summary>
public async Task<ConfigFileModel> CreateConfigAsync(string configType, List<ConfigVariableModel> variables, string? description = null)
{
var request = new
{
ConfigType = configType,
Variables = variables,
Description = description
};
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}{ApiPath}", request);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
return await response.Content.ReadFromJsonAsync<ConfigFileModel>()
?? throw new Exception("Failed to create config. Invalid response from server.");
}
/// <summary>
/// Cập nhật config
/// </summary>
public async Task<ConfigFileModel> UpdateConfigAsync(Guid id, List<ConfigVariableModel>? variables = null, string? description = null)
{
var request = new
{
Variables = variables,
Description = description
};
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}{ApiPath}/{id}", request);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
return await response.Content.ReadFromJsonAsync<ConfigFileModel>()
?? throw new Exception("Failed to update config. Invalid response from server.");
}
/// <summary>
/// Xóa config
/// </summary>
public async Task DeleteConfigAsync(Guid id)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}{ApiPath}/{id}");
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
}
// ==========================================
// IMPORT/EXPORT
// ==========================================
/// <summary>
/// Import config từ JSON file
/// </summary>
public async Task<ConfigFileModel> ImportConfigAsync(Stream fileStream, string fileName, string configType, string? description = null)
{
using var content = new MultipartFormDataContent();
var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
content.Add(streamContent, "file", fileName);
content.Add(new StringContent(configType), "configType");
if (!string.IsNullOrWhiteSpace(description))
{
content.Add(new StringContent(description), "description");
}
var response = await _httpClient.PostAsync($"{_baseUrl}{ApiPath}/import", content);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
return await response.Content.ReadFromJsonAsync<ConfigFileModel>()
?? throw new Exception("Failed to import config. Invalid response from server.");
}
/// <summary>
/// Export config ra JSON file
/// </summary>
public async Task<Stream> ExportConfigAsync(Guid id)
{
var response = await _httpClient.GetAsync($"{_baseUrl}{ApiPath}/{id}/export");
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
try
{
// Read content as byte array first, then create memory stream
var bytes = await response.Content.ReadAsByteArrayAsync();
var memoryStream = new MemoryStream(bytes);
memoryStream.Position = 0; // Reset position to beginning
return memoryStream;
}
catch (Exception ex)
{
throw new HttpRequestException($"Failed to read export data: {HttpErrorHelper.GetErrorMessage(ex)}", ex);
}
}
// ==========================================
// VARIABLE MANAGEMENT
// ==========================================
/// <summary>
/// Cập nhật giá trị của một variable
/// </summary>
public async Task<ConfigFileModel> UpdateVariableAsync(Guid configId, string variableName, object? value)
{
var request = new { Value = value };
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}{ApiPath}/{configId}/variables/{Uri.EscapeDataString(variableName)}", request);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
return await response.Content.ReadFromJsonAsync<ConfigFileModel>()
?? throw new Exception("Failed to update variable. Invalid response from server.");
}
/// <summary>
/// Thêm variable mới vào config
/// </summary>
public async Task<ConfigFileModel> AddVariableAsync(Guid configId, ConfigVariableModel variable)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}{ApiPath}/{configId}/variables", variable);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
return await response.Content.ReadFromJsonAsync<ConfigFileModel>()
?? throw new Exception("Failed to add variable. Invalid response from server.");
}
/// <summary>
/// Xóa variable khỏi config
/// </summary>
public async Task<ConfigFileModel> RemoveVariableAsync(Guid configId, string variableName)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}{ApiPath}/{configId}/variables/{Uri.EscapeDataString(variableName)}");
if (!response.IsSuccessStatusCode)
{
var errorMessage = await HttpErrorHelper.GetErrorMessageAsync(response);
throw new HttpRequestException(errorMessage);
}
return await response.Content.ReadFromJsonAsync<ConfigFileModel>()
?? throw new Exception("Failed to remove variable. Invalid response from server.");
}
}

View File

@@ -0,0 +1,149 @@
using System.Net;
using System.Text.Json;
namespace RobotNet10.CustomConfigurationEditor.Services.API;
/// <summary>
/// Helper class để parse error messages từ HTTP responses
/// </summary>
public static class HttpErrorHelper
{
/// <summary>
/// Extract user-friendly error message từ HttpResponseMessage
/// </summary>
public static async Task<string> GetErrorMessageAsync(HttpResponseMessage response)
{
try
{
// Try to read error message from response body
var content = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(content))
{
// Try to parse as JSON error object
try
{
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
// Check for common error property names
if (root.TryGetProperty("error", out var errorProp))
{
var errorMsg = errorProp.GetString();
if (!string.IsNullOrWhiteSpace(errorMsg))
return errorMsg;
}
if (root.TryGetProperty("message", out var messageProp))
{
var message = messageProp.GetString();
if (!string.IsNullOrWhiteSpace(message))
return message;
}
if (root.TryGetProperty("errors", out var errorsProp) && errorsProp.ValueKind == JsonValueKind.Array)
{
var errors = errorsProp.EnumerateArray()
.Select(e => e.GetString())
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList();
if (errors.Count > 0)
return string.Join("; ", errors);
}
// If it's a simple string, return it
if (root.ValueKind == JsonValueKind.String)
{
return root.GetString() ?? GetDefaultMessage(response.StatusCode);
}
}
catch (JsonException)
{
// If JSON parsing fails, check if content is a simple error message
if (content.Length < 500) // Reasonable length for error message
{
return content;
}
}
}
}
catch (Exception ex) when (ex is HttpRequestException or ObjectDisposedException)
{
// Fall through to default message
}
return GetDefaultMessage(response.StatusCode);
}
/// <summary>
/// Extract user-friendly error message từ Exception
/// </summary>
public static string GetErrorMessage(Exception ex)
{
// Check for HttpRequestException
if (ex is HttpRequestException httpEx)
{
// Try to extract meaningful message
var message = httpEx.Message;
// Remove technical details
if (message.Contains("net_http_message_not_success_statuscode"))
{
return "Unable to connect to server. Please check your network connection.";
}
if (message.Contains("timeout"))
{
return "Request timeout. Please try again.";
}
if (message.Contains("connection"))
{
return "Unable to connect to server. Please check your network connection.";
}
return message;
}
// Check for TaskCanceledException (often timeout)
if (ex is TaskCanceledException)
{
return "Request timeout. Please try again.";
}
// Return original message if it's user-friendly
var exMessage = ex.Message;
if (!string.IsNullOrWhiteSpace(exMessage) &&
!exMessage.Contains("net_http") &&
!exMessage.Contains("StatusCode") &&
!exMessage.Contains("Bad Request") &&
exMessage.Length < 200)
{
return exMessage;
}
// Default fallback
return "An error occurred. Please try again or contact the administrator.";
}
/// <summary>
/// Get default error message based on HTTP status code
/// </summary>
private static string GetDefaultMessage(HttpStatusCode statusCode)
{
return statusCode switch
{
HttpStatusCode.BadRequest => "Invalid data. Please check your input.",
HttpStatusCode.Unauthorized => "You do not have permission to perform this action.",
HttpStatusCode.Forbidden => "You do not have access to this resource.",
HttpStatusCode.NotFound => "The requested data was not found.",
HttpStatusCode.Conflict => "Data already exists or conflicts with existing data.",
HttpStatusCode.InternalServerError => "Server error. Please try again later.",
HttpStatusCode.ServiceUnavailable => "Service is temporarily unavailable. Please try again later.",
HttpStatusCode.GatewayTimeout => "Request timeout. Please try again.",
_ => $"Error: {statusCode}. Please try again."
};
}
}