mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 15:56:11 -04:00
Add credit card fields to basic CSV export (#1946)
This commit is contained in:
committed by
Leendert de Borst
parent
5fa191bb43
commit
98f0eef484
@@ -35,6 +35,12 @@ interface IItemCsvRecord {
|
||||
AliasLastName: string;
|
||||
AliasNickName: string;
|
||||
AliasBirthDate: string;
|
||||
CardholderName: string;
|
||||
CardNumber: string;
|
||||
CardExpiryMonth: string;
|
||||
CardExpiryYear: string;
|
||||
CardCvv: string;
|
||||
CardPin: string;
|
||||
Notes: string;
|
||||
CreatedAt: string;
|
||||
UpdatedAt: string;
|
||||
@@ -127,6 +133,12 @@ export default function ImportExportScreen(): React.ReactNode {
|
||||
const aliasLastName = getFieldValueAsString(item, 'alias.lastName');
|
||||
const aliasBirthdate = getFieldValueAsString(item, 'alias.birthdate');
|
||||
const notes = getFieldValueAsString(item, 'notes.content');
|
||||
const cardholderName = getFieldValueAsString(item, 'card.cardholder_name');
|
||||
const cardNumber = getFieldValueAsString(item, 'card.number');
|
||||
const cardExpiryMonth = getFieldValueAsString(item, 'card.expiry_month');
|
||||
const cardExpiryYear = getFieldValueAsString(item, 'card.expiry_year');
|
||||
const cardCvv = getFieldValueAsString(item, 'card.cvv');
|
||||
const cardPin = getFieldValueAsString(item, 'card.pin');
|
||||
|
||||
// Parse birthdate to formatted string (server expects MM/DD/YYYY format in CSV)
|
||||
const formattedBirthDate = aliasBirthdate ? formatDate(aliasBirthdate) : '';
|
||||
@@ -144,6 +156,12 @@ export default function ImportExportScreen(): React.ReactNode {
|
||||
AliasLastName: aliasLastName,
|
||||
AliasNickName: '', // NickName is no longer stored as a separate field
|
||||
AliasBirthDate: formattedBirthDate,
|
||||
CardholderName: cardholderName,
|
||||
CardNumber: cardNumber,
|
||||
CardExpiryMonth: cardExpiryMonth,
|
||||
CardExpiryYear: cardExpiryYear,
|
||||
CardCvv: cardCvv,
|
||||
CardPin: cardPin,
|
||||
Notes: notes,
|
||||
CreatedAt: formatDate(item.CreatedAt),
|
||||
UpdatedAt: formatDate(item.UpdatedAt)
|
||||
@@ -166,6 +184,12 @@ export default function ImportExportScreen(): React.ReactNode {
|
||||
'AliasLastName',
|
||||
'AliasNickName',
|
||||
'AliasBirthDate',
|
||||
'CardholderName',
|
||||
'CardNumber',
|
||||
'CardExpiryMonth',
|
||||
'CardExpiryYear',
|
||||
'CardCvv',
|
||||
'CardPin',
|
||||
'Notes',
|
||||
'CreatedAt',
|
||||
'UpdatedAt'
|
||||
@@ -202,6 +226,12 @@ export default function ImportExportScreen(): React.ReactNode {
|
||||
escapeCsvValue(record.AliasLastName),
|
||||
escapeCsvValue(record.AliasNickName),
|
||||
escapeCsvValue(record.AliasBirthDate),
|
||||
escapeCsvValue(record.CardholderName),
|
||||
escapeCsvValue(record.CardNumber),
|
||||
escapeCsvValue(record.CardExpiryMonth),
|
||||
escapeCsvValue(record.CardExpiryYear),
|
||||
escapeCsvValue(record.CardCvv),
|
||||
escapeCsvValue(record.CardPin),
|
||||
escapeCsvValue(record.Notes),
|
||||
escapeCsvValue(record.CreatedAt),
|
||||
escapeCsvValue(record.UpdatedAt)
|
||||
|
||||
@@ -79,6 +79,115 @@ public class ImportExportTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for round-tripping a credit card item through CSV export and import.
|
||||
/// Verifies that card-specific columns are populated on export and that the
|
||||
/// importer recognizes the item as a credit card.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task ImportCreditCardItemFromCsv()
|
||||
{
|
||||
// Arrange
|
||||
var loginItem = new Item
|
||||
{
|
||||
Id = new Guid("00000000-0000-0000-0000-000000000001"),
|
||||
Name = "Login service",
|
||||
ItemType = ItemType.Login,
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now,
|
||||
};
|
||||
AddFieldValue(loginItem, FieldKey.LoginUsername, "loginuser");
|
||||
AddFieldValue(loginItem, FieldKey.LoginPassword, "loginpass");
|
||||
|
||||
var cardItem = new Item
|
||||
{
|
||||
Id = new Guid("00000000-0000-0000-0000-000000000002"),
|
||||
Name = "My Visa",
|
||||
ItemType = ItemType.CreditCard,
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now,
|
||||
};
|
||||
AddFieldValue(cardItem, FieldKey.CardCardholderName, "John Doe");
|
||||
AddFieldValue(cardItem, FieldKey.CardNumber, "4111111111111111");
|
||||
AddFieldValue(cardItem, FieldKey.CardExpiryMonth, "12");
|
||||
AddFieldValue(cardItem, FieldKey.CardExpiryYear, "2030");
|
||||
AddFieldValue(cardItem, FieldKey.CardCvv, "123");
|
||||
AddFieldValue(cardItem, FieldKey.CardPin, "9876");
|
||||
AddFieldValue(cardItem, FieldKey.NotesContent, "Card notes");
|
||||
|
||||
var csvContent = ItemCsvService.ExportItemsToCsv([loginItem, cardItem]);
|
||||
var csvString = System.Text.Encoding.Default.GetString(csvContent);
|
||||
|
||||
// Assert: header includes the new card columns
|
||||
var headerLine = csvString.Split('\n')[0];
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(headerLine, Does.Contain("CardholderName"));
|
||||
Assert.That(headerLine, Does.Contain("CardNumber"));
|
||||
Assert.That(headerLine, Does.Contain("CardExpiryMonth"));
|
||||
Assert.That(headerLine, Does.Contain("CardExpiryYear"));
|
||||
Assert.That(headerLine, Does.Contain("CardCvv"));
|
||||
Assert.That(headerLine, Does.Contain("CardPin"));
|
||||
});
|
||||
|
||||
// Act
|
||||
var importedCredentials = await ItemCsvService.ImportItemsFromCsv(csvString);
|
||||
|
||||
// Assert
|
||||
Assert.That(importedCredentials, Has.Count.EqualTo(2));
|
||||
|
||||
var importedLogin = importedCredentials.First(c => c.ServiceName == "Login service");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(importedLogin.ItemType, Is.Null, "Login items should not have ItemType set by CSV importer.");
|
||||
Assert.That(importedLogin.Creditcard, Is.Null);
|
||||
Assert.That(importedLogin.Username, Is.EqualTo("loginuser"));
|
||||
});
|
||||
|
||||
var importedCard = importedCredentials.First(c => c.ServiceName == "My Visa");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(importedCard.ItemType, Is.EqualTo(ImportedItemType.Creditcard));
|
||||
Assert.That(importedCard.Creditcard, Is.Not.Null);
|
||||
Assert.That(importedCard.Creditcard!.CardholderName, Is.EqualTo("John Doe"));
|
||||
Assert.That(importedCard.Creditcard.Number, Is.EqualTo("4111111111111111"));
|
||||
Assert.That(importedCard.Creditcard.ExpiryMonth, Is.EqualTo("12"));
|
||||
Assert.That(importedCard.Creditcard.ExpiryYear, Is.EqualTo("2030"));
|
||||
Assert.That(importedCard.Creditcard.Cvv, Is.EqualTo("123"));
|
||||
Assert.That(importedCard.Creditcard.Pin, Is.EqualTo("9876"));
|
||||
Assert.That(importedCard.Notes, Is.EqualTo("Card notes"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that CSV files exported by older versions (without credit card columns)
|
||||
/// can still be imported successfully.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task ImportLegacyCsvWithoutCreditCardColumns()
|
||||
{
|
||||
// Arrange: a CSV with the pre-card column set only.
|
||||
var legacyCsv =
|
||||
"ServiceName,FolderPath,ServiceUrl,Username,CurrentPassword,AliasEmail,TwoFactorSecret,AliasGender,AliasFirstName,AliasLastName,AliasNickName,AliasBirthDate,Notes,CreatedAt,UpdatedAt\n"
|
||||
+ "Old Service,,https://old.example,olduser,oldpass,,,,,,,,,2024-01-01 00:00:00,2024-01-01 00:00:00\n";
|
||||
|
||||
// Act
|
||||
var importedCredentials = await ItemCsvService.ImportItemsFromCsv(legacyCsv);
|
||||
|
||||
// Assert
|
||||
Assert.That(importedCredentials, Has.Count.EqualTo(1));
|
||||
var credential = importedCredentials[0];
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(credential.ServiceName, Is.EqualTo("Old Service"));
|
||||
Assert.That(credential.Username, Is.EqualTo("olduser"));
|
||||
Assert.That(credential.ItemType, Is.Null);
|
||||
Assert.That(credential.Creditcard, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test case for importing credentials from Bitwarden CSV and ensuring all values are present.
|
||||
/// </summary>
|
||||
|
||||
@@ -48,6 +48,12 @@ public static class ItemCsvService
|
||||
Notes = GetFieldValue(item, FieldKey.NotesContent),
|
||||
CreatedAt = item.CreatedAt,
|
||||
UpdatedAt = item.UpdatedAt,
|
||||
CardholderName = GetFieldValue(item, FieldKey.CardCardholderName),
|
||||
CardNumber = GetFieldValue(item, FieldKey.CardNumber),
|
||||
CardExpiryMonth = GetFieldValue(item, FieldKey.CardExpiryMonth),
|
||||
CardExpiryYear = GetFieldValue(item, FieldKey.CardExpiryYear),
|
||||
CardCvv = GetFieldValue(item, FieldKey.CardCvv),
|
||||
CardPin = GetFieldValue(item, FieldKey.CardPin),
|
||||
};
|
||||
|
||||
records.Add(record);
|
||||
@@ -117,6 +123,20 @@ public static class ItemCsvService
|
||||
FolderPath = string.IsNullOrWhiteSpace(record.FolderPath) ? null : record.FolderPath,
|
||||
};
|
||||
|
||||
if (HasCardData(record))
|
||||
{
|
||||
credential.ItemType = ImportedItemType.Creditcard;
|
||||
credential.Creditcard = new ImportedCreditcard
|
||||
{
|
||||
CardholderName = record.CardholderName,
|
||||
Number = record.CardNumber,
|
||||
ExpiryMonth = record.CardExpiryMonth,
|
||||
ExpiryYear = record.CardExpiryYear,
|
||||
Cvv = record.CardCvv,
|
||||
Pin = record.CardPin,
|
||||
};
|
||||
}
|
||||
|
||||
credentials.Add(credential);
|
||||
}
|
||||
|
||||
@@ -161,6 +181,21 @@ public static class ItemCsvService
|
||||
?.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the CSV record has any credit card field populated.
|
||||
/// </summary>
|
||||
/// <param name="record">The CSV record to inspect.</param>
|
||||
/// <returns>True if any card field has a non-empty value.</returns>
|
||||
private static bool HasCardData(ItemCsvRecord record)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(record.CardholderName)
|
||||
|| !string.IsNullOrWhiteSpace(record.CardNumber)
|
||||
|| !string.IsNullOrWhiteSpace(record.CardExpiryMonth)
|
||||
|| !string.IsNullOrWhiteSpace(record.CardExpiryYear)
|
||||
|| !string.IsNullOrWhiteSpace(record.CardCvv)
|
||||
|| !string.IsNullOrWhiteSpace(record.CardPin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a birth date string to a DateTime.
|
||||
/// </summary>
|
||||
@@ -248,6 +283,42 @@ public class ItemCsvRecord
|
||||
/// </summary>
|
||||
public DateTime? AliasBirthDate { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credit card cardholder name.
|
||||
/// Empty for non-credit-card items.
|
||||
/// </summary>
|
||||
public string CardholderName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credit card number.
|
||||
/// Empty for non-credit-card items.
|
||||
/// </summary>
|
||||
public string CardNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credit card expiry month (01-12).
|
||||
/// Empty for non-credit-card items.
|
||||
/// </summary>
|
||||
public string CardExpiryMonth { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credit card expiry year (e.g., "2028").
|
||||
/// Empty for non-credit-card items.
|
||||
/// </summary>
|
||||
public string CardExpiryYear { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credit card CVV.
|
||||
/// Empty for non-credit-card items.
|
||||
/// </summary>
|
||||
public string CardCvv { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credit card PIN.
|
||||
/// Empty for non-credit-card items.
|
||||
/// </summary>
|
||||
public string CardPin { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the notes.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user