Files
2026-04-26 20:37:04 +02:00

337 lines
12 KiB
C#

//-----------------------------------------------------------------------
// <copyright file="ItemCsvService.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.ImportExport;
using AliasClientDb;
using AliasClientDb.Models;
using AliasVault.ImportExport.Importers;
using AliasVault.ImportExport.Models;
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
/// <summary>
/// Exports and imports Item objects to and from CSV files.
/// </summary>
public static class ItemCsvService
{
/// <summary>
/// Export list of items to CSV file.
/// </summary>
/// <param name="items">List of items to export.</param>
/// <returns>CSV file as byte array.</returns>
public static byte[] ExportItemsToCsv(List<Item> items)
{
var records = new List<ItemCsvRecord>();
foreach (var item in items)
{
var record = new ItemCsvRecord
{
ServiceName = item.Name ?? string.Empty,
FolderPath = BuildFolderPath(item.Folder),
ServiceUrl = GetFieldValue(item, FieldKey.LoginUrl),
Username = GetFieldValue(item, FieldKey.LoginUsername),
CurrentPassword = GetFieldValue(item, FieldKey.LoginPassword),
AliasEmail = GetFieldValue(item, FieldKey.LoginEmail),
TwoFactorSecret = item.TotpCodes.FirstOrDefault(t => !t.IsDeleted)?.SecretKey ?? string.Empty,
AliasGender = GetFieldValue(item, FieldKey.AliasGender),
AliasFirstName = GetFieldValue(item, FieldKey.AliasFirstName),
AliasLastName = GetFieldValue(item, FieldKey.AliasLastName),
AliasNickName = string.Empty, // NickName is no longer stored as a separate field
AliasBirthDate = ParseBirthDate(GetFieldValue(item, FieldKey.AliasBirthdate)),
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);
}
using var memoryStream = new MemoryStream();
using var writer = new StreamWriter(memoryStream);
using var csv = new CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture));
csv.WriteRecords(records);
writer.Flush();
return memoryStream.ToArray();
}
/// <summary>
/// Imports Item objects from a CSV file.
/// </summary>
/// <param name="fileContent">The content of the CSV file.</param>
/// <returns>The imported list of ImportedCredential objects.</returns>
public static async Task<List<ImportedCredential>> ImportItemsFromCsv(string fileContent)
{
using var reader = new StringReader(fileContent);
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
// Allow missing headers - fields that don't exist will be skipped
HeaderValidated = null,
MissingFieldFound = null,
};
using var csv = new CsvReader(reader, config);
var records = new List<ItemCsvRecord>();
await foreach (var record in csv.GetRecordsAsync<ItemCsvRecord>())
{
records.Add(record);
}
if (records.Count == 0)
{
throw new InvalidOperationException("No records found in the CSV file.");
}
var credentials = new List<ImportedCredential>();
foreach (var record in records)
{
var credential = new ImportedCredential
{
ServiceName = record.ServiceName,
ServiceUrls = BaseImporter.ParseUrls(record.ServiceUrl),
Username = record.Username,
Password = record.CurrentPassword,
Email = record.AliasEmail,
Notes = record.Notes,
Alias = new ImportedAlias
{
Gender = record.AliasGender,
FirstName = record.AliasFirstName,
LastName = record.AliasLastName,
NickName = record.AliasNickName,
BirthDate = record.AliasBirthDate,
CreatedAt = record.CreatedAt,
UpdatedAt = record.UpdatedAt,
},
TwoFactorSecret = record.TwoFactorSecret,
CreatedAt = record.CreatedAt,
UpdatedAt = record.UpdatedAt,
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);
}
return credentials;
}
/// <summary>
/// Builds the full hierarchical path for a folder by traversing parent folders.
/// </summary>
/// <param name="folder">The folder to build the path for.</param>
/// <returns>The full hierarchical path (e.g., "Work/Projects/Active"), or empty string if null.</returns>
private static string BuildFolderPath(Folder? folder)
{
if (folder == null)
{
return string.Empty;
}
var pathParts = new List<string>();
var currentFolder = folder;
// Traverse up the folder hierarchy
while (currentFolder != null)
{
pathParts.Insert(0, currentFolder.Name);
currentFolder = currentFolder.ParentFolder;
}
return string.Join("/", pathParts);
}
/// <summary>
/// Gets a field value from an item by field key.
/// </summary>
/// <param name="item">The item to get the field value from.</param>
/// <param name="fieldKey">The field key to look up.</param>
/// <returns>The field value, or empty string if not found.</returns>
private static string GetFieldValue(Item item, string fieldKey)
{
return item.FieldValues
.FirstOrDefault(fv => fv.FieldKey == fieldKey && !fv.IsDeleted)
?.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>
/// <param name="birthDateStr">The birth date string in yyyy-MM-dd format.</param>
/// <returns>The parsed DateTime, or null if the string is empty or invalid.</returns>
private static DateTime? ParseBirthDate(string birthDateStr)
{
if (string.IsNullOrEmpty(birthDateStr))
{
return null;
}
if (DateTime.TryParseExact(birthDateStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
{
return date;
}
return null;
}
}
/// <summary>
/// CSV record for Item objects.
/// </summary>
public class ItemCsvRecord
{
/// <summary>
/// Gets or sets the service name.
/// </summary>
public string ServiceName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the folder path.
/// Added in version 1.7.0.
/// </summary>
public string FolderPath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the service URL.
/// </summary>
public string ServiceUrl { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the username.
/// </summary>
public string Username { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the current password.
/// </summary>
public string CurrentPassword { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the alias email.
/// </summary>
public string AliasEmail { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the two-factor secret.
/// </summary>
public string TwoFactorSecret { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the alias gender.
/// </summary>
public string AliasGender { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the alias first name.
/// </summary>
public string AliasFirstName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the alias last name.
/// </summary>
public string AliasLastName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the alias nickname (legacy field, no longer used).
/// </summary>
public string AliasNickName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the alias birth date.
/// </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>
public string Notes { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the created timestamp.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.MinValue;
/// <summary>
/// Gets or sets the updated timestamp.
/// </summary>
public DateTime UpdatedAt { get; set; } = DateTime.MinValue;
}