Initial commit
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using RobotNet10.CustomConfigurationEditor.Models;
|
||||
using RobotNet10.CustomConfigurationEditor.Services.API;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
|
||||
namespace RobotNet10.CustomConfigurationEditor.Services.State;
|
||||
|
||||
/// <summary>
|
||||
/// State management cho Config Manager
|
||||
/// </summary>
|
||||
public class ConfigManagerState(ConfigApiService apiService, AuthenticationStateProvider? authStateProvider = null, string editorRole = "")
|
||||
{
|
||||
private readonly ConfigApiService _apiService = apiService;
|
||||
private readonly AuthenticationStateProvider? _authStateProvider = authStateProvider;
|
||||
private readonly SemaphoreSlim _stateLock = new(1, 1);
|
||||
|
||||
// ===== DATA =====
|
||||
public List<ConfigFileMetadataModel> Configs { get; private set; } = [];
|
||||
public ConfigFileModel? SelectedConfig { get; private set; }
|
||||
|
||||
// ===== FILTERS & SEARCH =====
|
||||
public string? SearchQuery { get; set; }
|
||||
|
||||
// ===== UI STATE =====
|
||||
public bool IsLoading { get; private set; }
|
||||
public bool IsSaving { get; private set; }
|
||||
public string? ErrorMessage { get; private set; }
|
||||
|
||||
// ===== EVENTS =====
|
||||
public event Action? OnStateChanged;
|
||||
|
||||
// ==========================================
|
||||
// PUBLIC METHODS
|
||||
// ==========================================
|
||||
|
||||
// Role Editor
|
||||
public string EditorRole { get; } = editorRole;
|
||||
|
||||
/// <summary>
|
||||
/// Load tất cả configs
|
||||
/// </summary>
|
||||
public async Task LoadConfigsAsync(string? searchQuery = null)
|
||||
{
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
SearchQuery = searchQuery;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
Configs = await _apiService.GetAllConfigsAsync(searchQuery);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
Configs = [];
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load config theo ID
|
||||
/// </summary>
|
||||
public async Task LoadConfigByIdAsync(Guid id)
|
||||
{
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
SelectedConfig = await _apiService.GetConfigByIdAsync(id);
|
||||
if (SelectedConfig == null)
|
||||
{
|
||||
ErrorMessage = "Config not found";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
SelectedConfig = null;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load config theo ConfigType
|
||||
/// </summary>
|
||||
public async Task LoadConfigByTypeAsync(string configType)
|
||||
{
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
SelectedConfig = await _apiService.GetConfigByTypeAsync(configType);
|
||||
if (SelectedConfig == null)
|
||||
{
|
||||
ErrorMessage = "Config not found";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
SelectedConfig = null;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select config
|
||||
/// </summary>
|
||||
public async Task SelectConfigAsync(ConfigFileMetadataModel configMetadata)
|
||||
{
|
||||
await LoadConfigByIdAsync(configMetadata.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear error message
|
||||
/// </summary>
|
||||
public void ClearError()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear selection
|
||||
/// </summary>
|
||||
public void ClearSelection()
|
||||
{
|
||||
SelectedConfig = null;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tạo config mới
|
||||
/// </summary>
|
||||
public async Task<ConfigFileModel> CreateConfigAsync(string configType, List<ConfigVariableModel> variables, string? description = null)
|
||||
{
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var config = await _apiService.CreateConfigAsync(configType, variables, description);
|
||||
await ReloadConfigsInternalAsync();
|
||||
SelectedConfig = config;
|
||||
NotifyStateChanged();
|
||||
return config;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cập nhật config
|
||||
/// </summary>
|
||||
public async Task UpdateConfigAsync(List<ConfigVariableModel>? variables = null, string? description = null)
|
||||
{
|
||||
if (SelectedConfig == null)
|
||||
{
|
||||
throw new InvalidOperationException("No config selected");
|
||||
}
|
||||
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
SelectedConfig = await _apiService.UpdateConfigAsync(SelectedConfig.Id, variables, description);
|
||||
await ReloadConfigsInternalAsync();
|
||||
NotifyStateChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xóa config
|
||||
/// </summary>
|
||||
public async Task DeleteConfigAsync(Guid id)
|
||||
{
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await _apiService.DeleteConfigAsync(id);
|
||||
await ReloadConfigsInternalAsync();
|
||||
|
||||
// Clear selection if deleted
|
||||
if (SelectedConfig?.Id == id)
|
||||
{
|
||||
SelectedConfig = null;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import config từ file
|
||||
/// </summary>
|
||||
public async Task<ConfigFileModel> ImportConfigAsync(Stream fileStream, string fileName, string configType, string? description = null)
|
||||
{
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var config = await _apiService.ImportConfigAsync(fileStream, fileName, configType, description);
|
||||
await ReloadConfigsInternalAsync();
|
||||
SelectedConfig = config;
|
||||
NotifyStateChanged();
|
||||
return config;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export config ra file
|
||||
/// </summary>
|
||||
public async Task<Stream> ExportConfigAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _apiService.ExportConfigAsync(id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cập nhật variable value
|
||||
/// </summary>
|
||||
public async Task UpdateVariableAsync(string variableName, object? value)
|
||||
{
|
||||
if (SelectedConfig == null)
|
||||
{
|
||||
throw new InvalidOperationException("No config selected");
|
||||
}
|
||||
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
SelectedConfig = await _apiService.UpdateVariableAsync(SelectedConfig.Id, variableName, value);
|
||||
NotifyStateChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thêm variable
|
||||
/// </summary>
|
||||
public async Task AddVariableAsync(ConfigVariableModel variable)
|
||||
{
|
||||
if (SelectedConfig == null)
|
||||
{
|
||||
throw new InvalidOperationException("No config selected");
|
||||
}
|
||||
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
SelectedConfig = await _apiService.AddVariableAsync(SelectedConfig.Id, variable);
|
||||
NotifyStateChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xóa variable
|
||||
/// </summary>
|
||||
public async Task RemoveVariableAsync(string variableName)
|
||||
{
|
||||
if (SelectedConfig == null)
|
||||
{
|
||||
throw new InvalidOperationException("No config selected");
|
||||
}
|
||||
|
||||
await _stateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
SelectedConfig = await _apiService.RemoveVariableAsync(SelectedConfig.Id, variableName);
|
||||
NotifyStateChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra ConfigType có tồn tại không
|
||||
/// </summary>
|
||||
public async Task<bool> ConfigTypeExistsAsync(string configType)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _apiService.ConfigTypeExistsAsync(configType);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ROLE-BASED PERMISSION CHECKS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra user hiện tại có quyền chỉnh sửa config không
|
||||
/// </summary>
|
||||
public async Task<bool> CanEditConfigAsync()
|
||||
{
|
||||
// Nếu EditorRole rỗng, mọi thứ hoạt động bình thường
|
||||
if (string.IsNullOrWhiteSpace(EditorRole))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Kiểm tra role của user hiện tại
|
||||
var userRoles = await GetCurrentUserRolesAsync();
|
||||
return userRoles.Contains(EditorRole, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra user hiện tại có quyền chỉnh sửa variable không
|
||||
/// </summary>
|
||||
public async Task<bool> CanEditVariableAsync(ConfigVariableModel variable)
|
||||
{
|
||||
// Nếu EditorRole rỗng, mọi thứ hoạt động bình thường
|
||||
if (string.IsNullOrWhiteSpace(EditorRole))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var userRoles = await GetCurrentUserRolesAsync();
|
||||
|
||||
// Kiểm tra nếu user có role = EditorRole
|
||||
if (userRoles.Contains(EditorRole, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Kiểm tra nếu role của user nằm trong Roles của variable
|
||||
if (!string.IsNullOrWhiteSpace(variable.Roles))
|
||||
{
|
||||
var variableRoles = variable.Roles.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(r => r.Trim())
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r));
|
||||
|
||||
return variableRoles.Any(role => userRoles.Contains(role, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy danh sách roles của user hiện tại
|
||||
/// </summary>
|
||||
private async Task<List<string>> GetCurrentUserRolesAsync()
|
||||
{
|
||||
if (_authStateProvider == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var authState = await _authStateProvider.GetAuthenticationStateAsync();
|
||||
var user = authState?.User;
|
||||
|
||||
if (user == null || user.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Lấy roles từ claims
|
||||
var roles = user.Claims
|
||||
.Where(c => c.Type == ClaimTypes.Role)
|
||||
.Select(c => c.Value)
|
||||
.ToList();
|
||||
|
||||
return roles;
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or NullReferenceException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Reload configs without acquiring the lock (for use inside locked methods)
|
||||
/// </summary>
|
||||
private async Task ReloadConfigsInternalAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Configs = await _apiService.GetAllConfigsAsync(SearchQuery);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = HttpErrorHelper.GetErrorMessage(ex);
|
||||
Configs = [];
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyStateChanged()
|
||||
{
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user