refactor: address code review findings across all projects
Apply comprehensive fixes from code reviews including: - Extract shared utilities (SqlFormatHelper, CellValueConverter, DbDestinationBase) - Add interface abstractions (IAuthenticationService, IDatabaseMigrator, IMisQueryBuilder) - Implement SecureStore for encrypted secrets storage - Fix error handling with proper HTTP status codes and logging - Optimize double enumeration in DevEtlRegistry - Add DataSync.Dev README for developer onboarding - Extract filter panel base classes to reduce duplication - Update code review docs to mark all issues as fixed
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows.Input;
|
||||
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 SecretItemViewModel? _selectedSecret;
|
||||
private string _statusMessage = "Ready";
|
||||
|
||||
public MainWindowViewModel() : this(new Services.SecureStoreManager())
|
||||
{
|
||||
}
|
||||
|
||||
public MainWindowViewModel(ISecureStoreManager storeManager)
|
||||
{
|
||||
_storeManager = storeManager;
|
||||
Secrets = new ObservableCollection<SecretItemViewModel>();
|
||||
|
||||
// File commands
|
||||
NewStoreCommand = new RelayCommand(ExecuteNewStore);
|
||||
OpenStoreCommand = new RelayCommand(ExecuteOpenStore);
|
||||
SaveCommand = new RelayCommand(ExecuteSave, CanSave);
|
||||
CloseStoreCommand = new RelayCommand(ExecuteCloseStore, () => _storeManager.IsStoreOpen);
|
||||
ExitCommand = new RelayCommand(ExecuteExit);
|
||||
|
||||
// Secret commands
|
||||
AddSecretCommand = new RelayCommand(ExecuteAddSecret, () => _storeManager.IsStoreOpen);
|
||||
EditSecretCommand = new RelayCommand(ExecuteEditSecret, CanEditOrDeleteSecret);
|
||||
DeleteSecretCommand = new RelayCommand(ExecuteDeleteSecret, CanEditOrDeleteSecret);
|
||||
|
||||
// Tools commands
|
||||
GenerateKeyFileCommand = new RelayCommand(ExecuteGenerateKeyFile);
|
||||
ExportKeyCommand = new RelayCommand(ExecuteExportKey, () => _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 => SetProperty(ref _selectedSecret, value);
|
||||
}
|
||||
|
||||
/// <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
|
||||
public ICommand NewStoreCommand { get; }
|
||||
public ICommand OpenStoreCommand { get; }
|
||||
public ICommand SaveCommand { get; }
|
||||
public ICommand CloseStoreCommand { get; }
|
||||
public ICommand ExitCommand { get; }
|
||||
|
||||
// Secret Commands
|
||||
public ICommand AddSecretCommand { get; }
|
||||
public ICommand EditSecretCommand { get; }
|
||||
public ICommand DeleteSecretCommand { get; }
|
||||
|
||||
// Tools Commands
|
||||
public ICommand GenerateKeyFileCommand { get; }
|
||||
public ICommand ExportKeyCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new store. Called by the dialog.
|
||||
/// </summary>
|
||||
public async Task CreateNewStoreAsync(string storePath, string? keyFilePath, string? password)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(keyFilePath))
|
||||
{
|
||||
_storeManager.CreateStore(storePath, keyFilePath);
|
||||
StatusMessage = $"Created store with key file: {keyFilePath}";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
_storeManager.CreateStoreWithPassword(storePath, password);
|
||||
StatusMessage = "Created password-protected store";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Either key file path or password must be provided.");
|
||||
}
|
||||
|
||||
RefreshSecrets();
|
||||
NotifyStoreChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error creating store: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to create store:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens an existing store. Called by the dialog.
|
||||
/// </summary>
|
||||
public async Task OpenExistingStoreAsync(string storePath, string? keyFilePath, string? password)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(keyFilePath))
|
||||
{
|
||||
_storeManager.OpenStore(storePath, keyFilePath);
|
||||
StatusMessage = "Opened store with key file";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
_storeManager.OpenStoreWithPassword(storePath, password);
|
||||
StatusMessage = "Opened password-protected store";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Either key file path or password must be provided.");
|
||||
}
|
||||
|
||||
RefreshSecrets();
|
||||
NotifyStoreChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error opening store: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to open store:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a secret. Called by the dialog.
|
||||
/// </summary>
|
||||
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 (OnShowError?.Invoke($"Failed to save secret:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
if (OnShowUnsavedChangesPrompt == null)
|
||||
return true;
|
||||
|
||||
var result = await OnShowUnsavedChangesPrompt();
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case UnsavedChangesResult.Save:
|
||||
await ExecuteSaveAsync();
|
||||
return true;
|
||||
case UnsavedChangesResult.DontSave:
|
||||
return true;
|
||||
case UnsavedChangesResult.Cancel:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteNewStore()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
|
||||
// The view will show the NewStoreDialog
|
||||
OnRequestNewStoreDialog?.Invoke();
|
||||
}
|
||||
|
||||
private async void ExecuteOpenStore()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
|
||||
// The view will show the OpenStoreDialog
|
||||
OnRequestOpenStoreDialog?.Invoke();
|
||||
}
|
||||
|
||||
private async void ExecuteSave()
|
||||
{
|
||||
await ExecuteSaveAsync();
|
||||
}
|
||||
|
||||
private async Task ExecuteSaveAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_storeManager.Save();
|
||||
NotifyStoreChanged();
|
||||
StatusMessage = "Store saved";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error saving: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to save store:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanSave() => _storeManager.IsStoreOpen && _storeManager.HasUnsavedChanges;
|
||||
|
||||
private async void ExecuteCloseStore()
|
||||
{
|
||||
if (!await PromptForUnsavedChangesAsync())
|
||||
return;
|
||||
|
||||
_storeManager.CloseStore();
|
||||
Secrets.Clear();
|
||||
NotifyStoreChanged();
|
||||
StatusMessage = "Store closed";
|
||||
}
|
||||
|
||||
private async void ExecuteExit()
|
||||
{
|
||||
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 void ExecuteDeleteSecret()
|
||||
{
|
||||
if (SelectedSecret == null)
|
||||
return;
|
||||
|
||||
if (OnShowDeleteConfirmation == null)
|
||||
return;
|
||||
|
||||
var confirmed = await OnShowDeleteConfirmation(SelectedSecret.Key);
|
||||
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 (OnShowError?.Invoke($"Failed to delete secret:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanEditOrDeleteSecret() => _storeManager.IsStoreOpen && SelectedSecret != null;
|
||||
|
||||
private async void ExecuteGenerateKeyFile()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var filePath = await OnShowSaveFileDialog("Generate Key File", "Key Files", "*.key", ".key");
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_storeManager.GenerateKeyFile(filePath);
|
||||
StatusMessage = $"Generated key file: {filePath}";
|
||||
await (OnShowInfo?.Invoke($"Key file generated successfully:\n\n{filePath}", "Key Generated") ?? Task.CompletedTask);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error generating key: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to generate key file:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExecuteExportKey()
|
||||
{
|
||||
if (OnShowSaveFileDialog == null)
|
||||
return;
|
||||
|
||||
var filePath = await OnShowSaveFileDialog("Export Key", "Key Files", "*.key", ".key");
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_storeManager.ExportKey(filePath);
|
||||
StatusMessage = $"Exported key to: {filePath}";
|
||||
await (OnShowInfo?.Invoke($"Key exported successfully:\n\n{filePath}", "Key Exported") ?? Task.CompletedTask);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Error exporting key: {ex.Message}";
|
||||
await (OnShowError?.Invoke($"Failed to export key:\n\n{ex.Message}", "Error") ?? Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyStoreChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsStoreOpen));
|
||||
OnPropertyChanged(nameof(HasUnsavedChanges));
|
||||
OnPropertyChanged(nameof(WindowTitle));
|
||||
|
||||
// Manually raise CanExecuteChanged for all commands
|
||||
(SaveCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CloseStoreCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(AddSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(EditSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(DeleteSecretCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(ExportKeyCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
// Events for view to show dialogs (sync)
|
||||
public event Action? OnRequestNewStoreDialog;
|
||||
public event Action? OnRequestOpenStoreDialog;
|
||||
public event Action? OnRequestAddSecretDialog;
|
||||
public event Action<string, string>? OnRequestEditSecretDialog;
|
||||
public event Action? OnRequestClose;
|
||||
|
||||
// Events for view to show dialogs (async)
|
||||
public event Func<string, string, Task>? OnShowError;
|
||||
public event Func<string, string, Task>? OnShowInfo;
|
||||
public event Func<Task<UnsavedChangesResult>>? OnShowUnsavedChangesPrompt;
|
||||
public event Func<string, Task<bool>>? OnShowDeleteConfirmation;
|
||||
public event Func<string, string, string, string, Task<string?>>? OnShowSaveFileDialog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from unsaved changes prompt.
|
||||
/// </summary>
|
||||
public enum UnsavedChangesResult
|
||||
{
|
||||
Save,
|
||||
DontSave,
|
||||
Cancel
|
||||
}
|
||||
Reference in New Issue
Block a user