diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor
index b6deb003b..c4f130ad2 100644
--- a/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor
+++ b/apps/server/AliasVault.Client/Main/Pages/Settings/ImportExport/Components/ImportServiceCard.razor
@@ -1104,18 +1104,80 @@
///
/// Detects and removes duplicates from the import list, and also detects folders.
+ /// Duplicates are identified by matching title, folder path, username, password, and notes (when present).
+ /// For each field: if both items have values, they must match; if both are null/empty, they match; otherwise no match.
///
private async Task DetectAndRemoveDuplicates()
{
- var existingItems = await ItemService.LoadAllAsync();
+ var context = await DbService.GetDbContextAsync();
+
+ // Load all items with folder navigation included
+ var existingItems = await context.Items
+ .Include(x => x.FieldValues.Where(fv => !fv.IsDeleted))
+ .ThenInclude(fv => fv.FieldDefinition)
+ .Include(x => x.Folder)
+ .Where(x => !x.IsDeleted && x.DeletedAt == null)
+ .ToListAsync();
+
var duplicates = ImportedCredentials.Where(imported =>
existingItems.Any(existing =>
- existing.Name != null && existing.Name.Equals(imported.ServiceName, StringComparison.OrdinalIgnoreCase) &&
- ItemService.GetFieldValue(existing, FieldKey.LoginUsername) != null &&
- ItemService.GetFieldValue(existing, FieldKey.LoginUsername)!.Equals(imported.Username, StringComparison.OrdinalIgnoreCase) &&
- ItemService.GetFieldValue(existing, FieldKey.LoginPassword) != null &&
- ItemService.GetFieldValue(existing, FieldKey.LoginPassword)!.Equals(imported.Password, StringComparison.OrdinalIgnoreCase)
- )).ToList();
+ {
+ // Title must match (case-insensitive)
+ if (existing.Name == null || !existing.Name.Equals(imported.ServiceName, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Folder path must match (case-insensitive)
+ var existingFolderPath = existing.Folder?.Name ?? string.Empty;
+ var importedFolderPath = imported.FolderPath ?? string.Empty;
+ if (!existingFolderPath.Equals(importedFolderPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Helper function to compare nullable fields
+ bool FieldsMatch(string? existingValue, string? importedValue)
+ {
+ bool existingHasValue = !string.IsNullOrEmpty(existingValue);
+ bool importedHasValue = !string.IsNullOrEmpty(importedValue);
+
+ // If both have values, they must match (case-insensitive)
+ if (existingHasValue && importedHasValue)
+ {
+ return existingValue!.Equals(importedValue, StringComparison.OrdinalIgnoreCase);
+ }
+
+ // If both are empty/null, they match
+ if (!existingHasValue && !importedHasValue)
+ {
+ return true;
+ }
+
+ // If only one has a value, they don't match
+ return false;
+ }
+
+ // Username must match
+ if (!FieldsMatch(ItemService.GetFieldValue(existing, FieldKey.LoginUsername), imported.Username))
+ {
+ return false;
+ }
+
+ // Password must match
+ if (!FieldsMatch(ItemService.GetFieldValue(existing, FieldKey.LoginPassword), imported.Password))
+ {
+ return false;
+ }
+
+ // Notes must match
+ if (!FieldsMatch(ItemService.GetFieldValue(existing, FieldKey.NotesContent), imported.Notes))
+ {
+ return false;
+ }
+
+ return true;
+ })).ToList();
DuplicateCredentialsCount = duplicates.Count;
diff --git a/apps/server/Utilities/AliasVault.ImportExport/AliasVault.ImportExport.csproj b/apps/server/Utilities/AliasVault.ImportExport/AliasVault.ImportExport.csproj
index 7b01f9d5d..3dba352f3 100644
--- a/apps/server/Utilities/AliasVault.ImportExport/AliasVault.ImportExport.csproj
+++ b/apps/server/Utilities/AliasVault.ImportExport/AliasVault.ImportExport.csproj
@@ -17,6 +17,7 @@
+