chore: deprecate standalone SecureStoreManager utility
Move SecureStoreManager project and tests to Deprecated folder and remove from solution. SecureStore functionality is now integrated into ConfigManager.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// An async command implementation that properly handles async operations.
|
||||
/// </summary>
|
||||
public class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isExecuting;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the result of CanExecute() may have changed.
|
||||
/// </summary>
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
remove => _canExecuteChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AsyncRelayCommand that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="execute">The async action to execute.</param>
|
||||
public AsyncRelayCommand(Func<Task> execute)
|
||||
: this(execute, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new AsyncRelayCommand with a CanExecute predicate.
|
||||
/// </summary>
|
||||
/// <param name="execute">The async action to execute.</param>
|
||||
/// <param name="canExecute">The predicate to determine if the command can execute.</param>
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the command can be executed in its current state.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The command parameter (unused).</param>
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the async command if it can execute.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The command parameter (unused).</param>
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
return;
|
||||
|
||||
_isExecuting = true;
|
||||
RaiseCanExecuteChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await _execute();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the CanExecuteChanged event.
|
||||
/// </summary>
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.SecureStoreManager.Constants;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for creating a new store.
|
||||
/// </summary>
|
||||
public class NewStoreDialogViewModel : ViewModelBase
|
||||
{
|
||||
private string _storePath = string.Empty;
|
||||
private string _keyFilePath = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NewStoreDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
public NewStoreDialogViewModel()
|
||||
{
|
||||
BrowseStorePathCommand = new RelayCommand(BrowseStorePath);
|
||||
BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the store file to create.
|
||||
/// </summary>
|
||||
public string StorePath
|
||||
{
|
||||
get => _storePath;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _storePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the key file for encryption.
|
||||
/// </summary>
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _keyFilePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to browse for store path location.
|
||||
/// </summary>
|
||||
public ICommand BrowseStorePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to browse for key file path location.
|
||||
/// </summary>
|
||||
public ICommand BrowseKeyFilePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(StorePath) && !string.IsNullOrWhiteSpace(KeyFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(StorePath))
|
||||
return DialogStrings.StorePathRequired;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeyFilePath))
|
||||
return DialogStrings.KeyFilePathRequired;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to request save file dialog for store path.
|
||||
/// Parameters: title, fileTypeName, pattern, defaultExtension
|
||||
/// Returns: selected file path or null
|
||||
/// </summary>
|
||||
public event Func<string, string, string, string, Task<string?>>? OnShowSaveFileDialog;
|
||||
|
||||
private async void BrowseStorePath()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowSaveFileDialog(
|
||||
DialogStrings.ChooseStoreLocation,
|
||||
FileExtensions.StoreTypeName,
|
||||
FileExtensions.StorePattern,
|
||||
FileExtensions.StoreExtension);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
StorePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
private async void BrowseKeyFilePath()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowSaveFileDialog(
|
||||
DialogStrings.ChooseKeyFileLocation,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern,
|
||||
FileExtensions.KeyExtension);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
KeyFilePath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for opening an existing store.
|
||||
/// </summary>
|
||||
public class OpenStoreDialogViewModel : ViewModelBase
|
||||
{
|
||||
private string _storePath = string.Empty;
|
||||
private string _keyFilePath = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OpenStoreDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
public OpenStoreDialogViewModel()
|
||||
{
|
||||
BrowseStorePathCommand = new RelayCommand(BrowseStorePath);
|
||||
BrowseKeyFilePathCommand = new RelayCommand(BrowseKeyFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the store file to open.
|
||||
/// </summary>
|
||||
public string StorePath
|
||||
{
|
||||
get => _storePath;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _storePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the key file for decryption.
|
||||
/// </summary>
|
||||
public string KeyFilePath
|
||||
{
|
||||
get => _keyFilePath;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _keyFilePath, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to browse for store path location.
|
||||
/// </summary>
|
||||
public ICommand BrowseStorePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to browse for key file path location.
|
||||
/// </summary>
|
||||
public ICommand BrowseKeyFilePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(StorePath))
|
||||
return false;
|
||||
|
||||
return !string.IsNullOrWhiteSpace(KeyFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(StorePath))
|
||||
return DialogStrings.StorePathRequired;
|
||||
|
||||
if (!System.IO.File.Exists(StorePath))
|
||||
return DialogStrings.StoreFileNotFound;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(KeyFilePath))
|
||||
return DialogStrings.KeyFilePathRequired;
|
||||
|
||||
if (!System.IO.File.Exists(KeyFilePath))
|
||||
return DialogStrings.KeyFileNotFound;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised to request open file dialog for store path.
|
||||
/// Parameters: title, fileTypeName, pattern
|
||||
/// Returns: selected file path or null
|
||||
/// </summary>
|
||||
public event Func<string, string, string, Task<string?>>? OnShowOpenFileDialog;
|
||||
|
||||
private async void BrowseStorePath()
|
||||
{
|
||||
if (OnShowOpenFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowOpenFileDialog(
|
||||
DialogStrings.SelectStoreFile,
|
||||
FileExtensions.StoreTypeName,
|
||||
FileExtensions.StorePattern);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
StorePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
private async void BrowseKeyFilePath()
|
||||
{
|
||||
if (OnShowOpenFileDialog == null)
|
||||
return;
|
||||
|
||||
var path = await OnShowOpenFileDialog(
|
||||
DialogStrings.SelectKeyFile,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
KeyFilePath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View model for adding or editing a secret.
|
||||
/// </summary>
|
||||
public class SecretEditDialogViewModel : ViewModelBase
|
||||
{
|
||||
private string _key = string.Empty;
|
||||
private string _value = string.Empty;
|
||||
private bool _isNewSecret = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialogViewModel"/> class.
|
||||
/// </summary>
|
||||
public SecretEditDialogViewModel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretEditDialogViewModel"/> class with a key and value for editing.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key.</param>
|
||||
/// <param name="value">The secret value.</param>
|
||||
public SecretEditDialogViewModel(string key, string value)
|
||||
{
|
||||
_key = key;
|
||||
_value = value;
|
||||
_isNewSecret = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key.
|
||||
/// </summary>
|
||||
public string Key
|
||||
{
|
||||
get => _key;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _key, value))
|
||||
NotifyValidationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret value.
|
||||
/// </summary>
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this is a new secret being added.
|
||||
/// </summary>
|
||||
public bool IsNewSecret
|
||||
{
|
||||
get => _isNewSecret;
|
||||
set => SetProperty(ref _isNewSecret, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the key field is editable.
|
||||
/// </summary>
|
||||
public bool IsKeyEditable => _isNewSecret;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dialog title based on whether this is a new secret or edit.
|
||||
/// </summary>
|
||||
public string DialogTitle => _isNewSecret ? "Add Secret" : "Edit Secret";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the dialog input is valid.
|
||||
/// </summary>
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(Key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validation error message, or null if valid.
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Key))
|
||||
return DialogStrings.KeyRequired;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyValidationChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsValid));
|
||||
OnPropertyChanged(nameof(ValidationError));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.SecureStoreManager.Constants;
|
||||
using JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Main window view model containing all application logic.
|
||||
/// </summary>
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ISecureStoreManager _storeManager;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private SecretItemViewModel? _selectedSecret;
|
||||
private string _statusMessage = "Ready";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MainWindowViewModel.
|
||||
/// </summary>
|
||||
/// <param name="storeManager">The secure store manager service.</param>
|
||||
/// <param name="dialogService">The dialog service for user interactions.</param>
|
||||
/// <param name="clipboardService">The clipboard service for copying secrets.</param>
|
||||
public MainWindowViewModel(
|
||||
ISecureStoreManager storeManager,
|
||||
IDialogService dialogService,
|
||||
IClipboardService clipboardService)
|
||||
{
|
||||
_storeManager = storeManager ?? throw new ArgumentNullException(nameof(storeManager));
|
||||
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
||||
_clipboardService = clipboardService ?? throw new ArgumentNullException(nameof(clipboardService));
|
||||
Secrets = new ObservableCollection<SecretItemViewModel>();
|
||||
|
||||
// File commands (async)
|
||||
NewStoreCommand = new AsyncRelayCommand(ExecuteNewStoreAsync);
|
||||
OpenStoreCommand = new AsyncRelayCommand(ExecuteOpenStoreAsync);
|
||||
SaveCommand = new AsyncRelayCommand(ExecuteSaveAsync, CanSave);
|
||||
CloseStoreCommand = new AsyncRelayCommand(ExecuteCloseStoreAsync, () => _storeManager.IsStoreOpen);
|
||||
ExitCommand = new AsyncRelayCommand(ExecuteExitAsync);
|
||||
|
||||
// Secret commands
|
||||
AddSecretCommand = new RelayCommand(ExecuteAddSecret, () => _storeManager.IsStoreOpen);
|
||||
EditSecretCommand = new RelayCommand(ExecuteEditSecret, CanEditOrDeleteSecret);
|
||||
DeleteSecretCommand = new AsyncRelayCommand(ExecuteDeleteSecretAsync, CanEditOrDeleteSecret);
|
||||
|
||||
// Tools commands (async)
|
||||
GenerateKeyFileCommand = new AsyncRelayCommand(ExecuteGenerateKeyFileAsync);
|
||||
ExportKeyCommand = new AsyncRelayCommand(ExecuteExportKeyAsync, () => _storeManager.IsStoreOpen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of secrets in the current store.
|
||||
/// </summary>
|
||||
public ObservableCollection<SecretItemViewModel> Secrets { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the currently selected secret.
|
||||
/// </summary>
|
||||
public SecretItemViewModel? SelectedSecret
|
||||
{
|
||||
get => _selectedSecret;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedSecret, value))
|
||||
{
|
||||
(EditSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current status message.
|
||||
/// </summary>
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
private set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the window title including the current store path.
|
||||
/// </summary>
|
||||
public string WindowTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
var title = "SecureStore Manager";
|
||||
if (_storeManager.IsStoreOpen)
|
||||
{
|
||||
title += $" - {_storeManager.CurrentStorePath}";
|
||||
if (_storeManager.HasUnsavedChanges)
|
||||
title += " *";
|
||||
}
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a store is currently open.
|
||||
/// </summary>
|
||||
public bool IsStoreOpen => _storeManager.IsStoreOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether there are unsaved changes.
|
||||
/// </summary>
|
||||
public bool HasUnsavedChanges => _storeManager.HasUnsavedChanges;
|
||||
|
||||
// File Commands
|
||||
/// <summary>
|
||||
/// Gets the command to create a new store.
|
||||
/// </summary>
|
||||
public ICommand NewStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to open an existing store.
|
||||
/// </summary>
|
||||
public ICommand OpenStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to save the current store.
|
||||
/// </summary>
|
||||
public ICommand SaveCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to close the current store.
|
||||
/// </summary>
|
||||
public ICommand CloseStoreCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to exit the application.
|
||||
/// </summary>
|
||||
public ICommand ExitCommand { get; }
|
||||
|
||||
// Secret Commands
|
||||
/// <summary>
|
||||
/// Gets the command to add a new secret.
|
||||
/// </summary>
|
||||
public ICommand AddSecretCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to edit the selected secret.
|
||||
/// </summary>
|
||||
public ICommand EditSecretCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to delete the selected secret.
|
||||
/// </summary>
|
||||
public ICommand DeleteSecretCommand { get; }
|
||||
|
||||
// Tools Commands
|
||||
/// <summary>
|
||||
/// Gets the command to generate a new key file.
|
||||
/// </summary>
|
||||
public ICommand GenerateKeyFileCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command to export the store's key.
|
||||
/// </summary>
|
||||
public ICommand ExportKeyCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new store with a key file. Called by the dialog.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path where the store file will be created.</param>
|
||||
/// <param name="keyFilePath">The path to a key file for encryption.</param>
|
||||
public async Task CreateNewStoreAsync(string storePath, string keyFilePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyFilePath))
|
||||
{
|
||||
throw new ArgumentException("Key file path must be provided.");
|
||||
}
|
||||
|
||||
_storeManager.CreateStore(storePath, keyFilePath);
|
||||
StatusMessage = $"Created store with key file: {keyFilePath}";
|
||||
|
||||
RefreshSecrets();
|
||||
NotifyStoreChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error creating store: {ex.Message}";
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToCreateStoreFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens an existing store with a key file. Called by the dialog.
|
||||
/// </summary>
|
||||
/// <param name="storePath">The path to the store file to open.</param>
|
||||
/// <param name="keyFilePath">The path to a key file for decryption.</param>
|
||||
public async Task OpenExistingStoreAsync(string storePath, string keyFilePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(keyFilePath))
|
||||
{
|
||||
throw new ArgumentException("Key file path must be provided.");
|
||||
}
|
||||
|
||||
_storeManager.OpenStore(storePath, keyFilePath);
|
||||
StatusMessage = "Opened store with key file";
|
||||
|
||||
RefreshSecrets();
|
||||
NotifyStoreChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error opening store: {ex.Message}";
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToOpenStoreFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a secret. Called by the dialog.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key identifier.</param>
|
||||
/// <param name="value">The secret value to store.</param>
|
||||
/// <param name="isNew">True if this is a new secret, false if updating an existing one.</param>
|
||||
public async Task SaveSecretAsync(string key, string value, bool isNew)
|
||||
{
|
||||
try
|
||||
{
|
||||
_storeManager.SetSecret(key, value);
|
||||
RefreshSecrets();
|
||||
NotifyStoreChanged();
|
||||
StatusMessage = isNew ? $"Added secret: {key}" : $"Updated secret: {key}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error saving secret: {ex.Message}";
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToSaveSecretFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for unsaved changes and prompts the user.
|
||||
/// </summary>
|
||||
/// <returns>True if it's safe to proceed, false if the user cancelled.</returns>
|
||||
public async Task<bool> PromptForUnsavedChangesAsync()
|
||||
{
|
||||
if (!_storeManager.HasUnsavedChanges)
|
||||
return true;
|
||||
|
||||
var result = await _dialogService.ShowUnsavedChangesPromptAsync();
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case UnsavedChangesResult.Save:
|
||||
await ExecuteSaveAsync();
|
||||
return true;
|
||||
case UnsavedChangesResult.DontSave:
|
||||
return true;
|
||||
case UnsavedChangesResult.Cancel:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteNewStoreAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
|
||||
// The view will show the NewStoreDialog
|
||||
OnRequestNewStoreDialog?.Invoke();
|
||||
}
|
||||
|
||||
private async Task ExecuteOpenStoreAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
|
||||
// The view will show the OpenStoreDialog
|
||||
OnRequestOpenStoreDialog?.Invoke();
|
||||
}
|
||||
|
||||
private async Task ExecuteSaveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_storeManager.Save();
|
||||
NotifyStoreChanged();
|
||||
StatusMessage = "Store saved";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error saving: {ex.Message}";
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToSaveStoreFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanSave() => _storeManager.IsStoreOpen && _storeManager.HasUnsavedChanges;
|
||||
|
||||
private async Task ExecuteCloseStoreAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
|
||||
_storeManager.CloseStore();
|
||||
Secrets.Clear();
|
||||
NotifyStoreChanged();
|
||||
StatusMessage = "Store closed";
|
||||
}
|
||||
|
||||
private async Task ExecuteExitAsync()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
|
||||
OnRequestClose?.Invoke();
|
||||
}
|
||||
|
||||
private void ExecuteAddSecret()
|
||||
{
|
||||
// The view will show the SecretEditDialog
|
||||
OnRequestAddSecretDialog?.Invoke();
|
||||
}
|
||||
|
||||
private void ExecuteEditSecret()
|
||||
{
|
||||
if (SelectedSecret == null)
|
||||
return;
|
||||
|
||||
// The view will show the SecretEditDialog with existing values
|
||||
OnRequestEditSecretDialog?.Invoke(SelectedSecret.Key, SelectedSecret.ActualValue);
|
||||
}
|
||||
|
||||
private async Task ExecuteDeleteSecretAsync()
|
||||
{
|
||||
if (SelectedSecret == null)
|
||||
return;
|
||||
|
||||
var confirmMessage = string.Format(DialogStrings.ConfirmDeleteFormat, SelectedSecret.Key);
|
||||
var confirmed = await _dialogService.ShowConfirmationAsync(confirmMessage, DialogStrings.ConfirmDeleteTitle);
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var key = SelectedSecret.Key;
|
||||
_storeManager.RemoveSecret(key);
|
||||
RefreshSecrets();
|
||||
NotifyStoreChanged();
|
||||
StatusMessage = $"Deleted secret: {key}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error deleting secret: {ex.Message}";
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToDeleteSecretFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanEditOrDeleteSecret() => _storeManager.IsStoreOpen && SelectedSecret != null;
|
||||
|
||||
private async Task ExecuteGenerateKeyFileAsync()
|
||||
{
|
||||
var filePath = await _dialogService.ShowSaveFileDialogAsync(
|
||||
DialogStrings.GenerateKeyFileTitle,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern,
|
||||
FileExtensions.KeyExtension);
|
||||
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_storeManager.GenerateKeyFile(filePath);
|
||||
StatusMessage = $"Generated key file: {filePath}";
|
||||
await _dialogService.ShowInfoAsync(
|
||||
string.Format(DialogStrings.KeyFileGeneratedFormat, filePath),
|
||||
DialogStrings.KeyGeneratedTitle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error generating key: {ex.Message}";
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToGenerateKeyFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteExportKeyAsync()
|
||||
{
|
||||
var filePath = await _dialogService.ShowSaveFileDialogAsync(
|
||||
DialogStrings.ExportKeyTitle,
|
||||
FileExtensions.KeyTypeName,
|
||||
FileExtensions.KeyPattern,
|
||||
FileExtensions.KeyExtension);
|
||||
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_storeManager.ExportKey(filePath);
|
||||
StatusMessage = $"Exported key to: {filePath}";
|
||||
await _dialogService.ShowInfoAsync(
|
||||
string.Format(DialogStrings.KeyExportedFormat, filePath),
|
||||
DialogStrings.KeyExportedTitle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error exporting key: {ex.Message}";
|
||||
await _dialogService.ShowErrorAsync(
|
||||
string.Format(DialogStrings.FailedToExportKeyFormat, ex.Message),
|
||||
DialogStrings.ErrorTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshSecrets()
|
||||
{
|
||||
Secrets.Clear();
|
||||
if (!_storeManager.IsStoreOpen)
|
||||
return;
|
||||
|
||||
foreach (var key in _storeManager.GetKeys())
|
||||
{
|
||||
var value = _storeManager.GetSecret(key);
|
||||
Secrets.Add(new SecretItemViewModel(key, value, _clipboardService));
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyStoreChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsStoreOpen));
|
||||
OnPropertyChanged(nameof(HasUnsavedChanges));
|
||||
OnPropertyChanged(nameof(WindowTitle));
|
||||
|
||||
// Manually raise CanExecuteChanged for all commands
|
||||
(SaveCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CloseStoreCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(AddSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(EditSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(DeleteSecretCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
(ExportKeyCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
// Events for view to show dialogs (these require view-specific DataContext setup)
|
||||
/// <summary>
|
||||
/// Raised when a new store creation dialog should be shown.
|
||||
/// </summary>
|
||||
public event Action? OnRequestNewStoreDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an open store dialog should be shown.
|
||||
/// </summary>
|
||||
public event Action? OnRequestOpenStoreDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a new secret dialog should be shown.
|
||||
/// </summary>
|
||||
public event Action? OnRequestAddSecretDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an edit secret dialog should be shown with the specified key and value.
|
||||
/// </summary>
|
||||
public event Action<string, string>? OnRequestEditSecretDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the application should close.
|
||||
/// </summary>
|
||||
public event Action? OnRequestClose;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// A command implementation that delegates to action methods.
|
||||
/// </summary>
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
private EventHandler? _canExecuteChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the command's ability to execute may have changed.
|
||||
/// </summary>
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add => _canExecuteChanged += value;
|
||||
remove => _canExecuteChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RelayCommand that can always execute.
|
||||
/// </summary>
|
||||
/// <param name="execute">The action to execute.</param>
|
||||
public RelayCommand(Action<object?> execute)
|
||||
: this(execute, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RelayCommand with a CanExecute predicate.
|
||||
/// </summary>
|
||||
/// <param name="execute">The action to execute.</param>
|
||||
/// <param name="canExecute">The predicate to determine if the command can execute.</param>
|
||||
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RelayCommand from a parameterless action.
|
||||
/// </summary>
|
||||
/// <param name="execute">The action to execute.</param>
|
||||
public RelayCommand(Action execute)
|
||||
: this(_ => execute(), null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RelayCommand from a parameterless action with a CanExecute predicate.
|
||||
/// </summary>
|
||||
/// <param name="execute">The action to execute.</param>
|
||||
/// <param name="canExecute">The predicate to determine if the command can execute.</param>
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute)
|
||||
: this(_ => execute(), canExecute != null ? _ => canExecute() : null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the command can execute.
|
||||
/// </summary>
|
||||
/// <param name="parameter">An optional command parameter.</param>
|
||||
/// <returns>True if the command can execute, false otherwise.</returns>
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return _canExecute == null || _canExecute(parameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the command with the specified parameter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">An optional command parameter.</param>
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
_execute(parameter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the CanExecuteChanged event.
|
||||
/// </summary>
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Windows.Input;
|
||||
using JdeScoping.SecureStoreManager.Services;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for an individual secret item with show/hide toggle.
|
||||
/// </summary>
|
||||
public class SecretItemViewModel : ViewModelBase
|
||||
{
|
||||
private readonly string _actualValue;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private bool _isValueVisible;
|
||||
private const string MaskedValue = "********";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SecretItemViewModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The secret key name.</param>
|
||||
/// <param name="value">The secret value.</param>
|
||||
/// <param name="clipboardService">The clipboard service for copy operations.</param>
|
||||
public SecretItemViewModel(string key, string value, IClipboardService clipboardService)
|
||||
{
|
||||
Key = key;
|
||||
_actualValue = value;
|
||||
_clipboardService = clipboardService ?? throw new ArgumentNullException(nameof(clipboardService));
|
||||
ToggleVisibilityCommand = new RelayCommand(ToggleVisibility);
|
||||
CopyToClipboardCommand = new RelayCommand(CopyToClipboard);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the secret key.
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the displayed value (masked or actual based on visibility).
|
||||
/// </summary>
|
||||
public string DisplayValue => _isValueVisible ? _actualValue : MaskedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actual unmasked value.
|
||||
/// </summary>
|
||||
public string ActualValue => _actualValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the value is visible.
|
||||
/// </summary>
|
||||
public bool IsValueVisible
|
||||
{
|
||||
get => _isValueVisible;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isValueVisible, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(DisplayValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Command to toggle visibility of the secret value.
|
||||
/// </summary>
|
||||
public ICommand ToggleVisibilityCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Command to copy the secret value to clipboard.
|
||||
/// </summary>
|
||||
public ICommand CopyToClipboardCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when clipboard copy fails.
|
||||
/// </summary>
|
||||
public event Action<string>? OnCopyFailed;
|
||||
|
||||
private void ToggleVisibility()
|
||||
{
|
||||
IsValueVisible = !IsValueVisible;
|
||||
}
|
||||
|
||||
private async void CopyToClipboard()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _clipboardService.SetTextAsync(_actualValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnCopyFailed?.Invoke($"Failed to copy to clipboard: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace JdeScoping.SecureStoreManager.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all view models providing INotifyPropertyChanged implementation.
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>Occurs when a property value changes.</summary>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raises the PropertyChanged event for the specified property.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the property that changed.</param>
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property value and raises PropertyChanged if the value changed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property.</typeparam>
|
||||
/// <param name="field">Reference to the backing field.</param>
|
||||
/// <param name="value">The new value.</param>
|
||||
/// <param name="propertyName">The name of the property.</param>
|
||||
/// <returns>True if the value changed, false otherwise.</returns>
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user