From a2c2caed79105bd6817807f242b5147c3448fb90 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 25 Mar 2025 18:37:33 +0100 Subject: [PATCH] Add multistep import flow (#542) --- .../Components/ImportServiceAliasVault.razor | 42 +++++- .../Components/ImportServiceBitwarden.razor | 19 +-- .../Components/ImportServiceCard.razor | 139 +++++++++++++++--- .../Components/ImportServiceKeePass.razor | 14 +- .../wwwroot/css/tailwind.css | 103 +++++++++++++ 5 files changed, 272 insertions(+), 45 deletions(-) diff --git a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceAliasVault.razor b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceAliasVault.razor index 53e432e2f..83fbaed92 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceAliasVault.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceAliasVault.razor @@ -1,28 +1,54 @@ @inject ILogger Logger +@inject NavigationManager NavigationManager +@inject GlobalNotificationService GlobalNotificationService +@using AliasVault.ImportExport.Models + OnImportConfirmed="ProcessFile" + @ref="_importServiceCard">

Upload your AliasVault export file:

+
-
@code { - private async Task HandleFileUpload(InputFileChangeEventArgs e) + private ImportServiceCard _importServiceCard = null!; + private IBrowserFile? _selectedFile; + + private void HandleFileUpload(InputFileChangeEventArgs e) { - // AliasVault specific file upload and parsing logic - await Task.Delay(500); - Logger.LogInformation($"Processing AliasVault file: {e.File.Name}"); + Logger.LogInformation($"File selected: {e.File.Name}"); + _selectedFile = e.File; + } + + private async Task ProcessFile() + { + if (_selectedFile == null) + { + throw new ArgumentException("Please select a valid AliasVault export file to import"); + } + + Logger.LogInformation($"Processing AliasVault file: {_selectedFile.Name}"); + + try + { + await using var stream = _selectedFile.OpenReadStream(); + using var reader = new StreamReader(stream); + var fileContents = await reader.ReadToEndAsync(); + } + catch + { + throw new ArgumentException("Error processing AliasVault file. Please check the file format and try again."); + } } private void RefreshVault() { - // Refresh the vault - // ... + GlobalNotificationService.AddSuccessMessage("AliasVault import successful!"); } } \ No newline at end of file diff --git a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceBitwarden.razor b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceBitwarden.razor index 8bfef0ee2..ee68a7acd 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceBitwarden.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceBitwarden.razor @@ -12,14 +12,13 @@ @ref="_importServiceCard">

Upload your Bitwarden CSV export file:

+
- @code { private ImportServiceCard _importServiceCard = null!; - - private IBrowserFile? _selectedFile; + private IBrowserFile? _selectedFile; private void HandleFileUpload(InputFileChangeEventArgs e) { @@ -29,13 +28,12 @@ private async Task ProcessFile() { - if (_selectedFile == null) + if (_selectedFile == null || string.IsNullOrEmpty(_selectedFile.Name)) { - Logger.LogWarning("No file selected for import"); - return; + throw new ArgumentException("Please select a valid Bitwarden CSV file to import"); } - Logger.LogInformation($"Processing KeePass file: {_selectedFile.Name}"); + Logger.LogInformation($"Processing Bitwarden file: {_selectedFile.Name}"); try { @@ -46,15 +44,14 @@ var importCredentials = await BitwardenImporter.ImportFromCsvAsync(fileContents); _importServiceCard.SetImportedCredentials(importCredentials); } - catch (Exception ex) + catch { - Logger.LogError(ex, "Error processing KeePass CSV file"); - throw; + throw new ArgumentException("Error processing Bitwarden CSV file. Please check the file format and try again."); } } + private void RefreshVault() { GlobalNotificationService.AddSuccessMessage("Bitwarden CSV import successful!"); - NavigationManager.NavigateTo("/credentials"); } } diff --git a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor index 7d1bb7f7c..ec5f02b78 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor @@ -2,6 +2,7 @@ @inject IJSRuntime JSRuntime @inject CredentialService CredentialService @inject DbService DbService +@inject NavigationManager NavigationManager @using AliasVault.ImportExport.Importers @using AliasVault.ImportExport.Models @@ -53,13 +54,70 @@ } else { - @ChildContent - } + @switch (CurrentStep) + { + case ImportStep.FileUpload: + @if (!string.IsNullOrEmpty(ImportError)) + { + + } +
+ @ChildContent +
+
+ + +
+ break; -
- - -
+ case ImportStep.Preview: +
+

Check if the credentials are correct before importing:

+
+ + + + + + + + + + @foreach (var credential in ImportedCredentials.Take(10)) + { + + + + + + } + +
ServiceUsernamePassword
@credential.ServiceName@credential.Username@(new string('*', credential.Password?.Length ?? 0))
+ @if (ImportedCredentials.Count > 10) + { +

... and @(ImportedCredentials.Count - 10) more credentials

+ } +
+
+
+ + +
+ break; + + case ImportStep.Confirm: +
+

Are you sure you want to import @ImportedCredentials.Count credentials?

+
+
+ + +
+ break; + } + } @@ -67,6 +125,13 @@ } @code { + private enum ImportStep + { + FileUpload, + Preview, + Confirm + } + /// /// /// @@ -96,8 +161,9 @@ private bool IsModalOpen { get; set; } = false; private bool IsImporting { get; set; } = false; - private string? ImportErrorMessage { get; set; } + private string? ImportError { get; set; } private string? ImportSuccessMessage { get; set; } + private ImportStep CurrentStep { get; set; } = ImportStep.FileUpload; /// /// Child content which is shown in the modal popup. This can contain custom instructions. @@ -125,6 +191,7 @@ protected virtual void OpenImportModal() { IsModalOpen = true; + CurrentStep = ImportStep.FileUpload; StateHasChanged(); } @@ -134,8 +201,50 @@ protected virtual void CloseModal() { IsModalOpen = false; - ImportErrorMessage = null; + CurrentStep = ImportStep.FileUpload; + ImportError = null; ImportSuccessMessage = null; + ImportedCredentials.Clear(); + StateHasChanged(); + } + + protected virtual async Task HandleNextStep() + { + if (CurrentStep == ImportStep.FileUpload) + { + try + { + ImportError = null; + await OnImportConfirmed.InvokeAsync(); + if (ImportedCredentials.Count > 0) + { + CurrentStep = ImportStep.Preview; + } + } + catch (Exception ex) + { + ImportError = ex.Message; + Logger.LogError(ex, "Error during import confirmation"); + StateHasChanged(); + } + } + else if (CurrentStep == ImportStep.Preview) + { + CurrentStep = ImportStep.Confirm; + } + StateHasChanged(); + } + + protected virtual void HandlePreviousStep() + { + if (CurrentStep == ImportStep.Confirm) + { + CurrentStep = ImportStep.Preview; + } + else if (CurrentStep == ImportStep.Preview) + { + CurrentStep = ImportStep.FileUpload; + } StateHasChanged(); } @@ -150,15 +259,12 @@ } IsImporting = true; - ImportErrorMessage = null; + ImportError = null; ImportSuccessMessage = null; StateHasChanged(); try { - // Call the OnImportConfirmed callback first - await OnImportConfirmed.InvokeAsync(); - // Convert imported credentials to AliasVault format and insert them to the database. var credentials = BaseImporter.ConvertToCredential(ImportedCredentials); foreach (var credential in credentials) @@ -171,24 +277,23 @@ if (success) { ImportSuccessMessage = $"Successfully imported {ImportedCredentials.Count} credentials."; + await OnImportComplete.InvokeAsync(); + NavigationManager.NavigateTo("/credentials"); } else { - ImportErrorMessage = "Error saving database."; + ImportError = "Error saving database."; } } catch (Exception ex) { Logger.LogError(ex, "Error importing credentials"); - ImportErrorMessage = $"Error importing credentials: {ex.Message}"; + ImportError = $"Error importing credentials: {ex.Message}"; } finally { IsImporting = false; StateHasChanged(); } - - await OnImportComplete.InvokeAsync(); - CloseModal(); } } diff --git a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceKeePass.razor b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceKeePass.razor index 6387a9400..3f475a5d0 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceKeePass.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceKeePass.razor @@ -12,8 +12,8 @@ @ref="_importServiceCard">

Upload your KeePass export file:

+
- @code { @@ -28,12 +28,10 @@ private async Task ProcessFile() { - if (_selectedFile == null) + if (_selectedFile == null || string.IsNullOrEmpty(_selectedFile.Name)) { - Logger.LogWarning("No file selected for import"); - return; + throw new ArgumentException("Please select a valid KeePass CSV file to import"); } - Logger.LogInformation($"Processing KeePass file: {_selectedFile.Name}"); try @@ -45,16 +43,14 @@ var importCredentials = await KeePassImporter.ImportFromCsvAsync(fileContents); _importServiceCard.SetImportedCredentials(importCredentials); } - catch (Exception ex) + catch { - Logger.LogError(ex, "Error processing KeePass CSV file"); - throw; + throw new ArgumentException("Error processing KeePass CSV file. Please check the file format and try again."); } } private void RefreshVault() { GlobalNotificationService.AddSuccessMessage("KeePass CSV import successful!"); - NavigationManager.NavigateTo("/credentials"); } } diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css index 89fe87363..af5daa3ff 100644 --- a/src/AliasVault.Client/wwwroot/css/tailwind.css +++ b/src/AliasVault.Client/wwwroot/css/tailwind.css @@ -741,6 +741,10 @@ video { margin-bottom: 1rem; } +.mb-5 { + margin-bottom: 1.25rem; +} + .mb-6 { margin-bottom: 1.5rem; } @@ -833,6 +837,10 @@ video { margin-top: 2rem; } +.mt-auto { + margin-top: auto; +} + .line-clamp-2 { overflow: hidden; display: -webkit-box; @@ -1028,6 +1036,10 @@ video { max-width: 80rem; } +.max-w-lg { + max-width: 32rem; +} + .max-w-md { max-width: 28rem; } @@ -1609,6 +1621,11 @@ video { fill: #d68338; } +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + .\!p-0 { padding: 0px !important; } @@ -1984,6 +2001,11 @@ video { color: rgb(220 38 38 / var(--tw-text-opacity)); } +.text-red-700 { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity)); +} + .text-red-800 { --tw-text-opacity: 1; color: rgb(153 27 27 / var(--tw-text-opacity)); @@ -2109,6 +2131,47 @@ video { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } +.file\:mr-4::file-selector-button { + margin-right: 1rem; +} + +.file\:rounded-lg::file-selector-button { + border-radius: 0.5rem; +} + +.file\:border-0::file-selector-button { + border-width: 0px; +} + +.file\:bg-blue-50::file-selector-button { + --tw-bg-opacity: 1; + background-color: rgb(239 246 255 / var(--tw-bg-opacity)); +} + +.file\:px-4::file-selector-button { + padding-left: 1rem; + padding-right: 1rem; +} + +.file\:py-2::file-selector-button { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.file\:text-sm::file-selector-button { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.file\:font-semibold::file-selector-button { + font-weight: 600; +} + +.file\:text-blue-700::file-selector-button { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity)); +} + .hover\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; @@ -2235,6 +2298,11 @@ video { color: rgb(107 114 128 / var(--tw-text-opacity)); } +.hover\:text-gray-700:hover { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + .hover\:text-gray-800:hover { --tw-text-opacity: 1; color: rgb(31 41 55 / var(--tw-text-opacity)); @@ -2279,6 +2347,11 @@ video { text-decoration-line: underline; } +.hover\:file\:bg-blue-100::file-selector-button:hover { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + .focus\:border-blue-500:focus { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); @@ -2529,6 +2602,11 @@ video { background-color: rgb(123 74 30 / var(--tw-bg-opacity)); } +.dark\:bg-red-200:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(254 202 202 / var(--tw-bg-opacity)); +} + .dark\:bg-red-600:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(220 38 38 / var(--tw-bg-opacity)); @@ -2647,6 +2725,11 @@ video { color: rgb(239 68 68 / var(--tw-text-opacity)); } +.dark\:text-red-800:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity)); +} + .dark\:text-white:is(.dark *) { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); @@ -2676,6 +2759,16 @@ video { --tw-ring-offset-color: #1f2937; } +.dark\:file\:bg-gray-700:is(.dark *)::file-selector-button { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + +.dark\:file\:text-gray-300:is(.dark *)::file-selector-button { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + .dark\:hover\:bg-blue-500:hover:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(59 130 246 / var(--tw-bg-opacity)); @@ -2756,6 +2849,11 @@ video { color: rgb(229 231 235 / var(--tw-text-opacity)); } +.dark\:hover\:text-gray-300:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + .dark\:hover\:text-primary-300:hover:is(.dark *) { --tw-text-opacity: 1; color: rgb(248 185 99 / var(--tw-text-opacity)); @@ -2781,6 +2879,11 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.dark\:hover\:file\:bg-gray-600:is(.dark *)::file-selector-button:hover { + --tw-bg-opacity: 1; + background-color: rgb(75 85 99 / var(--tw-bg-opacity)); +} + .dark\:focus\:border-blue-500:focus:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity));