@DuplicateCredentialsCount duplicate credential(s) were found and will not be imported.
}
-
+
@if (ImportedCredentials.Count == 0)
{
@@ -369,7 +369,7 @@
ImportError = null;
ImportSuccessMessage = null;
StateHasChanged();
-
+
// Let UI update to start showing the loading indicator
await Task.Delay(50);
}
@@ -479,6 +479,12 @@
return;
}
+ if (e.File.Name.EndsWith(".zip"))
+ {
+ ImportError = $"Please unzip the {ServiceName} export file before importing, please read the instructions below for more information.";
+ return;
+ }
+
try
{
IsImporting = true;
@@ -500,10 +506,10 @@
await Task.WhenAll(processingTask, delayTask);
ImportedCredentials = await processingTask;
-
+
// Detect and remove duplicates before showing the preview
await DetectAndRemoveDuplicates();
-
+
CurrentStep = ImportStep.Preview;
}
catch (Exception ex)
@@ -532,7 +538,7 @@
)).ToList();
DuplicateCredentialsCount = duplicates.Count;
-
+
// Remove duplicates from the import list
ImportedCredentials = ImportedCredentials.Except(duplicates).ToList();
}
diff --git a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceDashlane.razor b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceDashlane.razor
new file mode 100644
index 000000000..52e134928
--- /dev/null
+++ b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceDashlane.razor
@@ -0,0 +1,21 @@
+@using AliasVault.ImportExport.Models
+@using AliasVault.ImportExport.Importers
+@inject NavigationManager NavigationManager
+@inject GlobalNotificationService GlobalNotificationService
+@inject ILogger Logger
+
+
+
In order to import your Dashlane passwords, you need to export it as a CSV file. You can do this by logging into your Dashlane account, going to the 'Account' > 'Settings' menu and selecting 'Export to CSV'.
+
Note: the .zip file you download will contain a "credentials.csv" file. You need to unzip the archive first, and then upload the "credentials.csv" CSV file below.
+
+
+@code {
+ private static async Task> ProcessFile(string fileContents)
+ {
+ return await DashlaneImporter.ImportFromCsvAsync(fileContents);
+ }
+}
diff --git a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor
index d81bf2ef1..275562a3f 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/ImportExport/ImportExport.razor
@@ -24,6 +24,7 @@
+
diff --git a/src/AliasVault.Client/wwwroot/img/importers/dashlane.svg b/src/AliasVault.Client/wwwroot/img/importers/dashlane.svg
new file mode 100644
index 000000000..612b63ba8
--- /dev/null
+++ b/src/AliasVault.Client/wwwroot/img/importers/dashlane.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj b/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj
index efd702f83..585aa6602 100644
--- a/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj
+++ b/src/Tests/AliasVault.UnitTests/AliasVault.UnitTests.csproj
@@ -69,6 +69,7 @@
+
diff --git a/src/Tests/AliasVault.UnitTests/TestData/Exports/dashlane.csv b/src/Tests/AliasVault.UnitTests/TestData/Exports/dashlane.csv
new file mode 100644
index 000000000..157518a15
--- /dev/null
+++ b/src/Tests/AliasVault.UnitTests/TestData/Exports/dashlane.csv
@@ -0,0 +1,4 @@
+username,username2,username3,title,password,note,url,category,otpUrl
+Test username,,,Test,password123,,https://Test,,
+googleuser,,,Google,googlepassword,,https://www.google.com,,
+testusername,testusernamealternative,,Local,testpassword,testnote,https://www.testwebsite.local,,
\ No newline at end of file
diff --git a/src/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs b/src/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs
index 59c72c9fc..2fa73ba90 100644
--- a/src/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs
+++ b/src/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs
@@ -406,4 +406,52 @@ public class ImportExportTests
Assert.That(testWithEmailCredential.Email, Is.EqualTo("testalias.gating981@passinbox.com"));
});
}
+
+ ///
+ /// Test case for importing credentials from Dashlane CSV and ensuring all values are present.
+ ///
+ /// Async task.
+ [Test]
+ public async Task ImportCredentialsFromDashlaneCsv()
+ {
+ // Arrange
+ var fileContent = await ResourceReaderUtility.ReadEmbeddedResourceStringAsync("AliasVault.UnitTests.TestData.Exports.dashlane.csv");
+
+ // Act
+ var importedCredentials = await DashlaneImporter.ImportFromCsvAsync(fileContent);
+
+ // Assert
+ Assert.That(importedCredentials, Has.Count.EqualTo(3));
+
+ // Test specific entries
+ var testCredential = importedCredentials.First(c => c.ServiceName == "Test");
+ Assert.Multiple(() =>
+ {
+ Assert.That(testCredential.ServiceName, Is.EqualTo("Test"));
+ Assert.That(testCredential.ServiceUrl, Is.EqualTo("https://Test"));
+ Assert.That(testCredential.Username, Is.EqualTo("Test username"));
+ Assert.That(testCredential.Password, Is.EqualTo("password123"));
+ Assert.That(testCredential.Notes, Is.Null);
+ });
+
+ var googleCredential = importedCredentials.First(c => c.ServiceName == "Google");
+ Assert.Multiple(() =>
+ {
+ Assert.That(googleCredential.ServiceName, Is.EqualTo("Google"));
+ Assert.That(googleCredential.ServiceUrl, Is.EqualTo("https://www.google.com"));
+ Assert.That(googleCredential.Username, Is.EqualTo("googleuser"));
+ Assert.That(googleCredential.Password, Is.EqualTo("googlepassword"));
+ Assert.That(googleCredential.Notes, Is.Null);
+ });
+
+ var localCredential = importedCredentials.First(c => c.ServiceName == "Local");
+ Assert.Multiple(() =>
+ {
+ Assert.That(localCredential.ServiceName, Is.EqualTo("Local"));
+ Assert.That(localCredential.ServiceUrl, Is.EqualTo("https://www.testwebsite.local"));
+ Assert.That(localCredential.Username, Is.EqualTo("testusername"));
+ Assert.That(localCredential.Password, Is.EqualTo("testpassword"));
+ Assert.That(localCredential.Notes, Is.EqualTo("testnote\nAlternative username 1: testusernamealternative"));
+ });
+ }
}
diff --git a/src/Utilities/AliasVault.ImportExport/Importers/DashlaneImporter.cs b/src/Utilities/AliasVault.ImportExport/Importers/DashlaneImporter.cs
new file mode 100644
index 000000000..1847c0efe
--- /dev/null
+++ b/src/Utilities/AliasVault.ImportExport/Importers/DashlaneImporter.cs
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT 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 Dashlane.
+///
+public static class DashlaneImporter
+{
+ ///
+ /// Imports Dashlane 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())
+ {
+ var credential = new ImportedCredential
+ {
+ ServiceName = record.Title,
+ ServiceUrl = record.URL,
+ Username = record.Username,
+ Password = record.Password,
+ TwoFactorSecret = record.OTPUrl,
+ Notes = BuildNotes(record)
+ };
+
+ credentials.Add(credential);
+ }
+
+ if (credentials.Count == 0)
+ {
+ throw new InvalidOperationException("No records found in the CSV file.");
+ }
+
+ return credentials;
+ }
+
+ private static string? BuildNotes(DashlaneCsvRecord record)
+ {
+ var notes = new List();
+
+ if (!string.IsNullOrEmpty(record.Note))
+ {
+ notes.Add(record.Note);
+ }
+
+ if (!string.IsNullOrEmpty(record.Username2))
+ {
+ notes.Add($"Alternative username 1: {record.Username2}");
+ }
+
+ if (!string.IsNullOrEmpty(record.Username3))
+ {
+ notes.Add($"Alternative username 2: {record.Username3}");
+ }
+
+ if (!string.IsNullOrEmpty(record.Category))
+ {
+ notes.Add($"Category: {record.Category}");
+ }
+
+ return notes.Count > 0 ? string.Join(Environment.NewLine, notes) : null;
+ }
+}
\ No newline at end of file
diff --git a/src/Utilities/AliasVault.ImportExport/Models/Imports/DashlaneCsvRecord.cs b/src/Utilities/AliasVault.ImportExport/Models/Imports/DashlaneCsvRecord.cs
new file mode 100644
index 000000000..45060a361
--- /dev/null
+++ b/src/Utilities/AliasVault.ImportExport/Models/Imports/DashlaneCsvRecord.cs
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+using AliasVault.ImportExport.Converters;
+using CsvHelper.Configuration.Attributes;
+
+namespace AliasVault.ImportExport.Models.Imports;
+
+///
+/// Represents a Dashlane CSV record that is being imported from a Dashlane CSV export file.
+///
+public class DashlaneCsvRecord
+{
+ ///
+ /// Gets or sets the primary username.
+ ///
+ [Name("username")]
+ public string Username { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the second username.
+ ///
+ [Name("username2")]
+ public string? Username2 { get; set; }
+
+ ///
+ /// Gets or sets the third username.
+ ///
+ [Name("username3")]
+ public string? Username3 { get; set; }
+
+ ///
+ /// Gets or sets the title/service name.
+ ///
+ [Name("title")]
+ public string Title { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the password.
+ ///
+ [Name("password")]
+ public string? Password { get; set; }
+
+ ///
+ /// Gets or sets any additional notes.
+ ///
+ [Name("note")]
+ public string? Note { get; set; }
+
+ ///
+ /// Gets or sets the service URL.
+ ///
+ [Name("url")]
+ public string? URL { get; set; }
+
+ ///
+ /// Gets or sets the category.
+ ///
+ [Name("category")]
+ public string? Category { get; set; }
+
+ ///
+ /// Gets or sets the OTP URL.
+ ///
+ [Name("otpUrl")]
+ public string? OTPUrl { get; set; }
+}