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.
16 KiB
Implementation Plan: SecureStore Auto-Initialization & Tree Flattening
Overview
This plan implements automatic SecureStore initialization on config load and flattens the tree hierarchy from "Secure Stores > secrets > [keys]" to "Secure Store > [keys]".
Design Decisions (from brainstorming)
- Auto-creation: Fully automatic on config folder load when
AutoCreateStore=true - Path updates: Write actual paths back to appsettings.json after creating files
- Tree structure: Single "Secure Store" node with secrets as direct children
- Authentication: Keyfile-only (remove password option entirely)
- Lock concept: Removed - store is always open while config folder is loaded
- Icon: Key icon (🔑) for "Secure Store" node
- Right panel: Show instructions "Select a secret to edit" when store node selected
- Error handling: Show error dialog if keyfile missing/corrupted
Task 1: Update TreeNodeType enum
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs
Changes:
- Remove
SecureStoresFolderenum value (no longer needed) - Keep
SecureStorefor the single store node - Keep
Secretfor individual secrets
Before (lines 5-13):
public enum TreeNodeType
{
Folder,
SettingsSection,
Pipeline,
SecureStoresFolder, // The "Secure Stores" folder
SecureStore, // Individual store files
Secret // Individual secrets within a store
}
After:
public enum TreeNodeType
{
Folder,
SettingsSection,
Pipeline,
SecureStore, // The single secure store node
Secret // Individual secrets within a store
}
Task 2: Remove lock-related properties from TreeNodeViewModel
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs
Remove these properties (lines 55-79):
IsUnlockedproperty and backing fieldIsLockedcomputed propertyLockIconcomputed property
Keep:
StorePathandKeyFilePath(needed for store identification)SecretKey(needed for secret nodes)
Task 3: Remove password-related methods from ISecureStoreManager
File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs
Remove these methods:
CreateStoreWithPassword(string storePath, string password)(lines 30-35)OpenStoreWithPassword(string storePath, string password)(lines 44-49)
Task 4: Remove password-related methods from SecureStoreManager
File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs
Remove these methods:
CreateStoreWithPassword()(lines 75-96)OpenStoreWithPassword()(lines 120-140)
Task 5: Add EnsureRequiredKeys method to ISecureStoreManager
File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs
Add new method:
/// <summary>
/// Ensures all required keys exist in the store, creating blank values for any missing keys.
/// </summary>
/// <param name="requiredKeys">List of keys that must exist.</param>
/// <returns>List of keys that were added.</returns>
IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys);
Task 6: Implement EnsureRequiredKeys in SecureStoreManager
File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs
Add implementation:
/// <inheritdoc />
public IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys)
{
ThrowIfDisposed();
if (_secretsManager == null)
throw new InvalidOperationException("No store is currently open.");
var addedKeys = new List<string>();
foreach (var key in requiredKeys)
{
if (!_keys.Contains(key))
{
_logger.LogInformation("Adding missing required key: {Key}", key);
SetSecret(key, string.Empty);
addedKeys.Add(key);
}
}
if (addedKeys.Count > 0)
{
Save();
}
return addedKeys.AsReadOnly();
}
Task 7: Delete locked/unlocked form files
Delete these files entirely:
ViewModels:
NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreLockedFormViewModel.csNEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreUnlockedFormViewModel.cs
Views:
NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axamlNEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml.csNEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axamlNEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml.cs
Task 8: Create SecureStoreInfoFormViewModel
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreInfoFormViewModel.cs (new file)
Content:
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// View model for the SecureStore info panel shown when the store node is selected.
/// </summary>
public class SecureStoreInfoFormViewModel : ViewModelBase
{
/// <summary>
/// Gets the instruction text to display.
/// </summary>
public string InstructionText => "Select a secret from the tree to view or edit its value.";
/// <summary>
/// Gets the store path for display.
/// </summary>
public string StorePath { get; }
/// <summary>
/// Gets the key file path for display.
/// </summary>
public string KeyFilePath { get; }
/// <summary>
/// Gets the number of secrets in the store.
/// </summary>
public int SecretCount { get; }
public SecureStoreInfoFormViewModel(string storePath, string keyFilePath, int secretCount)
{
StorePath = storePath;
KeyFilePath = keyFilePath;
SecretCount = secretCount;
}
}
Task 9: Create SecureStoreInfoFormView
File: NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml (new file)
Content: Simple panel with instruction text and optional store info display.
File: NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml.cs (new file)
Content: Code-behind for the view.
Task 10: Update MainWindowViewModel - Remove lock/unlock commands and state
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs
Remove:
_openStoresdictionary (line 41)_selectedStoreNodefield (line 42)UnlockStoreCommandand related methodsLockStoreCommandand related methodsRaiseSecureStoreCommandsCanExecuteChanged()methodCreateLockedStoreFormViewModel()methodCreateUnlockedStoreFormViewModel()methodFindParentStoreNode()method (if only used for lock state)
Task 11: Update MainWindowViewModel - Add auto-initialization logic
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs
Add new method InitializeSecureStoreAsync():
/// <summary>
/// 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 InitializeSecureStoreAsync()
{
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
{
if (!File.Exists(storePath))
{
if (!secureStoreConfig.AutoCreateStore)
{
_logger?.LogWarning("SecureStore not found and AutoCreateStore is false");
return;
}
// Create new store with keyfile
_logger?.LogInformation("Creating SecureStore at {StorePath}", storePath);
_secureStoreManager.CreateStore(storePath, keyFilePath);
// 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))
{
await _dialogService!.ShowErrorAsync(
"SecureStore Error",
$"Key file not found: {keyFilePath}\n\nThe SecureStore cannot be opened without its key file.");
return;
}
_secureStoreManager.OpenStore(storePath, keyFilePath);
}
// Ensure all required keys exist
if (secureStoreConfig.RequiredKeys?.Count > 0)
{
var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys);
if (addedKeys.Count > 0)
{
_logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count);
}
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to initialize SecureStore");
await _dialogService!.ShowErrorAsync(
"SecureStore Error",
$"Failed to initialize SecureStore:\n\n{ex.Message}");
}
}
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));
}
Task 12: Update MainWindowViewModel - Modify BuildTreeNodes
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs
Replace the Secure Stores section in BuildTreeNodes() (around lines 464-467):
Before:
// Secure Stores folder
var secureStoresFolder = new TreeNodeViewModel("Secure Stores", "🔑", TreeNodeType.SecureStoresFolder) { IsExpanded = true };
LoadConfiguredSecureStore(secureStoresFolder);
TreeNodes.Add(secureStoresFolder);
After:
// Secure Store node (single store, secrets as direct children)
var secureStoreNode = CreateSecureStoreNode();
if (secureStoreNode != null)
{
TreeNodes.Add(secureStoreNode);
}
Add new method CreateSecureStoreNode():
private TreeNodeViewModel? CreateSecureStoreNode()
{
if (_appSettings?.SecureStore == null)
return null;
if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
return null;
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)
{
StorePath = storePath,
KeyFilePath = keyFilePath,
SectionKey = storePath,
IsExpanded = true
};
// Add secrets as direct children if store is open
if (_secureStoreManager.IsStoreOpen)
{
foreach (var key in _secureStoreManager.GetKeys().OrderBy(k => k))
{
var secretNode = new TreeNodeViewModel(key, "🔐", TreeNodeType.Secret)
{
SecretKey = key
};
storeNode.Children.Add(secretNode);
}
}
return storeNode;
}
Task 13: Update MainWindowViewModel - Modify OnSelectedNodeChanged
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs
Update the switch statement in OnSelectedNodeChanged() (around lines 530-557):
Remove:
case TreeNodeType.SecureStoresFolder:block- Lock/unlock logic in
case TreeNodeType.SecureStore:block
Replace with:
case TreeNodeType.SecureStore:
SelectedFormViewModel = CreateSecureStoreInfoFormViewModel();
return;
case TreeNodeType.Secret:
SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode);
return;
Add method:
private SecureStoreInfoFormViewModel CreateSecureStoreInfoFormViewModel()
{
var storePath = _appSettings?.SecureStore?.StorePath ?? "Unknown";
var keyFilePath = _appSettings?.SecureStore?.KeyFilePath ?? "Unknown";
var secretCount = _secureStoreManager.IsStoreOpen ? _secureStoreManager.GetKeys().Count : 0;
return new SecureStoreInfoFormViewModel(storePath, keyFilePath, secretCount);
}
Task 14: Update MainWindowViewModel - Call InitializeSecureStoreAsync on load
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs
Find the method that loads the config folder (likely LoadConfigFolderAsync or similar).
Add call to InitializeSecureStoreAsync() after loading appsettings.json but before building tree nodes:
// After loading appsettings
_appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath, ct);
// Initialize SecureStore (auto-create if needed, open, sync required keys)
await InitializeSecureStoreAsync();
// Then build tree nodes
BuildTreeNodes();
Task 15: Remove LoadConfiguredSecureStore method
File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs
Delete the LoadConfiguredSecureStore() method entirely (lines 474-514).
This is replaced by CreateSecureStoreNode() and InitializeSecureStoreAsync().
Task 16: Update DataTemplates for form views
File: NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml (or wherever DataTemplates are defined)
Remove:
- DataTemplate for
SecureStoreLockedFormViewModel - DataTemplate for
SecureStoreUnlockedFormViewModel
Add:
- DataTemplate for
SecureStoreInfoFormViewModel
Task 17: Update tests
File: NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs
Update or remove tests related to:
SecureStoresFoldernode type- Lock/unlock commands
- Password-based store operations
Add tests for:
- Auto-creation of SecureStore on config load
- Sync of missing required keys
- Flattened tree structure
Task 18: Build and verify
dotnet build NEW/JdeScoping.slnx
dotnet test NEW/JdeScoping.slnx
Execution Order
- Tasks 1-2: TreeNodeViewModel changes (enum, remove lock properties)
- Tasks 3-6: SecureStoreManager interface and implementation changes
- Tasks 7-9: Delete old form files, create new info form
- Tasks 10-15: MainWindowViewModel changes (largest task)
- Task 16: Update XAML DataTemplates
- Task 17: Update tests
- Task 18: Build and verify
Risk Areas
- Missing references: Removing lock/unlock commands may break menu items or toolbar buttons
- View DataTemplates: Must update form selector to use new
SecureStoreInfoFormViewModel - Test coverage: Existing tests may expect lock/unlock behavior
Verification Checklist
- App builds without errors
- All tests pass
- Opening a config folder without SecureStore creates it automatically
- Opening a config folder with existing SecureStore opens it
- Missing required keys are added as blank values
- Tree shows "Secure Store" with secrets directly underneath
- Clicking "Secure Store" shows instruction panel
- Clicking a secret shows the secret editor
- Error dialog shown when keyfile is missing