From 783b2d44ef23f055a4cd27545c74fab7bcde5f4e Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 13 Aug 2025 21:06:54 +0200 Subject: [PATCH] Add Dropbox Passwords import method (#1114) --- .../Components/ImportServiceDropbox.razor | 25 +++++++ .../Settings/ImportExport/ImportExport.razor | 1 + .../ImportExport/ImportServices.en.resx | 9 +++ .../wwwroot/img/importers/dropbox.svg | 4 + .../AliasVault.UnitTests.csproj | 3 +- .../TestData/Exports/dropbox.csv | 6 ++ .../Utilities/ImportExportTests.cs | 50 +++++++++++++ .../Importers/DropboxImporter.cs | 74 +++++++++++++++++++ .../Models/Imports/DropboxCsvRecord.cs | 46 ++++++++++++ 9 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceDropbox.razor create mode 100644 apps/server/AliasVault.Client/wwwroot/img/importers/dropbox.svg create mode 100644 apps/server/Tests/AliasVault.UnitTests/TestData/Exports/dropbox.csv create mode 100644 apps/server/Utilities/AliasVault.ImportExport/Importers/DropboxImporter.cs create mode 100644 apps/server/Utilities/AliasVault.ImportExport/Models/Imports/DropboxCsvRecord.cs diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceDropbox.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceDropbox.razor new file mode 100644 index 000000000..d9c3e8d49 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceDropbox.razor @@ -0,0 +1,25 @@ +@using AliasVault.ImportExport.Models +@using AliasVault.ImportExport.Importers +@using Microsoft.Extensions.Localization +@inject NavigationManager NavigationManager +@inject GlobalNotificationService GlobalNotificationService +@inject IStringLocalizerFactory LocalizerFactory +@inject ILogger Logger + + +

@Localizer["DropboxInstructionsPart1"]

+

@Localizer["UploadFileInstructionCommon"]

+
+ +@code { + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Settings.ImportExport.ImportServices", "AliasVault.Client"); + + private static async Task> ProcessFile(string fileContents) + { + return await DropboxImporter.ImportFromCsvAsync(fileContents); + } +} diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor index a0711ed84..c93febdab 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor @@ -26,6 +26,7 @@ + diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx index 6ade2d393..2ad58d01e 100644 --- a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx @@ -226,6 +226,15 @@ If you have a CSV file back-up of your AliasVault database (from a different AliasVault server), you can import it here. AliasVault import instructions + + + Import passwords from Dropbox Passwords + Description for Dropbox import service + + + In order to import your Dropbox Passwords, you need to export them as a CSV file. You can do this by opening Dropbox Passwords, going to 'Account' > 'Export' (to .CSV). + Dropbox export instructions part 1 + Once you have exported the file, you can upload it below. diff --git a/apps/server/AliasVault.Client/wwwroot/img/importers/dropbox.svg b/apps/server/AliasVault.Client/wwwroot/img/importers/dropbox.svg new file mode 100644 index 000000000..84ac86026 --- /dev/null +++ b/apps/server/AliasVault.Client/wwwroot/img/importers/dropbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/server/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj b/apps/server/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj index 0897983ba..4de5f17a4 100644 --- a/apps/server/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj +++ b/apps/server/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj @@ -62,13 +62,14 @@ + + - diff --git a/apps/server/Tests/AliasVault.UnitTests/TestData/Exports/dropbox.csv b/apps/server/Tests/AliasVault.UnitTests/TestData/Exports/dropbox.csv new file mode 100644 index 000000000..867310ae5 --- /dev/null +++ b/apps/server/Tests/AliasVault.UnitTests/TestData/Exports/dropbox.csv @@ -0,0 +1,6 @@ +Name,URL,Username,Password,Notes +Gmail,https://gmail.com,testuser@gmail.com,gmailpass123,Important email account +Facebook,https://facebook.com,fbuser,fbpass456,Social media account +GitHub,https://github.com,devuser,devpass789,Development platform +Secure Note,,,,Important information stored securely +Test Site,https://test.example.com,testlogin,testpassword,Test notes for example site diff --git a/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs b/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs index 79dcd7c80..2493e7c6a 100644 --- a/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs +++ b/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs @@ -592,6 +592,56 @@ public class ImportExportTests }); } + /// + /// Test case for importing credentials from Dropbox CSV and ensuring all values are present. + /// + /// Async task. + [Test] + public async Task ImportCredentialsFromDropboxCsv() + { + // Arrange + var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.dropbox.csv"); + + // Act + var importedCredentials = await DropboxImporter.ImportFromCsvAsync(fileContent); + + // Assert + Assert.That(importedCredentials, Has.Count.EqualTo(5)); + + // Test Gmail credential + var gmailCredential = importedCredentials.First(c => c.ServiceName == "Gmail"); + Assert.Multiple(() => + { + Assert.That(gmailCredential.ServiceName, Is.EqualTo("Gmail")); + Assert.That(gmailCredential.ServiceUrl, Is.EqualTo("https://gmail.com")); + Assert.That(gmailCredential.Username, Is.EqualTo("testuser@gmail.com")); + Assert.That(gmailCredential.Password, Is.EqualTo("gmailpass123")); + Assert.That(gmailCredential.Notes, Is.EqualTo("Important email account")); + }); + + // Test GitHub credential + var githubCredential = importedCredentials.First(c => c.ServiceName == "GitHub"); + Assert.Multiple(() => + { + Assert.That(githubCredential.ServiceName, Is.EqualTo("GitHub")); + Assert.That(githubCredential.ServiceUrl, Is.EqualTo("https://github.com")); + Assert.That(githubCredential.Username, Is.EqualTo("devuser")); + Assert.That(githubCredential.Password, Is.EqualTo("devpass789")); + Assert.That(githubCredential.Notes, Is.EqualTo("Development platform")); + }); + + // Test Secure Note (no login credentials) + var secureNoteCredential = importedCredentials.First(c => c.ServiceName == "Secure Note"); + Assert.Multiple(() => + { + Assert.That(secureNoteCredential.ServiceName, Is.EqualTo("Secure Note")); + Assert.That(secureNoteCredential.ServiceUrl, Is.Null); + Assert.That(secureNoteCredential.Username, Is.Empty); + Assert.That(secureNoteCredential.Password, Is.Empty); + Assert.That(secureNoteCredential.Notes, Is.EqualTo("Important information stored securely")); + }); + } + /// /// Test case for getting the CSV template structure. /// diff --git a/apps/server/Utilities/AliasVault.ImportExport/Importers/DropboxImporter.cs b/apps/server/Utilities/AliasVault.ImportExport/Importers/DropboxImporter.cs new file mode 100644 index 000000000..844e5c72f --- /dev/null +++ b/apps/server/Utilities/AliasVault.ImportExport/Importers/DropboxImporter.cs @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.ImportExport.Importers; + +using AliasVault.ImportExport.Models; +using AliasVault.ImportExport.Models.Imports; +using CsvHelper; +using CsvHelper.Configuration; +using System.Globalization; + +/// +/// Imports credentials from Dropbox Passwords. +/// +public static class DropboxImporter +{ + /// + /// Imports Dropbox CSV file and converts contents to list of ImportedCredential model objects. + /// + /// The content of the CSV file. + /// The imported list of ImportedCredential objects. + public static async Task> ImportFromCsvAsync(string fileContent) + { + using var reader = new StringReader(fileContent); + using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture)); + + var credentials = new List(); + await foreach (var record in csv.GetRecordsAsync()) + { + // Skip empty records (records with no title) + if (string.IsNullOrWhiteSpace(record.Name)) + { + continue; + } + + var credential = new ImportedCredential + { + ServiceName = record.Name, + ServiceUrl = NormalizeUrl(record.Url), + Username = record.Username, + Password = record.Password, + Notes = record.Notes + }; + + credentials.Add(credential); + } + + if (credentials.Count == 0) + { + throw new InvalidOperationException("No records found in the CSV file."); + } + + return credentials; + } + + /// + /// Normalizes URL values from Dropbox CSV format. + /// + /// The URL from the CSV record. + /// The normalized URL or null if it's empty or invalid. + private static string? NormalizeUrl(string? url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return null; + } + + return url; + } +} diff --git a/apps/server/Utilities/AliasVault.ImportExport/Models/Imports/DropboxCsvRecord.cs b/apps/server/Utilities/AliasVault.ImportExport/Models/Imports/DropboxCsvRecord.cs new file mode 100644 index 000000000..df5845cec --- /dev/null +++ b/apps/server/Utilities/AliasVault.ImportExport/Models/Imports/DropboxCsvRecord.cs @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +using CsvHelper.Configuration.Attributes; + +namespace AliasVault.ImportExport.Models.Imports; + +/// +/// Represents a Dropbox CSV record that is being imported from a Dropbox CSV export file. +/// +public class DropboxCsvRecord +{ + /// + /// Gets or sets the title/service name (e.g., "Facebook", "Gmail"). + /// + [Name("Name")] + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the service URL. + /// + [Name("URL")] + public string? Url { get; set; } + + /// + /// Gets or sets the username/login. + /// + [Name("Username")] + public string? Username { get; set; } + + /// + /// Gets or sets the password. + /// + [Name("Password")] + public string? Password { get; set; } + + /// + /// Gets or sets any additional notes. + /// + [Name("Notes")] + public string? Notes { get; set; } +} \ No newline at end of file