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:
Joseph Doherty
2026-01-22 09:40:38 -05:00
parent 5669bac221
commit 9bf0c29add
28 changed files with 2811 additions and 1527 deletions
+10 -1
View File
@@ -145,9 +145,18 @@
"RequiredKeys": [
"RsaPrivateKey",
"ExcelExport:CriteriaSheetPassword",
"ExcelExport:DataSheetPassword"
"ExcelExport:DataSheetPassword",
"JdeUser",
"JdePassword",
"GiwUser",
"GiwPassword",
"CmsUser",
"CmsPassword"
]
},
"Pipelines": {
"ConfigPath": "Pipelines/pipelines.json"
},
"WorkProcessor": {
"Enabled": true,
"WorkInterval": "00:00:05",
@@ -23,55 +23,33 @@ public class StoreUseCases
}
/// <summary>
/// Creates a new store with either key file or password authentication.
/// Creates a new store with key file authentication.
/// </summary>
/// <param name="storePath">The path where the store will be created.</param>
/// <param name="keyFilePath">The path to the key file, or null for password-based authentication.</param>
/// <param name="password">The password for authentication, or null for key file-based authentication.</param>
public void CreateStore(string storePath, string? keyFilePath, string? password)
/// <param name="keyFilePath">The path to the key file.</param>
public void CreateStore(string storePath, string keyFilePath)
{
_logger.LogInformation("Creating store at {StorePath}", storePath);
if (string.IsNullOrEmpty(keyFilePath))
throw new ArgumentException("Key file path must be provided.", nameof(keyFilePath));
if (!string.IsNullOrEmpty(keyFilePath))
{
_storeManager.CreateStore(storePath, keyFilePath);
_logger.LogInformation("Store created with key file: {KeyFilePath}", keyFilePath);
}
else if (!string.IsNullOrEmpty(password))
{
_storeManager.CreateStoreWithPassword(storePath, password);
_logger.LogInformation("Password-protected store created");
}
else
{
throw new ArgumentException("Either key file path or password must be provided.");
}
_logger.LogInformation("Creating store at {StorePath}", storePath);
_storeManager.CreateStore(storePath, keyFilePath);
_logger.LogInformation("Store created with key file: {KeyFilePath}", keyFilePath);
}
/// <summary>
/// Opens an existing store with either key file or password authentication.
/// Opens an existing store with key file authentication.
/// </summary>
/// <param name="storePath">The path to the existing store.</param>
/// <param name="keyFilePath">The path to the key file, or null for password-based authentication.</param>
/// <param name="password">The password for authentication, or null for key file-based authentication.</param>
public void OpenStore(string storePath, string? keyFilePath, string? password)
/// <param name="keyFilePath">The path to the key file.</param>
public void OpenStore(string storePath, string keyFilePath)
{
_logger.LogInformation("Opening store at {StorePath}", storePath);
if (string.IsNullOrEmpty(keyFilePath))
throw new ArgumentException("Key file path must be provided.", nameof(keyFilePath));
if (!string.IsNullOrEmpty(keyFilePath))
{
_storeManager.OpenStore(storePath, keyFilePath);
_logger.LogDebug("Store opened with key file");
}
else if (!string.IsNullOrEmpty(password))
{
_storeManager.OpenStoreWithPassword(storePath, password);
_logger.LogDebug("Store opened with password");
}
else
{
throw new ArgumentException("Either key file path or password must be provided.");
}
_logger.LogInformation("Opening store at {StorePath}", storePath);
_storeManager.OpenStore(storePath, keyFilePath);
_logger.LogDebug("Store opened with key file");
}
/// <summary>
@@ -41,6 +41,16 @@ public class ConfigModel
/// Gets or sets the connection strings for external data sources.
/// </summary>
public Dictionary<string, string> ConnectionStrings { get; set; } = new();
/// <summary>
/// Gets or sets the secure store configuration.
/// </summary>
public SecureStoreSection SecureStore { get; set; } = new();
/// <summary>
/// Gets or sets the pipelines configuration.
/// </summary>
public PipelinesSection Pipelines { get; set; } = new();
}
public class DataSyncSection
@@ -230,3 +240,45 @@ public class ExcelExportSection
/// </summary>
public string TimezoneAbbreviation { get; set; } = "CT";
}
/// <summary>
/// Configuration section for the secure store.
/// </summary>
public class SecureStoreSection
{
/// <summary>
/// Gets or sets the path to the secure store file.
/// </summary>
public string StorePath { get; set; } = "data/secrets.json";
/// <summary>
/// Gets or sets the path to the key file for decryption.
/// </summary>
public string KeyFilePath { get; set; } = "data/secrets.key";
/// <summary>
/// Gets or sets the environment variable name for the master key.
/// </summary>
public string MasterKeyEnvVar { get; set; } = "SCOPINGTOOL_MASTER_KEY";
/// <summary>
/// Gets or sets a value indicating whether to auto-create the store if it doesn't exist.
/// </summary>
public bool AutoCreateStore { get; set; } = true;
/// <summary>
/// Gets or sets the list of required secret keys that must exist in the store.
/// </summary>
public List<string> RequiredKeys { get; set; } = [];
}
/// <summary>
/// Configuration section for pipelines.
/// </summary>
public class PipelinesSection
{
/// <summary>
/// Gets or sets the path to the pipelines configuration file.
/// </summary>
public string ConfigPath { get; set; } = "Pipelines/pipelines.json";
}
@@ -39,6 +39,29 @@ public class AvaloniaDialogService : IDialogService
return folders.Count > 0 ? folders[0].Path.LocalPath : null;
}
/// <inheritdoc />
public async Task<string?> ShowFilePickerAsync(string? title = null)
{
var window = _getMainWindow();
if (window == null)
return null;
var files = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = title ?? "Select File",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType("JSON Files")
{
Patterns = new[] { "*.json" }
}
}
});
return files.Count > 0 ? files[0].Path.LocalPath : null;
}
/// <inheritdoc />
public async Task ShowMessageAsync(string title, string message)
{
@@ -13,6 +13,13 @@ public interface IDialogService
/// <returns>The selected folder path, or null if cancelled.</returns>
Task<string?> ShowFolderPickerAsync(string? title = null);
/// <summary>
/// Shows a file picker dialog filtered for JSON files.
/// </summary>
/// <param name="title">Optional title for the dialog.</param>
/// <returns>The selected file path, or null if cancelled.</returns>
Task<string?> ShowFilePickerAsync(string? title = null);
/// <summary>
/// Shows a message dialog.
/// </summary>
@@ -27,13 +27,6 @@ public interface ISecureStoreManager
/// <param name="keyFilePath">Path for the key file (.key).</param>
void CreateStore(string storePath, string keyFilePath);
/// <summary>
/// Creates a new store secured with a password.
/// </summary>
/// <param name="storePath">Path for the new store file (.json).</param>
/// <param name="password">Password to encrypt the store.</param>
void CreateStoreWithPassword(string storePath, string password);
/// <summary>
/// Opens an existing store using a key file.
/// </summary>
@@ -41,13 +34,6 @@ public interface ISecureStoreManager
/// <param name="keyFilePath">Path to the key file (.key).</param>
void OpenStore(string storePath, string keyFilePath);
/// <summary>
/// Opens an existing store using a password.
/// </summary>
/// <param name="storePath">Path to the store file (.json).</param>
/// <param name="password">Password to decrypt the store.</param>
void OpenStoreWithPassword(string storePath, string password);
/// <summary>
/// Closes the currently open store without saving.
/// </summary>
@@ -84,6 +70,13 @@ public interface ISecureStoreManager
/// <param name="key">The secret key to remove.</param>
void RemoveSecret(string key);
/// <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);
/// <summary>
/// Generates a new key file for use with store encryption.
/// </summary>
@@ -72,29 +72,6 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable
_logger.LogInformation("Store created with key file: {KeyFilePath}", keyFilePath);
}
/// <inheritdoc />
public void CreateStoreWithPassword(string storePath, string password)
{
ThrowIfDisposed();
_logger.LogInformation("Creating password-protected store at {StorePath}", storePath);
CloseStoreInternal();
if (string.IsNullOrEmpty(password))
throw new ArgumentException("Password cannot be empty.", nameof(password));
EnsureDirectory(storePath);
_secretsManager = SecretsManager.CreateStore();
_secretsManager.LoadKeyFromPassword(password);
_currentStorePath = storePath;
_keys.Clear();
_hasUnsavedChanges = true;
Save();
_logger.LogInformation("Password-protected store created");
}
/// <inheritdoc />
public void OpenStore(string storePath, string keyFilePath)
{
@@ -117,28 +94,6 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable
_logger.LogDebug("Store opened with key file, contains {KeyCount} keys", _keys.Count);
}
/// <inheritdoc />
public void OpenStoreWithPassword(string storePath, string password)
{
ThrowIfDisposed();
_logger.LogInformation("Opening store at {StorePath} with password", storePath);
CloseStoreInternal();
if (!File.Exists(storePath))
throw new FileNotFoundException("Store file not found.", storePath);
if (string.IsNullOrEmpty(password))
throw new ArgumentException("Password cannot be empty.", nameof(password));
_secretsManager = SecretsManager.LoadStore(storePath);
_secretsManager.LoadKeyFromPassword(password);
_currentStorePath = storePath;
LoadKeysMetadata();
_hasUnsavedChanges = false;
_logger.LogDebug("Store opened with password, contains {KeyCount} keys", _keys.Count);
}
/// <inheritdoc />
public void CloseStore()
{
@@ -237,6 +192,33 @@ public class SecureStoreManager : ISecureStoreManager, IDisposable
_hasUnsavedChanges = true;
}
/// <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();
}
/// <inheritdoc />
public void GenerateKeyFile(string path)
{
@@ -9,45 +9,25 @@ public class ExcelExportFormViewModel : ViewModelBase
{
private readonly ExcelExportSection _model;
private readonly Action _onChanged;
private string _selectedTimezone;
/// <summary>
/// Gets the list of available system timezones (IANA format for cross-platform compatibility).
/// </summary>
public static IReadOnlyList<string> AvailableTimezones { get; } = TimeZoneInfo
.GetSystemTimeZones()
.Select(tz => tz.Id)
.OrderBy(id => id)
.ToList()
.AsReadOnly();
public ExcelExportFormViewModel(ExcelExportSection model, Action onChanged)
{
_model = model ?? throw new ArgumentNullException(nameof(model));
_onChanged = onChanged ?? throw new ArgumentNullException(nameof(onChanged));
}
/// <summary>
/// Gets or sets the password for protecting the criteria worksheet.
/// </summary>
public string CriteriaSheetPassword
{
get => _model.CriteriaSheetPassword;
set
{
if (_model.CriteriaSheetPassword != value)
{
_model.CriteriaSheetPassword = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the password for protecting the data worksheet.
/// </summary>
public string DataSheetPassword
{
get => _model.DataSheetPassword;
set
{
if (_model.DataSheetPassword != value)
{
_model.DataSheetPassword = value;
OnPropertyChanged();
_onChanged();
}
}
// Initialize selected timezone from model (model defaults to "America/Chicago")
_selectedTimezone = _model.TimezoneId;
}
/// <summary>
@@ -119,36 +99,20 @@ public class ExcelExportFormViewModel : ViewModelBase
}
/// <summary>
/// Gets or sets the time zone identifier for date/time conversions.
/// Gets or sets the selected timezone from the dropdown.
/// </summary>
public string TimezoneId
public string SelectedTimezone
{
get => _model.TimezoneId;
get => _selectedTimezone;
set
{
if (_model.TimezoneId != value)
if (_selectedTimezone != value)
{
_selectedTimezone = value;
_model.TimezoneId = value;
OnPropertyChanged();
_onChanged();
}
}
}
/// <summary>
/// Gets or sets the time zone abbreviation for display purposes.
/// </summary>
public string TimezoneAbbreviation
{
get => _model.TimezoneAbbreviation;
set
{
if (_model.TimezoneAbbreviation != value)
{
_model.TimezoneAbbreviation = value;
OnPropertyChanged();
_onChanged();
}
}
}
}
@@ -0,0 +1,40 @@
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; }
/// <summary>
/// Initializes a new instance of the <see cref="SecureStoreInfoFormViewModel"/> class.
/// </summary>
/// <param name="storePath">The path to the secure store file.</param>
/// <param name="keyFilePath">The path to the key file.</param>
/// <param name="secretCount">The number of secrets in the store.</param>
public SecureStoreInfoFormViewModel(string storePath, string keyFilePath, int secretCount)
{
StorePath = storePath;
KeyFilePath = keyFilePath;
SecretCount = secretCount;
}
}
@@ -1,52 +0,0 @@
using System.Windows.Input;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for displaying a locked secure store with unlock capability.
/// </summary>
public class SecureStoreLockedFormViewModel : ViewModelBase
{
private readonly Action _onUnlockRequested;
/// <summary>
/// Initializes a new instance of the <see cref="SecureStoreLockedFormViewModel"/> class.
/// </summary>
/// <param name="storeName">The name of the secure store.</param>
/// <param name="storePath">The full path to the secure store file.</param>
/// <param name="lastModified">The last modified date of the store file.</param>
/// <param name="onUnlockRequested">The action to invoke when unlock is requested.</param>
public SecureStoreLockedFormViewModel(
string storeName,
string storePath,
DateTime? lastModified,
Action onUnlockRequested)
{
StoreName = storeName ?? throw new ArgumentNullException(nameof(storeName));
StorePath = storePath ?? throw new ArgumentNullException(nameof(storePath));
LastModified = lastModified;
_onUnlockRequested = onUnlockRequested ?? throw new ArgumentNullException(nameof(onUnlockRequested));
UnlockCommand = new RelayCommand(_onUnlockRequested);
}
/// <summary>
/// Gets the name of the secure store.
/// </summary>
public string StoreName { get; }
/// <summary>
/// Gets the full path to the secure store file.
/// </summary>
public string StorePath { get; }
/// <summary>
/// Gets the last modified date of the store file.
/// </summary>
public DateTime? LastModified { get; }
/// <summary>
/// Gets the command to unlock the store.
/// </summary>
public ICommand UnlockCommand { get; }
}
@@ -1,92 +0,0 @@
using System.Windows.Input;
namespace JdeScoping.ConfigManager.ViewModels.Forms;
/// <summary>
/// ViewModel for displaying an unlocked secure store.
/// </summary>
public class SecureStoreUnlockedFormViewModel : ViewModelBase
{
private readonly Action _onLockRequested;
private readonly Action _onAddSecretRequested;
private readonly Action _onSaveRequested;
private bool _hasUnsavedChanges;
/// <summary>
/// Initializes a new instance of the <see cref="SecureStoreUnlockedFormViewModel"/> class.
/// </summary>
/// <param name="storeName">The name of the secure store.</param>
/// <param name="storePath">The full path to the secure store file.</param>
/// <param name="secretCount">The number of secrets in the store.</param>
/// <param name="hasUnsavedChanges">Whether the store has unsaved changes.</param>
/// <param name="onLockRequested">The action to invoke when lock is requested.</param>
/// <param name="onAddSecretRequested">The action to invoke when adding a new secret is requested.</param>
/// <param name="onSaveRequested">The action to invoke when save is requested.</param>
public SecureStoreUnlockedFormViewModel(
string storeName,
string storePath,
int secretCount,
bool hasUnsavedChanges,
Action onLockRequested,
Action onAddSecretRequested,
Action onSaveRequested)
{
StoreName = storeName ?? throw new ArgumentNullException(nameof(storeName));
StorePath = storePath ?? throw new ArgumentNullException(nameof(storePath));
SecretCount = secretCount;
_hasUnsavedChanges = hasUnsavedChanges;
_onLockRequested = onLockRequested ?? throw new ArgumentNullException(nameof(onLockRequested));
_onAddSecretRequested = onAddSecretRequested ?? throw new ArgumentNullException(nameof(onAddSecretRequested));
_onSaveRequested = onSaveRequested ?? throw new ArgumentNullException(nameof(onSaveRequested));
LockCommand = new RelayCommand(_onLockRequested);
AddSecretCommand = new RelayCommand(_onAddSecretRequested);
SaveCommand = new RelayCommand(_onSaveRequested, () => HasUnsavedChanges);
}
/// <summary>
/// Gets the name of the secure store.
/// </summary>
public string StoreName { get; }
/// <summary>
/// Gets the full path to the secure store file.
/// </summary>
public string StorePath { get; }
/// <summary>
/// Gets the number of secrets in the store.
/// </summary>
public int SecretCount { get; }
/// <summary>
/// Gets or sets whether the store has unsaved changes.
/// </summary>
public bool HasUnsavedChanges
{
get => _hasUnsavedChanges;
set
{
if (SetProperty(ref _hasUnsavedChanges, value))
{
// Notify that SaveCommand's CanExecute may have changed
(SaveCommand as RelayCommand)?.RaiseCanExecuteChanged();
}
}
}
/// <summary>
/// Gets the command to lock the store.
/// </summary>
public ICommand LockCommand { get; }
/// <summary>
/// Gets the command to add a new secret.
/// </summary>
public ICommand AddSecretCommand { get; }
/// <summary>
/// Gets the command to save the store.
/// </summary>
public ICommand SaveCommand { get; }
}
@@ -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);
@@ -7,8 +7,7 @@ public enum TreeNodeType
Folder,
SettingsSection,
Pipeline,
SecureStoresFolder, // The "Secure Stores" folder
SecureStore, // Individual store files
SecureStore, // The single secure store node
Secret // Individual secrets within a store
}
@@ -29,7 +28,6 @@ public class TreeNodeViewModel : ViewModelBase
private bool _isExpanded;
private bool _isSelected;
private ValidationState _validationState = ValidationState.Unknown;
private bool _isUnlocked;
/// <summary>
/// Gets the name of the tree node.
@@ -51,39 +49,18 @@ public class TreeNodeViewModel : ViewModelBase
/// </summary>
public string? SectionKey { get; init; }
/// <summary>
/// Gets or sets whether this secure store is currently unlocked.
/// Only applicable for SecureStore node types.
/// </summary>
public bool IsUnlocked
{
get => _isUnlocked;
set
{
if (SetProperty(ref _isUnlocked, value))
{
OnPropertyChanged(nameof(LockIcon));
OnPropertyChanged(nameof(IsLocked));
}
}
}
/// <summary>
/// Gets whether this secure store is locked.
/// </summary>
public bool IsLocked => !IsUnlocked;
/// <summary>
/// Gets the lock icon for secure store nodes.
/// </summary>
public string LockIcon => IsUnlocked ? "🔓" : "🔒";
/// <summary>
/// Gets or sets the full path to the secure store file.
/// Only applicable for SecureStore node types.
/// </summary>
public string? StorePath { get; init; }
/// <summary>
/// Gets or sets the full path to the key file for the secure store.
/// Only applicable for SecureStore node types.
/// </summary>
public string? KeyFilePath { get; init; }
/// <summary>
/// Gets or sets the secret key name.
/// Only applicable for Secret node types.
@@ -13,43 +13,6 @@
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
</StackPanel>
<!-- Sheet Protection Section -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
<StackPanel Spacing="16">
<TextBlock Text="Sheet Protection" Foreground="#E6EDF5"
FontWeight="SemiBold" FontSize="14"/>
<!-- Criteria Sheet Password -->
<StackPanel Spacing="4">
<TextBlock Text="Criteria Sheet Password"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding CriteriaSheetPassword}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
PasswordChar="*"
Watermark="(optional)"/>
<TextBlock Text="Password to protect the search criteria sheet"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
<!-- Data Sheet Password -->
<StackPanel Spacing="4">
<TextBlock Text="Data Sheet Password"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding DataSheetPassword}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
PasswordChar="*"
Watermark="(optional)"/>
<TextBlock Text="Password to protect the data sheet"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Output Format Section -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
@@ -92,31 +55,17 @@
<TextBlock Text="Timezone" Foreground="#E6EDF5"
FontWeight="SemiBold" FontSize="14"/>
<Grid ColumnDefinitions="*,16,Auto">
<!-- Timezone ID -->
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Timezone ID"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding TimezoneId}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="America/Chicago"/>
</StackPanel>
<!-- Timezone Abbreviation -->
<StackPanel Grid.Column="2" Spacing="4" Width="100">
<TextBlock Text="Abbreviation"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding TimezoneAbbreviation}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono"
Watermark="CST"/>
</StackPanel>
</Grid>
<TextBlock Text="Used for converting UTC timestamps to local time in exports"
Foreground="#5C6A7A" FontSize="11"/>
<StackPanel Spacing="4">
<TextBlock Text="Timezone"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<ComboBox ItemsSource="{Binding AvailableTimezones}"
SelectedItem="{Binding SelectedTimezone}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
HorizontalAlignment="Stretch"/>
<TextBlock Text="Used for converting UTC timestamps to local time in exports"
Foreground="#5C6A7A" FontSize="11"/>
</StackPanel>
</StackPanel>
</Border>
@@ -0,0 +1,75 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Forms"
x:Class="JdeScoping.ConfigManager.Views.Forms.SecureStoreInfoFormView"
x:DataType="vm:SecureStoreInfoFormViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="24" MaxWidth="600">
<!-- Header -->
<StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<PathIcon Data="M12.65 10C11.83 7.67 9.61 6 7 6C3.69 6 1 8.69 1 12C1 15.31 3.69 18 7 18C9.61 18 11.83 16.33 12.65 14H17V18H21V14H23V10H12.65M7 14C5.9 14 5 13.1 5 12C5 10.9 5.9 10 7 10C8.1 10 9 10.9 9 12C9 13.1 8.1 14 7 14Z"
Width="24" Height="24" Foreground="#F59E0B"/>
<TextBlock Text="Secure Store"
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
</StackPanel>
<!-- Instructions Card -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="M11 7V9H13V7H11M14 17V15H13V11H10V13H11V15H10V17H14M22 12C22 17.5 17.5 22 12 22C6.5 22 2 17.5 2 12C2 6.5 6.5 2 12 2C17.5 2 22 6.5 22 12Z"
Width="20" Height="20" Foreground="#3B82F6"/>
<TextBlock Text="{Binding InstructionText}"
Foreground="#E6EDF5" FontSize="14"
VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Store Info Card -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
<StackPanel Spacing="16">
<TextBlock Text="Store Information" Foreground="#E6EDF5"
FontWeight="SemiBold" FontSize="14"/>
<!-- Store Path -->
<StackPanel Spacing="4">
<TextBlock Text="Store Path"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding StorePath}"
Background="#1A1F26" Foreground="#9BA8B8"
BorderBrush="#2D3540" Height="36"
FontFamily="JetBrains Mono" FontSize="12"
IsReadOnly="True"/>
</StackPanel>
<!-- Key File Path -->
<StackPanel Spacing="4">
<TextBlock Text="Key File Path"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding KeyFilePath}"
Background="#1A1F26" Foreground="#9BA8B8"
BorderBrush="#2D3540" Height="36"
FontFamily="JetBrains Mono" FontSize="12"
IsReadOnly="True"/>
</StackPanel>
<!-- Secret Count -->
<StackPanel Spacing="4">
<TextBlock Text="Number of Secrets"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="{Binding SecretCount}"
Foreground="#E6EDF5" FontSize="14"/>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -2,9 +2,9 @@ using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Forms;
public partial class SecureStoreLockedFormView : UserControl
public partial class SecureStoreInfoFormView : UserControl
{
public SecureStoreLockedFormView()
public SecureStoreInfoFormView()
{
InitializeComponent();
}
@@ -1,82 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Forms"
x:Class="JdeScoping.ConfigManager.Views.Forms.SecureStoreLockedFormView"
x:DataType="vm:SecureStoreLockedFormViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="24" MaxWidth="600">
<!-- Header with Lock Icon -->
<StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<PathIcon Data="M12 17C10.89 17 10 16.1 10 15C10 13.89 10.89 13 12 13C13.11 13 14 13.89 14 15C14 16.1 13.11 17 12 17M18 8C19.1 8 20 8.9 20 10V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V10C4 8.9 4.9 8 6 8H7V6C7 3.24 9.24 1 12 1C14.76 1 17 3.24 17 6V8H18M12 3C10.34 3 9 4.34 9 6V8H15V6C15 4.34 13.66 3 12 3Z"
Width="24" Height="24" Foreground="#FFB84D"/>
<TextBlock Text="{Binding StoreName}"
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
</StackPanel>
<!-- Lock Status Card -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
<StackPanel Spacing="16">
<!-- Status Message -->
<StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Center">
<PathIcon Data="M12 17C10.89 17 10 16.1 10 15C10 13.89 10.89 13 12 13C13.11 13 14 13.89 14 15C14 16.1 13.11 17 12 17M18 8C19.1 8 20 8.9 20 10V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V10C4 8.9 4.9 8 6 8H7V6C7 3.24 9.24 1 12 1C14.76 1 17 3.24 17 6V8H18M12 3C10.34 3 9 4.34 9 6V8H15V6C15 4.34 13.66 3 12 3Z"
Width="48" Height="48" Foreground="#5C6A7A"/>
</StackPanel>
<TextBlock Text="This store is locked"
Foreground="#9BA8B8" FontSize="14" FontWeight="Medium"
HorizontalAlignment="Center"/>
<TextBlock Text="Enter your password to unlock and view secrets"
Foreground="#5C6A7A" FontSize="12"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Store Information Card -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
<StackPanel Spacing="16">
<TextBlock Text="Store Information" Foreground="#E6EDF5"
FontWeight="SemiBold" FontSize="14"/>
<!-- Store Path -->
<StackPanel Spacing="4">
<TextBlock Text="Store Path"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding StorePath}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono" FontSize="11"
IsReadOnly="True"/>
</StackPanel>
<!-- Last Modified -->
<StackPanel Spacing="4" IsVisible="{Binding LastModified, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Text="Last Modified"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBlock Text="{Binding LastModified, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}"
Foreground="#E6EDF5" FontSize="13"
FontFamily="JetBrains Mono"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Unlock Button -->
<Button Command="{Binding UnlockCommand}"
HorizontalAlignment="Center"
Background="#3B82F6" Foreground="#FFFFFF"
Padding="24,12" CornerRadius="6"
FontWeight="SemiBold">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="M18 8C19.1 8 20 8.9 20 10V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V10C4 8.9 4.9 8 6 8H17V6C17 3.24 14.76 1 12 1C10.07 1 8.39 2.11 7.53 3.71L6.14 2.43C7.34 0.88 9.54 0 12 0C15.87 0 19 3.13 19 7V8H18M12 17C13.11 17 14 16.1 14 15C14 13.89 13.11 13 12 13C10.89 13 10 13.89 10 15C10 16.1 10.89 17 12 17Z"
Width="16" Height="16" Foreground="#FFFFFF"/>
<TextBlock Text="Unlock Store..."/>
</StackPanel>
</Button>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -1,125 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:JdeScoping.ConfigManager.ViewModels.Forms"
x:Class="JdeScoping.ConfigManager.Views.Forms.SecureStoreUnlockedFormView"
x:DataType="vm:SecureStoreUnlockedFormViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="24" MaxWidth="600">
<!-- Header with Unlock Icon -->
<StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<PathIcon Data="M18 8C19.1 8 20 8.9 20 10V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V10C4 8.9 4.9 8 6 8H17V6C17 3.24 14.76 1 12 1C10.07 1 8.39 2.11 7.53 3.71L5.71 2.39C6.83 0.92 9.15 0 12 0C15.87 0 19 3.13 19 7V8H18M12 17C13.11 17 14 16.1 14 15C14 13.89 13.11 13 12 13C10.89 13 10 13.89 10 15C10 16.1 10.89 17 12 17Z"
Width="24" Height="24" Foreground="#22C55E"/>
<TextBlock Text="{Binding StoreName}"
Foreground="#E6EDF5" FontSize="18" FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<Border Height="1" Background="#2D3540" Margin="0,12,0,0"/>
</StackPanel>
<!-- Store Status Card -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
<StackPanel Spacing="16">
<TextBlock Text="Store Status" Foreground="#E6EDF5"
FontWeight="SemiBold" FontSize="14"/>
<!-- Store Path -->
<StackPanel Spacing="4">
<TextBlock Text="Store Path"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<TextBox Text="{Binding StorePath}"
Background="#232A35" Foreground="#E6EDF5"
BorderBrush="#3D4550" Height="36"
FontFamily="JetBrains Mono" FontSize="11"
IsReadOnly="True"/>
</StackPanel>
<!-- Secret Count -->
<StackPanel Spacing="4">
<TextBlock Text="Secrets"
Foreground="#9BA8B8" FontSize="12" FontWeight="Medium"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding SecretCount}"
Foreground="#E6EDF5" FontSize="24" FontWeight="Bold"
VerticalAlignment="Center"/>
<TextBlock Text="secret(s) stored"
Foreground="#5C6A7A" FontSize="13"
VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
<!-- Unsaved Changes Status -->
<Border Background="#151920" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="4" Padding="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<!-- Status indicator when no unsaved changes -->
<PathIcon Data="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2M10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z"
Width="20" Height="20" Foreground="#22C55E"
IsVisible="{Binding !HasUnsavedChanges}"/>
<TextBlock Text="All changes saved"
Foreground="#22C55E" FontSize="13" FontWeight="Medium"
VerticalAlignment="Center"
IsVisible="{Binding !HasUnsavedChanges}"/>
<!-- Status indicator when there are unsaved changes -->
<PathIcon Data="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2M13 17H11V15H13V17M13 13H11V7H13V13Z"
Width="20" Height="20" Foreground="#FFB84D"
IsVisible="{Binding HasUnsavedChanges}"/>
<TextBlock Text="Unsaved changes"
Foreground="#FFB84D" FontSize="13" FontWeight="Medium"
VerticalAlignment="Center"
IsVisible="{Binding HasUnsavedChanges}"/>
</StackPanel>
</Border>
</StackPanel>
</Border>
<!-- Actions Card -->
<Border Background="#0D0F12" BorderBrush="#2D3540" BorderThickness="1"
CornerRadius="6" Padding="16">
<StackPanel Spacing="16">
<TextBlock Text="Actions" Foreground="#E6EDF5"
FontWeight="SemiBold" FontSize="14"/>
<StackPanel Orientation="Horizontal" Spacing="12">
<!-- Lock Store Button -->
<Button Command="{Binding LockCommand}"
Background="#3D4550" Foreground="#E6EDF5"
Padding="16,10" CornerRadius="6">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="M12 17C10.89 17 10 16.1 10 15C10 13.89 10.89 13 12 13C13.11 13 14 13.89 14 15C14 16.1 13.11 17 12 17M18 8C19.1 8 20 8.9 20 10V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V10C4 8.9 4.9 8 6 8H7V6C7 3.24 9.24 1 12 1C14.76 1 17 3.24 17 6V8H18M12 3C10.34 3 9 4.34 9 6V8H15V6C15 4.34 13.66 3 12 3Z"
Width="16" Height="16" Foreground="#E6EDF5"/>
<TextBlock Text="Lock Store"/>
</StackPanel>
</Button>
<!-- Add Secret Button -->
<Button Command="{Binding AddSecretCommand}"
Background="#22C55E" Foreground="#FFFFFF"
Padding="16,10" CornerRadius="6">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z"
Width="16" Height="16" Foreground="#FFFFFF"/>
<TextBlock Text="Add Secret"/>
</StackPanel>
</Button>
<!-- Save Button -->
<Button Command="{Binding SaveCommand}"
Background="#3B82F6" Foreground="#FFFFFF"
Padding="16,10" CornerRadius="6"
IsEnabled="{Binding HasUnsavedChanges}">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="M15 9H5V5H15M12 19C10.34 19 9 17.66 9 16C9 14.34 10.34 13 12 13C13.66 13 15 14.34 15 16C15 17.66 13.66 19 12 19M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3Z"
Width="16" Height="16" Foreground="#FFFFFF"/>
<TextBlock Text="Save"/>
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace JdeScoping.ConfigManager.Views.Forms;
public partial class SecureStoreUnlockedFormView : UserControl
{
public SecureStoreUnlockedFormView()
{
InitializeComponent();
}
}