refactor(configmanager): simplify SecureStore UI with unified info view
Consolidate SecureStoreLockedFormView and SecureStoreUnlockedFormView into a single SecureStoreInfoFormView that displays store status and metadata. Simplifies MainWindowViewModel by removing redundant state management. Also adds design docs for RegexTransformer feature.
This commit is contained in:
@@ -37,10 +37,6 @@ 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>
|
||||
@@ -149,26 +145,11 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// </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>
|
||||
@@ -246,10 +227,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
// 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);
|
||||
@@ -300,69 +278,114 @@ public class MainWindowViewModel : ViewModelBase
|
||||
if (folder != null)
|
||||
{
|
||||
await LoadConfigAsync(folder);
|
||||
await EnsureDefaultSecureStoreAsync(folder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a default secure store exists and is loaded.
|
||||
/// Creates one if it doesn't exist.
|
||||
/// Initializes the SecureStore automatically on config load.
|
||||
/// Creates the store if it doesn't exist and AutoCreateStore is true.
|
||||
/// Opens the store and ensures all required keys exist.
|
||||
/// </summary>
|
||||
private async Task EnsureDefaultSecureStoreAsync(string configFolder)
|
||||
private async Task InitializeSecureStoreAsync()
|
||||
{
|
||||
var defaultStorePath = Path.Combine(configFolder, "default.secrets.json");
|
||||
var defaultKeyPath = Path.Combine(configFolder, "default.secrets.key");
|
||||
if (_appSettings?.SecureStore == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
|
||||
return;
|
||||
|
||||
var secureStoreConfig = _appSettings.SecureStore;
|
||||
|
||||
// Resolve paths relative to config folder
|
||||
var storePath = Path.IsPathRooted(secureStoreConfig.StorePath)
|
||||
? secureStoreConfig.StorePath
|
||||
: Path.Combine(ConfigFolderPath, secureStoreConfig.StorePath);
|
||||
|
||||
var keyFilePath = Path.IsPathRooted(secureStoreConfig.KeyFilePath)
|
||||
? secureStoreConfig.KeyFilePath
|
||||
: Path.Combine(ConfigFolderPath, secureStoreConfig.KeyFilePath);
|
||||
|
||||
try
|
||||
{
|
||||
// Create default store if it doesn't exist
|
||||
if (!File.Exists(defaultStorePath))
|
||||
if (!File.Exists(storePath))
|
||||
{
|
||||
_logger?.LogInformation("Creating default secure store at {Path}", defaultStorePath);
|
||||
if (!secureStoreConfig.AutoCreateStore)
|
||||
{
|
||||
_logger?.LogWarning("SecureStore not found and AutoCreateStore is false");
|
||||
return;
|
||||
}
|
||||
|
||||
_secureStoreManager.CreateStore(defaultStorePath, defaultKeyPath);
|
||||
// Create new store with keyfile
|
||||
_logger?.LogInformation("Creating SecureStore at {StorePath}", storePath);
|
||||
_secureStoreManager.CreateStore(storePath, keyFilePath);
|
||||
|
||||
// Add some example secrets
|
||||
_secureStoreManager.SetSecret("jde-password", "");
|
||||
_secureStoreManager.SetSecret("cms-password", "");
|
||||
_secureStoreManager.SetSecret("lotfinder-password", "");
|
||||
_secureStoreManager.Save();
|
||||
_secureStoreManager.CloseStore();
|
||||
// Update appsettings.json with actual paths
|
||||
secureStoreConfig.StorePath = GetRelativePath(ConfigFolderPath, storePath);
|
||||
secureStoreConfig.KeyFilePath = GetRelativePath(ConfigFolderPath, keyFilePath);
|
||||
await SaveAppSettingsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Open existing store
|
||||
if (!File.Exists(keyFilePath))
|
||||
{
|
||||
if (_dialogService != null)
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(
|
||||
"SecureStore Error",
|
||||
$"Key file not found: {keyFilePath}\n\nThe SecureStore cannot be opened without its key file.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Rebuild tree to show the new store
|
||||
BuildTreeNodes();
|
||||
_secureStoreManager.OpenStore(storePath, keyFilePath);
|
||||
}
|
||||
|
||||
// Auto-unlock the default store if key file exists
|
||||
if (File.Exists(defaultStorePath) && File.Exists(defaultKeyPath))
|
||||
// Ensure all required keys exist
|
||||
if (secureStoreConfig.RequiredKeys?.Count > 0)
|
||||
{
|
||||
// Find the default store node in the tree
|
||||
var secureStoresFolder = TreeNodes.FirstOrDefault(n => n.NodeType == TreeNodeType.SecureStoresFolder);
|
||||
var defaultStoreNode = secureStoresFolder?.Children.FirstOrDefault(n =>
|
||||
n.StorePath != null && n.StorePath.EndsWith("default.secrets.json"));
|
||||
|
||||
if (defaultStoreNode != null)
|
||||
var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys);
|
||||
if (addedKeys.Count > 0)
|
||||
{
|
||||
_secureStoreManager.OpenStore(defaultStorePath, defaultKeyPath);
|
||||
defaultStoreNode.IsUnlocked = true;
|
||||
_openStores[defaultStorePath] = defaultStoreNode;
|
||||
RefreshStoreChildren(defaultStoreNode);
|
||||
defaultStoreNode.IsExpanded = true;
|
||||
|
||||
_logger?.LogInformation("Auto-unlocked default secure store");
|
||||
_logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to initialize default secure store");
|
||||
_logger?.LogError(ex, "Failed to initialize SecureStore");
|
||||
if (_dialogService != null)
|
||||
{
|
||||
await _dialogService.ShowMessageAsync(
|
||||
"SecureStore Error",
|
||||
$"Failed to initialize SecureStore:\n\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a folder picker dialog to select a configuration folder.
|
||||
/// Gets the relative path from a base path to a full path.
|
||||
/// </summary>
|
||||
private static string GetRelativePath(string basePath, string fullPath)
|
||||
{
|
||||
var baseUri = new Uri(basePath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
|
||||
var fullUri = new Uri(fullPath);
|
||||
return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fullUri).ToString().Replace('/', Path.DirectorySeparatorChar));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the appsettings.json file.
|
||||
/// </summary>
|
||||
private async Task SaveAppSettingsAsync()
|
||||
{
|
||||
if (_appSettings == null) return;
|
||||
|
||||
var appSettingsPath = Path.Combine(ConfigFolderPath, "appsettings.json");
|
||||
await _configFileService.SaveAppSettingsAsync(appSettingsPath, _appSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file picker dialog to select a configuration file.
|
||||
/// </summary>
|
||||
private async Task OpenFolderAsync()
|
||||
{
|
||||
@@ -372,10 +395,14 @@ public class MainWindowViewModel : ViewModelBase
|
||||
return;
|
||||
}
|
||||
|
||||
var folder = await _dialogService.ShowFolderPickerAsync("Select Configuration Folder");
|
||||
if (folder != null)
|
||||
var filePath = await _dialogService.ShowFilePickerAsync("Select Configuration File");
|
||||
if (filePath != null)
|
||||
{
|
||||
await LoadConfigAsync(folder);
|
||||
var folder = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(folder))
|
||||
{
|
||||
await LoadConfigAsync(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,15 +417,20 @@ public class MainWindowViewModel : ViewModelBase
|
||||
ConfigFolderPath = folderPath;
|
||||
|
||||
var appSettingsPath = Path.Combine(folderPath, "appsettings.json");
|
||||
var pipelinesPath = Path.Combine(folderPath, "Pipelines", "pipelines.json");
|
||||
|
||||
_appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath);
|
||||
|
||||
// Use config-driven pipeline path
|
||||
var pipelinesConfigPath = _appSettings?.Pipelines?.ConfigPath ?? "Pipelines/pipelines.json";
|
||||
var pipelinesPath = Path.Combine(folderPath, pipelinesConfigPath);
|
||||
|
||||
if (File.Exists(pipelinesPath))
|
||||
{
|
||||
_pipelines = await _configFileService.LoadPipelinesAsync(pipelinesPath);
|
||||
}
|
||||
|
||||
// Initialize SecureStore (auto-create if needed, open, sync required keys)
|
||||
await InitializeSecureStoreAsync();
|
||||
|
||||
BuildTreeNodes();
|
||||
Validate();
|
||||
|
||||
@@ -438,48 +470,55 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
TreeNodes.Add(pipelinesFolder);
|
||||
|
||||
// Secure Stores folder
|
||||
var secureStoresFolder = new TreeNodeViewModel("Secure Stores", "🔑", TreeNodeType.SecureStoresFolder) { IsExpanded = true };
|
||||
DiscoverSecureStores(secureStoresFolder);
|
||||
TreeNodes.Add(secureStoresFolder);
|
||||
// Secure Store node (single store, secrets as direct children)
|
||||
var secureStoreNode = CreateSecureStoreNode();
|
||||
if (secureStoreNode != null)
|
||||
{
|
||||
TreeNodes.Add(secureStoreNode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers existing secure store files in the configuration folder.
|
||||
/// Creates the SecureStore tree node with secrets as direct children.
|
||||
/// </summary>
|
||||
/// <param name="parentNode">The parent tree node to add discovered stores to.</param>
|
||||
private void DiscoverSecureStores(TreeNodeViewModel parentNode)
|
||||
private TreeNodeViewModel? CreateSecureStoreNode()
|
||||
{
|
||||
if (_appSettings?.SecureStore == null)
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
|
||||
return;
|
||||
return null;
|
||||
|
||||
try
|
||||
var storePath = Path.IsPathRooted(_appSettings.SecureStore.StorePath)
|
||||
? _appSettings.SecureStore.StorePath
|
||||
: Path.Combine(ConfigFolderPath, _appSettings.SecureStore.StorePath);
|
||||
|
||||
var keyFilePath = Path.IsPathRooted(_appSettings.SecureStore.KeyFilePath)
|
||||
? _appSettings.SecureStore.KeyFilePath
|
||||
: Path.Combine(ConfigFolderPath, _appSettings.SecureStore.KeyFilePath);
|
||||
|
||||
var storeNode = new TreeNodeViewModel("Secure Store", "🔑", TreeNodeType.SecureStore)
|
||||
{
|
||||
// Look for *.secrets.json files in the config folder
|
||||
var secretsFiles = Directory.GetFiles(ConfigFolderPath, "*.secrets.json", SearchOption.TopDirectoryOnly);
|
||||
StorePath = storePath,
|
||||
KeyFilePath = keyFilePath,
|
||||
SectionKey = storePath,
|
||||
IsExpanded = true
|
||||
};
|
||||
|
||||
foreach (var filePath in secretsFiles)
|
||||
// Add secrets as direct children if store is open
|
||||
if (_secureStoreManager.IsStoreOpen)
|
||||
{
|
||||
foreach (var key in _secureStoreManager.GetKeys().OrderBy(k => k))
|
||||
{
|
||||
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, "🔒", TreeNodeType.SecureStore)
|
||||
var secretNode = new TreeNodeViewModel(key, "🔐", TreeNodeType.Secret)
|
||||
{
|
||||
StorePath = filePath,
|
||||
SectionKey = filePath,
|
||||
IsUnlocked = false
|
||||
SecretKey = key
|
||||
};
|
||||
parentNode.Children.Add(storeNode);
|
||||
storeNode.Children.Add(secretNode);
|
||||
}
|
||||
}
|
||||
|
||||
_logger?.LogDebug("Discovered {Count} secure store files", secretsFiles.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to discover secure store files");
|
||||
}
|
||||
return storeNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -492,34 +531,18 @@ public class MainWindowViewModel : ViewModelBase
|
||||
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);
|
||||
}
|
||||
SelectedFormViewModel = CreateSecureStoreInfoFormViewModel();
|
||||
RaiseSecureStoreCommandsCanExecuteChanged();
|
||||
return;
|
||||
|
||||
case TreeNodeType.Secret:
|
||||
// Find the parent store node
|
||||
_selectedStoreNode = FindParentStoreNode(_selectedNode);
|
||||
SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode);
|
||||
RaiseSecureStoreCommandsCanExecuteChanged();
|
||||
return;
|
||||
@@ -529,11 +552,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||
if (_appSettings == null)
|
||||
{
|
||||
SelectedFormViewModel = null;
|
||||
_selectedStoreNode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedStoreNode = null;
|
||||
SelectedFormViewModel = _selectedNode.SectionKey switch
|
||||
{
|
||||
"DataSync" => new DataSyncFormViewModel(_appSettings.DataSync, MarkAsChanged),
|
||||
@@ -552,39 +573,15 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a form view model for a locked secure store.
|
||||
/// Creates a form view model for the SecureStore info panel.
|
||||
/// </summary>
|
||||
private SecureStoreLockedFormViewModel CreateLockedStoreFormViewModel(TreeNodeViewModel storeNode)
|
||||
private SecureStoreInfoFormViewModel CreateSecureStoreInfoFormViewModel()
|
||||
{
|
||||
DateTime? lastModified = null;
|
||||
if (!string.IsNullOrEmpty(storeNode.StorePath) && File.Exists(storeNode.StorePath))
|
||||
{
|
||||
lastModified = File.GetLastWriteTime(storeNode.StorePath);
|
||||
}
|
||||
var storePath = _appSettings?.SecureStore?.StorePath ?? "Unknown";
|
||||
var keyFilePath = _appSettings?.SecureStore?.KeyFilePath ?? "Unknown";
|
||||
var secretCount = _secureStoreManager.IsStoreOpen ? _secureStoreManager.GetKeys().Count : 0;
|
||||
|
||||
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());
|
||||
return new SecureStoreInfoFormViewModel(storePath, keyFilePath, secretCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -628,34 +625,12 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
@@ -705,7 +680,8 @@ public class MainWindowViewModel : ViewModelBase
|
||||
// Save pipelines if loaded
|
||||
if (_pipelines != null)
|
||||
{
|
||||
var pipelinesPath = Path.Combine(ConfigFolderPath, "Pipelines", "pipelines.json");
|
||||
var pipelinesConfigPath = _appSettings?.Pipelines?.ConfigPath ?? "Pipelines/pipelines.json";
|
||||
var pipelinesPath = Path.Combine(ConfigFolderPath, pipelinesConfigPath);
|
||||
if (File.Exists(pipelinesPath))
|
||||
{
|
||||
await _backupService.CreateBackupAsync(pipelinesPath);
|
||||
@@ -932,33 +908,12 @@ public class MainWindowViewModel : ViewModelBase
|
||||
|
||||
#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
|
||||
return _secureStoreManager.IsStoreOpen
|
||||
&& _secureStoreManager.HasUnsavedChanges;
|
||||
}
|
||||
|
||||
@@ -967,8 +922,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private bool CanAddSecret()
|
||||
{
|
||||
return _selectedStoreNode != null
|
||||
&& _selectedStoreNode.IsUnlocked;
|
||||
return _secureStoreManager.IsStoreOpen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -978,8 +932,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
return _selectedNode != null
|
||||
&& _selectedNode.NodeType == TreeNodeType.Secret
|
||||
&& _selectedStoreNode != null
|
||||
&& _selectedStoreNode.IsUnlocked;
|
||||
&& _secureStoreManager.IsStoreOpen;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1018,105 +971,6 @@ public class MainWindowViewModel : ViewModelBase
|
||||
"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>
|
||||
@@ -1147,21 +1001,6 @@ public class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -1184,7 +1023,7 @@ public class MainWindowViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
private async Task AddSecretAsync()
|
||||
{
|
||||
if (_selectedStoreNode == null || !_selectedStoreNode.IsUnlocked)
|
||||
if (!_secureStoreManager.IsStoreOpen)
|
||||
return;
|
||||
|
||||
if (_dialogService == null)
|
||||
@@ -1227,13 +1066,14 @@ public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
_secureStoreManager.RemoveSecret(_selectedNode.SecretKey);
|
||||
|
||||
// Remove from tree
|
||||
if (_selectedStoreNode != null)
|
||||
// Find the Secure Store node and remove the secret from its children
|
||||
var secureStoreNode = TreeNodes.FirstOrDefault(n => n.NodeType == TreeNodeType.SecureStore);
|
||||
if (secureStoreNode != null)
|
||||
{
|
||||
_selectedStoreNode.Children.Remove(_selectedNode);
|
||||
secureStoreNode.Children.Remove(_selectedNode);
|
||||
|
||||
// Select the parent store node
|
||||
SelectedNode = _selectedStoreNode;
|
||||
SelectedNode = secureStoreNode;
|
||||
}
|
||||
|
||||
_logger?.LogInformation("Secret deleted: {Key}", _selectedNode.SecretKey);
|
||||
|
||||
Reference in New Issue
Block a user