feat(configmanager): integrate SecureStore for credential management
Add SecureStore integration to ConfigManager for secure handling of connection strings and sensitive configuration values. Includes store/secret management UI, encrypted .store file support, and comprehensive test coverage.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using Avalonia.Media;
|
||||
using JdeScoping.ConfigManager.Constants;
|
||||
using JdeScoping.ConfigManager.Models;
|
||||
using JdeScoping.ConfigManager.Services;
|
||||
using JdeScoping.ConfigManager.Services.SecureStore;
|
||||
using JdeScoping.ConfigManager.ViewModels.Dialogs;
|
||||
using JdeScoping.ConfigManager.ViewModels.Forms;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -19,6 +22,8 @@ public class MainWindowViewModel : ViewModelBase
|
||||
private readonly IBackupService _backupService;
|
||||
private readonly IAutoDiscoveryService _autoDiscoveryService;
|
||||
private readonly IDialogService? _dialogService;
|
||||
private readonly ISecureStoreManager _secureStoreManager;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly ILogger<MainWindowViewModel>? _logger;
|
||||
|
||||
private string _configFolderPath = "No folder selected";
|
||||
@@ -31,6 +36,10 @@ public class MainWindowViewModel : ViewModelBase
|
||||
private ConfigModel? _appSettings;
|
||||
private PipelinesConfigModel? _pipelines;
|
||||
|
||||
// SecureStore state tracking
|
||||
private readonly Dictionary<string, TreeNodeViewModel> _openStores = new();
|
||||
private TreeNodeViewModel? _selectedStoreNode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently loaded configuration folder path.
|
||||
/// </summary>
|
||||
@@ -129,6 +138,51 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
public ICommand TestConnectionCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for creating a new secure store.
|
||||
/// </summary>
|
||||
public ICommand NewStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for adding an existing secure store.
|
||||
/// </summary>
|
||||
public ICommand AddExistingStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for unlocking a secure store.
|
||||
/// </summary>
|
||||
public ICommand UnlockStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for locking a secure store.
|
||||
/// </summary>
|
||||
public ICommand LockStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for saving a secure store.
|
||||
/// </summary>
|
||||
public ICommand SaveStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for locking all open secure stores.
|
||||
/// </summary>
|
||||
public ICommand LockAllStoresCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for generating a new key file.
|
||||
/// </summary>
|
||||
public ICommand GenerateKeyFileCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for adding a secret to the current store.
|
||||
/// </summary>
|
||||
public ICommand AddSecretCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command for deleting a secret from the current store.
|
||||
/// </summary>
|
||||
public ICommand DeleteSecretCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
|
||||
/// </summary>
|
||||
@@ -138,6 +192,8 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// <param name="backupService">Service for creating configuration backups.</param>
|
||||
/// <param name="autoDiscoveryService">Service for discovering configuration folder locations.</param>
|
||||
/// <param name="dialogService">Service for showing platform dialogs.</param>
|
||||
/// <param name="secureStoreManager">Service for managing encrypted secret stores.</param>
|
||||
/// <param name="clipboardService">Service for clipboard operations.</param>
|
||||
/// <param name="logger">Optional logger for recording view model activities.</param>
|
||||
public MainWindowViewModel(
|
||||
IFileSystem fileSystem,
|
||||
@@ -146,6 +202,8 @@ public class MainWindowViewModel : ViewModelBase
|
||||
IBackupService backupService,
|
||||
IAutoDiscoveryService autoDiscoveryService,
|
||||
IDialogService? dialogService,
|
||||
ISecureStoreManager secureStoreManager,
|
||||
IClipboardService clipboardService,
|
||||
ILogger<MainWindowViewModel>? logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
@@ -154,6 +212,8 @@ public class MainWindowViewModel : ViewModelBase
|
||||
_backupService = backupService;
|
||||
_autoDiscoveryService = autoDiscoveryService;
|
||||
_dialogService = dialogService;
|
||||
_secureStoreManager = secureStoreManager;
|
||||
_clipboardService = clipboardService;
|
||||
_logger = logger;
|
||||
|
||||
OpenFolderCommand = new AsyncRelayCommand(OpenFolderAsync);
|
||||
@@ -164,6 +224,17 @@ public class MainWindowViewModel : ViewModelBase
|
||||
ValidateCommand = new RelayCommand(Validate);
|
||||
TestConnectionCommand = new AsyncRelayCommand(TestConnectionAsync);
|
||||
|
||||
// SecureStore commands
|
||||
NewStoreCommand = new AsyncRelayCommand(NewStoreAsync);
|
||||
AddExistingStoreCommand = new AsyncRelayCommand(AddExistingStoreAsync);
|
||||
UnlockStoreCommand = new AsyncRelayCommand(UnlockStoreAsync, CanUnlockStore);
|
||||
LockStoreCommand = new RelayCommand(LockStore, CanLockStore);
|
||||
SaveStoreCommand = new AsyncRelayCommand(SaveStoreAsync, CanSaveStore);
|
||||
LockAllStoresCommand = new RelayCommand(LockAllStores, () => _openStores.Count > 0);
|
||||
GenerateKeyFileCommand = new AsyncRelayCommand(GenerateKeyFileAsync);
|
||||
AddSecretCommand = new AsyncRelayCommand(AddSecretAsync, CanAddSecret);
|
||||
DeleteSecretCommand = new AsyncRelayCommand(DeleteSecretAsync, CanDeleteSecret);
|
||||
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
@@ -177,10 +248,20 @@ public class MainWindowViewModel : ViewModelBase
|
||||
new BackupService(new FileSystem()),
|
||||
new AutoDiscoveryService(new FileSystem()),
|
||||
null,
|
||||
new SecureStoreManager(),
|
||||
new NullClipboardService(),
|
||||
null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null implementation of clipboard service for design-time.
|
||||
/// </summary>
|
||||
private class NullClipboardService : IClipboardService
|
||||
{
|
||||
public Task SetTextAsync(string text) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the view model by auto-discovering and loading configuration.
|
||||
/// </summary>
|
||||
@@ -269,6 +350,49 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
TreeNodes.Add(pipelinesFolder);
|
||||
|
||||
// Secure Stores folder
|
||||
var secureStoresFolder = new TreeNodeViewModel("Secure Stores", "key", TreeNodeType.SecureStoresFolder) { IsExpanded = true };
|
||||
DiscoverSecureStores(secureStoresFolder);
|
||||
TreeNodes.Add(secureStoresFolder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers existing secure store files in the configuration folder.
|
||||
/// </summary>
|
||||
/// <param name="parentNode">The parent tree node to add discovered stores to.</param>
|
||||
private void DiscoverSecureStores(TreeNodeViewModel parentNode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Look for *.secrets.json files in the config folder
|
||||
var secretsFiles = Directory.GetFiles(ConfigFolderPath, "*.secrets.json", SearchOption.TopDirectoryOnly);
|
||||
|
||||
foreach (var filePath in secretsFiles)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var storeName = Path.GetFileNameWithoutExtension(fileName);
|
||||
if (storeName.EndsWith(".secrets"))
|
||||
storeName = storeName[..^8]; // Remove ".secrets" suffix for display
|
||||
|
||||
var storeNode = new TreeNodeViewModel(storeName, "lock", TreeNodeType.SecureStore)
|
||||
{
|
||||
StorePath = filePath,
|
||||
SectionKey = filePath,
|
||||
IsUnlocked = false
|
||||
};
|
||||
parentNode.Children.Add(storeNode);
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Discovered {Count} secure store files", secretsFiles.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to discover secure store files");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -276,12 +400,51 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private void OnSelectedNodeChanged()
|
||||
{
|
||||
if (_selectedNode == null || _appSettings == null)
|
||||
if (_selectedNode == null)
|
||||
{
|
||||
SelectedFormViewModel = null;
|
||||
_selectedStoreNode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle SecureStore-related node types first
|
||||
switch (_selectedNode.NodeType)
|
||||
{
|
||||
case TreeNodeType.SecureStoresFolder:
|
||||
SelectedFormViewModel = null; // Show empty state or instructions
|
||||
_selectedStoreNode = null;
|
||||
return;
|
||||
|
||||
case TreeNodeType.SecureStore:
|
||||
_selectedStoreNode = _selectedNode;
|
||||
if (_selectedNode.IsUnlocked)
|
||||
{
|
||||
SelectedFormViewModel = CreateUnlockedStoreFormViewModel(_selectedNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedFormViewModel = CreateLockedStoreFormViewModel(_selectedNode);
|
||||
}
|
||||
RaiseSecureStoreCommandsCanExecuteChanged();
|
||||
return;
|
||||
|
||||
case TreeNodeType.Secret:
|
||||
// Find the parent store node
|
||||
_selectedStoreNode = FindParentStoreNode(_selectedNode);
|
||||
SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode);
|
||||
RaiseSecureStoreCommandsCanExecuteChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle standard configuration sections
|
||||
if (_appSettings == null)
|
||||
{
|
||||
SelectedFormViewModel = null;
|
||||
_selectedStoreNode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedStoreNode = null;
|
||||
SelectedFormViewModel = _selectedNode.SectionKey switch
|
||||
{
|
||||
"DataSync" => new DataSyncFormViewModel(_appSettings.DataSync, MarkAsChanged),
|
||||
@@ -296,6 +459,116 @@ public class MainWindowViewModel : ViewModelBase
|
||||
: null,
|
||||
_ => null
|
||||
};
|
||||
RaiseSecureStoreCommandsCanExecuteChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a form view model for a locked secure store.
|
||||
/// </summary>
|
||||
private SecureStoreLockedFormViewModel CreateLockedStoreFormViewModel(TreeNodeViewModel storeNode)
|
||||
{
|
||||
DateTime? lastModified = null;
|
||||
if (!string.IsNullOrEmpty(storeNode.StorePath) && File.Exists(storeNode.StorePath))
|
||||
{
|
||||
lastModified = File.GetLastWriteTime(storeNode.StorePath);
|
||||
}
|
||||
|
||||
return new SecureStoreLockedFormViewModel(
|
||||
storeNode.Name,
|
||||
storeNode.StorePath ?? string.Empty,
|
||||
lastModified,
|
||||
() => _ = UnlockStoreAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a form view model for an unlocked secure store.
|
||||
/// </summary>
|
||||
private SecureStoreUnlockedFormViewModel CreateUnlockedStoreFormViewModel(TreeNodeViewModel storeNode)
|
||||
{
|
||||
var secretCount = storeNode.Children.Count;
|
||||
var hasUnsavedChanges = _secureStoreManager.HasUnsavedChanges;
|
||||
|
||||
return new SecureStoreUnlockedFormViewModel(
|
||||
storeNode.Name,
|
||||
storeNode.StorePath ?? string.Empty,
|
||||
secretCount,
|
||||
hasUnsavedChanges,
|
||||
() => LockStore(),
|
||||
() => _ = AddSecretAsync(),
|
||||
() => _ = SaveStoreAsync());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a form view model for a secret.
|
||||
/// </summary>
|
||||
private SecretFormViewModel? CreateSecretFormViewModel(TreeNodeViewModel secretNode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(secretNode.SecretKey))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var value = _secureStoreManager.GetSecret(secretNode.SecretKey);
|
||||
return new SecretFormViewModel(
|
||||
secretNode.SecretKey,
|
||||
value,
|
||||
_clipboardService,
|
||||
newValue => OnSecretValueChanged(secretNode.SecretKey, newValue),
|
||||
() => _ = DeleteSecretAsync());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to get secret value for key {Key}", secretNode.SecretKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a secret value changes in the form.
|
||||
/// </summary>
|
||||
private void OnSecretValueChanged(string key, string newValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
_secureStoreManager.SetSecret(key, newValue);
|
||||
_logger?.LogDebug("Secret {Key} value updated", key);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to update secret {Key}", key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the parent store node for a secret node.
|
||||
/// </summary>
|
||||
private TreeNodeViewModel? FindParentStoreNode(TreeNodeViewModel secretNode)
|
||||
{
|
||||
foreach (var rootNode in TreeNodes)
|
||||
{
|
||||
if (rootNode.NodeType == TreeNodeType.SecureStoresFolder)
|
||||
{
|
||||
foreach (var storeNode in rootNode.Children)
|
||||
{
|
||||
if (storeNode.Children.Contains(secretNode))
|
||||
return storeNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises CanExecuteChanged for all SecureStore commands.
|
||||
/// </summary>
|
||||
private void RaiseSecureStoreCommandsCanExecuteChanged()
|
||||
{
|
||||
(UnlockStoreCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(LockStoreCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(SaveStoreCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(LockAllStoresCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(AddSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -408,4 +681,354 @@ public class MainWindowViewModel : ViewModelBase
|
||||
_logger?.LogInformation("Test connection requested");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region SecureStore Commands
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a store can be unlocked.
|
||||
/// </summary>
|
||||
private bool CanUnlockStore()
|
||||
{
|
||||
return _selectedStoreNode != null
|
||||
&& _selectedStoreNode.NodeType == TreeNodeType.SecureStore
|
||||
&& !_selectedStoreNode.IsUnlocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a store can be locked.
|
||||
/// </summary>
|
||||
private bool CanLockStore()
|
||||
{
|
||||
return _selectedStoreNode != null
|
||||
&& _selectedStoreNode.NodeType == TreeNodeType.SecureStore
|
||||
&& _selectedStoreNode.IsUnlocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the current store can be saved.
|
||||
/// </summary>
|
||||
private bool CanSaveStore()
|
||||
{
|
||||
return _selectedStoreNode != null
|
||||
&& _selectedStoreNode.IsUnlocked
|
||||
&& _secureStoreManager.HasUnsavedChanges;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a secret can be added.
|
||||
/// </summary>
|
||||
private bool CanAddSecret()
|
||||
{
|
||||
return _selectedStoreNode != null
|
||||
&& _selectedStoreNode.IsUnlocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a secret can be deleted.
|
||||
/// </summary>
|
||||
private bool CanDeleteSecret()
|
||||
{
|
||||
return _selectedNode != null
|
||||
&& _selectedNode.NodeType == TreeNodeType.Secret
|
||||
&& _selectedStoreNode != null
|
||||
&& _selectedStoreNode.IsUnlocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new secure store.
|
||||
/// </summary>
|
||||
private async Task NewStoreAsync()
|
||||
{
|
||||
if (_dialogService == null)
|
||||
{
|
||||
_logger?.LogWarning("Dialog service is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: In a full implementation, this would show the NewStoreDialog
|
||||
// For now, we use a simple message dialog to inform the user
|
||||
// A proper implementation would wire up the NewStoreDialogViewModel events
|
||||
await _dialogService.ShowMessageAsync(
|
||||
"New Store",
|
||||
"New store creation dialog not yet implemented. Use Add Existing Store instead.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an existing secure store to the tree.
|
||||
/// </summary>
|
||||
private async Task AddExistingStoreAsync()
|
||||
{
|
||||
if (_dialogService == null)
|
||||
{
|
||||
_logger?.LogWarning("Dialog service is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: In a full implementation, this would show a file picker
|
||||
await _dialogService.ShowMessageAsync(
|
||||
"Add Existing Store",
|
||||
"File picker for existing stores not yet implemented.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks the currently selected secure store.
|
||||
/// </summary>
|
||||
private async Task UnlockStoreAsync()
|
||||
{
|
||||
if (_selectedStoreNode == null || string.IsNullOrEmpty(_selectedStoreNode.StorePath))
|
||||
return;
|
||||
|
||||
if (_dialogService == null)
|
||||
{
|
||||
_logger?.LogWarning("Dialog service is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: In a full implementation, this would show the UnlockStoreDialog
|
||||
// For now, we simulate with a simple confirmation
|
||||
var confirmed = await _dialogService.ShowConfirmationAsync(
|
||||
"Unlock Store",
|
||||
$"Enter credentials to unlock '{_selectedStoreNode.Name}'.\n\n(Dialog not yet implemented - this is a placeholder)");
|
||||
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
// In a real implementation, we would get the key file path or password from the dialog
|
||||
// For now, we look for a .key file with the same name
|
||||
var keyFilePath = Path.ChangeExtension(_selectedStoreNode.StorePath, ".key");
|
||||
if (!File.Exists(keyFilePath))
|
||||
{
|
||||
// Try alternate pattern: storename.secrets.key
|
||||
keyFilePath = _selectedStoreNode.StorePath.Replace(".secrets.json", ".secrets.key");
|
||||
}
|
||||
|
||||
if (!File.Exists(keyFilePath))
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(
|
||||
SecureStoreStrings.ErrorTitle,
|
||||
$"Key file not found. Expected at:\n{keyFilePath}");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_secureStoreManager.OpenStore(_selectedStoreNode.StorePath, keyFilePath);
|
||||
_selectedStoreNode.IsUnlocked = true;
|
||||
_openStores[_selectedStoreNode.StorePath] = _selectedStoreNode;
|
||||
|
||||
// Populate secret children
|
||||
RefreshStoreChildren(_selectedStoreNode);
|
||||
_selectedStoreNode.IsExpanded = true;
|
||||
|
||||
// Refresh the form view
|
||||
OnSelectedNodeChanged();
|
||||
|
||||
_logger?.LogInformation("Store unlocked: {StorePath}", _selectedStoreNode.StorePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to unlock store: {StorePath}", _selectedStoreNode.StorePath);
|
||||
await _dialogService.ShowMessageAsync(
|
||||
SecureStoreStrings.ErrorTitle,
|
||||
string.Format(SecureStoreStrings.FailedToOpenStoreFormat, ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks the currently selected secure store.
|
||||
/// </summary>
|
||||
private void LockStore()
|
||||
{
|
||||
if (_selectedStoreNode == null || !_selectedStoreNode.IsUnlocked)
|
||||
return;
|
||||
|
||||
LockStoreInternal(_selectedStoreNode);
|
||||
OnSelectedNodeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method to lock a store and clean up.
|
||||
/// </summary>
|
||||
private void LockStoreInternal(TreeNodeViewModel storeNode)
|
||||
{
|
||||
if (storeNode.StorePath != null)
|
||||
{
|
||||
_openStores.Remove(storeNode.StorePath);
|
||||
}
|
||||
|
||||
// Check if this is the currently open store in the manager
|
||||
if (_secureStoreManager.IsStoreOpen &&
|
||||
_secureStoreManager.CurrentStorePath == storeNode.StorePath)
|
||||
{
|
||||
_secureStoreManager.CloseStore();
|
||||
}
|
||||
|
||||
storeNode.IsUnlocked = false;
|
||||
storeNode.Children.Clear();
|
||||
|
||||
_logger?.LogInformation("Store locked: {StorePath}", storeNode.StorePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the currently open secure store.
|
||||
/// </summary>
|
||||
private async Task SaveStoreAsync()
|
||||
{
|
||||
if (!_secureStoreManager.IsStoreOpen)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_secureStoreManager.Save();
|
||||
|
||||
// Refresh the form to update the HasUnsavedChanges state
|
||||
OnSelectedNodeChanged();
|
||||
RaiseSecureStoreCommandsCanExecuteChanged();
|
||||
|
||||
_logger?.LogInformation("Store saved: {StorePath}", _secureStoreManager.CurrentStorePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to save store");
|
||||
if (_dialogService != null)
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(
|
||||
SecureStoreStrings.ErrorTitle,
|
||||
string.Format(SecureStoreStrings.FailedToSaveStoreFormat, ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks all open secure stores.
|
||||
/// </summary>
|
||||
private void LockAllStores()
|
||||
{
|
||||
var openStoresCopy = _openStores.Values.ToList();
|
||||
foreach (var storeNode in openStoresCopy)
|
||||
{
|
||||
LockStoreInternal(storeNode);
|
||||
}
|
||||
|
||||
OnSelectedNodeChanged();
|
||||
_logger?.LogInformation("All stores locked");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new key file.
|
||||
/// </summary>
|
||||
private async Task GenerateKeyFileAsync()
|
||||
{
|
||||
if (_dialogService == null)
|
||||
{
|
||||
_logger?.LogWarning("Dialog service is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: In a full implementation, this would show a save file dialog
|
||||
await _dialogService.ShowMessageAsync(
|
||||
SecureStoreStrings.GenerateKeyFileTitle,
|
||||
"Key file generation dialog not yet implemented.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new secret to the current store.
|
||||
/// </summary>
|
||||
private async Task AddSecretAsync()
|
||||
{
|
||||
if (_selectedStoreNode == null || !_selectedStoreNode.IsUnlocked)
|
||||
return;
|
||||
|
||||
if (_dialogService == null)
|
||||
{
|
||||
_logger?.LogWarning("Dialog service is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: In a full implementation, this would show the SecretEditDialog
|
||||
// For demonstration, we'll show a message
|
||||
await _dialogService.ShowMessageAsync(
|
||||
"Add Secret",
|
||||
"Secret edit dialog not yet implemented.\n\nTo add secrets, the SecretEditDialog must be wired up.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the currently selected secret.
|
||||
/// </summary>
|
||||
private async Task DeleteSecretAsync()
|
||||
{
|
||||
if (_selectedNode == null ||
|
||||
_selectedNode.NodeType != TreeNodeType.Secret ||
|
||||
string.IsNullOrEmpty(_selectedNode.SecretKey))
|
||||
return;
|
||||
|
||||
if (_dialogService == null)
|
||||
{
|
||||
_logger?.LogWarning("Dialog service is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmed = await _dialogService.ShowConfirmationAsync(
|
||||
SecureStoreStrings.ConfirmDeleteTitle,
|
||||
string.Format(SecureStoreStrings.ConfirmDeleteFormat, _selectedNode.SecretKey));
|
||||
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_secureStoreManager.RemoveSecret(_selectedNode.SecretKey);
|
||||
|
||||
// Remove from tree
|
||||
if (_selectedStoreNode != null)
|
||||
{
|
||||
_selectedStoreNode.Children.Remove(_selectedNode);
|
||||
|
||||
// Select the parent store node
|
||||
SelectedNode = _selectedStoreNode;
|
||||
}
|
||||
|
||||
_logger?.LogInformation("Secret deleted: {Key}", _selectedNode.SecretKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to delete secret: {Key}", _selectedNode.SecretKey);
|
||||
await _dialogService.ShowMessageAsync(
|
||||
SecureStoreStrings.ErrorTitle,
|
||||
string.Format(SecureStoreStrings.FailedToDeleteSecretFormat, ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the secret children of a store node.
|
||||
/// </summary>
|
||||
private void RefreshStoreChildren(TreeNodeViewModel storeNode)
|
||||
{
|
||||
storeNode.Children.Clear();
|
||||
|
||||
if (!_secureStoreManager.IsStoreOpen)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var keys = _secureStoreManager.GetKeys();
|
||||
foreach (var key in keys.OrderBy(k => k))
|
||||
{
|
||||
var secretNode = new TreeNodeViewModel(key, "key", TreeNodeType.Secret)
|
||||
{
|
||||
SecretKey = key,
|
||||
SectionKey = key
|
||||
};
|
||||
storeNode.Children.Add(secretNode);
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Refreshed {Count} secrets for store", keys.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Failed to refresh store children");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user