mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-19 23:43:59 -05:00
573 lines
20 KiB
C#
573 lines
20 KiB
C#
//-----------------------------------------------------------------------
|
|
// <copyright file="ItemEdit.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.Client.Main.Models;
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.Linq;
|
|
using AliasClientDb;
|
|
using AliasClientDb.Models;
|
|
using AliasVault.Client.Resources;
|
|
using AliasVault.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Item edit model for add/edit forms.
|
|
/// Uses a dynamic fields-based approach for flexibility when adding new system fields.
|
|
/// </summary>
|
|
public sealed class ItemEdit
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the Id of the item.
|
|
/// </summary>
|
|
public Guid Id { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the item type (Login, Alias, CreditCard, Note).
|
|
/// </summary>
|
|
public string ItemType { get; set; } = "Login";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the name of the service/item.
|
|
/// </summary>
|
|
[Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.ServiceNameRequired))]
|
|
[Display(Name = "Service Name")]
|
|
public string ServiceName { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the logo ID.
|
|
/// </summary>
|
|
public Guid? LogoId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the logo bytes.
|
|
/// </summary>
|
|
public byte[]? ServiceLogo { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the folder ID.
|
|
/// </summary>
|
|
public Guid? FolderId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the create date.
|
|
/// </summary>
|
|
public DateTime CreateDate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the last update date.
|
|
/// </summary>
|
|
public DateTime LastUpdate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the dynamic fields list (both system and custom fields).
|
|
/// </summary>
|
|
public List<SystemFieldEdit> Fields { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Attachment list.
|
|
/// </summary>
|
|
public List<Attachment> Attachments { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets or sets the TOTP codes list.
|
|
/// </summary>
|
|
public List<TotpCode> TotpCodes { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Passkeys list.
|
|
/// </summary>
|
|
public List<Passkey> Passkeys { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// Creates an ItemEdit instance from an Item entity.
|
|
/// </summary>
|
|
/// <param name="item">The item entity to convert.</param>
|
|
/// <returns>A new ItemEdit instance.</returns>
|
|
public static ItemEdit FromEntity(Item item)
|
|
{
|
|
var edit = new ItemEdit
|
|
{
|
|
Id = item.Id,
|
|
ItemType = item.ItemType,
|
|
ServiceName = item.Name ?? string.Empty,
|
|
LogoId = item.LogoId,
|
|
ServiceLogo = item.Logo?.FileData,
|
|
FolderId = item.FolderId,
|
|
Attachments = item.Attachments.Where(a => !a.IsDeleted).ToList(),
|
|
TotpCodes = item.TotpCodes.Where(t => !t.IsDeleted).ToList(),
|
|
Passkeys = item.Passkeys.Where(p => !p.IsDeleted).ToList(),
|
|
CreateDate = item.CreatedAt,
|
|
LastUpdate = item.UpdatedAt,
|
|
};
|
|
|
|
// Group field values by FieldKey to handle multi-value fields
|
|
var groupedFields = item.FieldValues
|
|
.Where(f => !f.IsDeleted)
|
|
.GroupBy(f => f.FieldKey ?? f.FieldDefinitionId?.ToString() ?? string.Empty);
|
|
|
|
foreach (var group in groupedFields)
|
|
{
|
|
var firstFv = group.First();
|
|
var isCustomField = firstFv.FieldDefinitionId != null && string.IsNullOrEmpty(firstFv.FieldKey);
|
|
var systemField = !string.IsNullOrEmpty(firstFv.FieldKey) ? SystemFieldRegistry.GetSystemField(firstFv.FieldKey) : null;
|
|
var isMultiValue = systemField?.IsMultiValue ?? false;
|
|
|
|
var fieldEdit = new SystemFieldEdit
|
|
{
|
|
FieldKey = firstFv.FieldKey ?? firstFv.FieldDefinitionId?.ToString() ?? string.Empty,
|
|
FieldValueId = firstFv.Id,
|
|
FieldDefinitionId = firstFv.FieldDefinitionId,
|
|
Label = isCustomField
|
|
? firstFv.FieldDefinition?.Label ?? "Custom Field"
|
|
: firstFv.FieldKey ?? string.Empty,
|
|
FieldType = isCustomField
|
|
? firstFv.FieldDefinition?.FieldType ?? "Text"
|
|
: systemField?.FieldType ?? "Text",
|
|
IsCustomField = isCustomField,
|
|
IsHidden = isCustomField
|
|
? firstFv.FieldDefinition?.IsHidden ?? false
|
|
: systemField?.IsHidden ?? false,
|
|
EnableHistory = isCustomField
|
|
? firstFv.FieldDefinition?.EnableHistory ?? false
|
|
: systemField?.EnableHistory ?? false,
|
|
DisplayOrder = systemField?.DefaultDisplayOrder ?? firstFv.Weight,
|
|
Category = GetCategoryFromFieldKey(firstFv.FieldKey),
|
|
IsMultiValue = isMultiValue,
|
|
Values = group.OrderBy(fv => fv.Weight).Select(fv => fv.Value ?? string.Empty).ToList(),
|
|
};
|
|
|
|
// Set Value to first value for easier handling with single-value form components
|
|
fieldEdit.Value = fieldEdit.Values.FirstOrDefault() ?? string.Empty;
|
|
|
|
edit.Fields.Add(fieldEdit);
|
|
}
|
|
|
|
return edit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts this ItemEdit instance to an Item entity.
|
|
/// </summary>
|
|
/// <returns>A new Item entity.</returns>
|
|
public Item ToEntity()
|
|
{
|
|
var item = new Item
|
|
{
|
|
Id = Id,
|
|
Name = ServiceName,
|
|
ItemType = ItemType,
|
|
LogoId = LogoId,
|
|
FolderId = FolderId,
|
|
Attachments = Attachments,
|
|
TotpCodes = TotpCodes,
|
|
};
|
|
|
|
// Convert all fields to FieldValue entities
|
|
foreach (var field in Fields)
|
|
{
|
|
// Check if field has any values (single or multi)
|
|
var hasValue = !string.IsNullOrEmpty(field.Value) ||
|
|
(field.IsMultiValue && field.Values.Any(v => !string.IsNullOrEmpty(v)));
|
|
|
|
// For system fields, skip if no value
|
|
// For custom fields, always persist (even if empty) - they're only deleted when explicitly removed
|
|
if (!hasValue && !field.IsCustomField)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (field.IsCustomField)
|
|
{
|
|
// Custom field handling
|
|
var now = DateTime.UtcNow;
|
|
|
|
if (field.FieldDefinitionId == null || field.FieldDefinitionId == Guid.Empty)
|
|
{
|
|
// New custom field - create FieldDefinition
|
|
var fieldDefinitionId = !string.IsNullOrEmpty(field.TempId) && Guid.TryParse(field.TempId, out var parsedGuid)
|
|
? parsedGuid
|
|
: Guid.NewGuid();
|
|
|
|
var fieldDefinition = new FieldDefinition
|
|
{
|
|
Id = fieldDefinitionId,
|
|
FieldType = field.FieldType,
|
|
Label = field.Label,
|
|
IsHidden = field.IsHidden,
|
|
IsMultiValue = false,
|
|
EnableHistory = false,
|
|
Weight = 0,
|
|
CreatedAt = now,
|
|
UpdatedAt = now,
|
|
};
|
|
|
|
item.FieldValues.Add(new FieldValue
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ItemId = item.Id,
|
|
FieldDefinitionId = fieldDefinitionId,
|
|
FieldDefinition = fieldDefinition,
|
|
FieldKey = null,
|
|
Value = field.Value,
|
|
Weight = 0,
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// Existing custom field - update value and include FieldDefinition
|
|
var fieldValue = new FieldValue
|
|
{
|
|
Id = field.FieldValueId != Guid.Empty ? field.FieldValueId : Guid.NewGuid(),
|
|
ItemId = item.Id,
|
|
FieldDefinitionId = field.FieldDefinitionId,
|
|
FieldKey = null,
|
|
Value = field.Value,
|
|
Weight = 0,
|
|
};
|
|
|
|
// Include FieldDefinition with potentially updated label
|
|
fieldValue.FieldDefinition = new FieldDefinition
|
|
{
|
|
Id = field.FieldDefinitionId.Value,
|
|
Label = field.Label,
|
|
FieldType = field.FieldType,
|
|
IsHidden = field.IsHidden,
|
|
IsMultiValue = false,
|
|
EnableHistory = false,
|
|
Weight = 0,
|
|
};
|
|
|
|
item.FieldValues.Add(fieldValue);
|
|
}
|
|
}
|
|
else if (field.IsMultiValue)
|
|
{
|
|
// Multi-value system field (e.g., login.url) - create separate FieldValue for each value
|
|
var nonEmptyValues = field.Values.Where(v => !string.IsNullOrEmpty(v)).ToList();
|
|
for (int i = 0; i < nonEmptyValues.Count; i++)
|
|
{
|
|
item.FieldValues.Add(new FieldValue
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ItemId = item.Id,
|
|
FieldKey = field.FieldKey,
|
|
Value = nonEmptyValues[i],
|
|
Weight = i, // Use index as weight for ordering
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Single-value system field
|
|
ItemService.SetFieldValue(item, field.FieldKey, field.Value);
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of a field by its field key.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key.</param>
|
|
/// <returns>The field value or empty string.</returns>
|
|
public string GetFieldValue(string fieldKey)
|
|
{
|
|
return Fields.FirstOrDefault(f => f.FieldKey == fieldKey)?.Value ?? string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all values for a multi-value field by its field key.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key.</param>
|
|
/// <returns>List of field values.</returns>
|
|
public List<string> GetFieldValues(string fieldKey)
|
|
{
|
|
var field = Fields.FirstOrDefault(f => f.FieldKey == fieldKey);
|
|
if (field == null)
|
|
{
|
|
return new List<string>();
|
|
}
|
|
|
|
return field.IsMultiValue && field.Values.Any()
|
|
? field.Values.Where(v => !string.IsNullOrEmpty(v)).ToList()
|
|
: (string.IsNullOrEmpty(field.Value) ? new List<string>() : new List<string> { field.Value });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets multiple values for a multi-value field.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key.</param>
|
|
/// <param name="values">The values to set.</param>
|
|
public void SetFieldValues(string fieldKey, List<string> values)
|
|
{
|
|
var field = GetField(fieldKey);
|
|
if (field != null)
|
|
{
|
|
field.Values = values ?? new List<string>();
|
|
field.Value = values?.FirstOrDefault() ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a field by its field key.
|
|
/// If the field doesn't exist, creates it based on the system field definition.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key.</param>
|
|
/// <returns>The field or null if not a valid system field.</returns>
|
|
public SystemFieldEdit? GetField(string fieldKey)
|
|
{
|
|
var field = Fields.FirstOrDefault(f => f.FieldKey == fieldKey);
|
|
if (field != null)
|
|
{
|
|
return field;
|
|
}
|
|
|
|
// Field doesn't exist - create it if it's a valid system field
|
|
var systemField = SystemFieldRegistry.GetSystemField(fieldKey);
|
|
if (systemField == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
field = new SystemFieldEdit
|
|
{
|
|
FieldKey = fieldKey,
|
|
Label = fieldKey,
|
|
FieldType = systemField.FieldType,
|
|
Value = string.Empty,
|
|
Values = systemField.IsMultiValue ? new List<string> { string.Empty } : new List<string>(),
|
|
IsCustomField = false,
|
|
IsHidden = systemField.IsHidden,
|
|
EnableHistory = systemField.EnableHistory,
|
|
DisplayOrder = systemField.DefaultDisplayOrder,
|
|
Category = systemField.Category,
|
|
IsMultiValue = systemField.IsMultiValue,
|
|
};
|
|
Fields.Add(field);
|
|
return field;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the value of a field by its field key.
|
|
/// If the field doesn't exist, it will be added.
|
|
/// For multi-value fields, this sets the first value.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key.</param>
|
|
/// <param name="value">The value to set.</param>
|
|
public void SetFieldValue(string fieldKey, string value)
|
|
{
|
|
var field = Fields.FirstOrDefault(f => f.FieldKey == fieldKey);
|
|
if (field != null)
|
|
{
|
|
field.Value = value;
|
|
|
|
// Also update Values list for multi-value fields
|
|
if (field.IsMultiValue)
|
|
{
|
|
field.Values = string.IsNullOrEmpty(value)
|
|
? new List<string> { string.Empty }
|
|
: new List<string> { value };
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Add new field based on system field definition
|
|
var systemField = SystemFieldRegistry.GetSystemField(fieldKey);
|
|
if (systemField != null)
|
|
{
|
|
var isMultiValue = systemField.IsMultiValue;
|
|
Fields.Add(new SystemFieldEdit
|
|
{
|
|
FieldKey = fieldKey,
|
|
Label = fieldKey,
|
|
FieldType = systemField.FieldType,
|
|
Value = value,
|
|
Values = isMultiValue
|
|
? (string.IsNullOrEmpty(value) ? new List<string> { string.Empty } : new List<string> { value })
|
|
: new List<string>(),
|
|
IsCustomField = false,
|
|
IsHidden = systemField.IsHidden,
|
|
EnableHistory = systemField.EnableHistory,
|
|
DisplayOrder = systemField.DefaultDisplayOrder,
|
|
Category = systemField.Category,
|
|
IsMultiValue = isMultiValue,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a field has a non-empty value.
|
|
/// For multi-value fields, checks if any value is non-empty.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key.</param>
|
|
/// <returns>True if the field has a value.</returns>
|
|
public bool HasFieldValue(string fieldKey)
|
|
{
|
|
var field = Fields.FirstOrDefault(f => f.FieldKey == fieldKey);
|
|
if (field == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (field.IsMultiValue)
|
|
{
|
|
return field.Values.Any(v => !string.IsNullOrEmpty(v));
|
|
}
|
|
|
|
return !string.IsNullOrEmpty(field.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all custom fields.
|
|
/// </summary>
|
|
/// <returns>List of custom fields.</returns>
|
|
public List<SystemFieldEdit> GetCustomFields()
|
|
{
|
|
return Fields.Where(f => f.IsCustomField).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all system fields.
|
|
/// </summary>
|
|
/// <returns>List of system fields.</returns>
|
|
public List<SystemFieldEdit> GetSystemFields()
|
|
{
|
|
return Fields.Where(f => !f.IsCustomField).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a new custom field.
|
|
/// </summary>
|
|
/// <param name="label">The field label.</param>
|
|
/// <param name="fieldType">The field type.</param>
|
|
public void AddCustomField(string label, string fieldType)
|
|
{
|
|
var tempId = Guid.NewGuid().ToString();
|
|
Fields.Add(new SystemFieldEdit
|
|
{
|
|
FieldKey = tempId,
|
|
TempId = tempId,
|
|
Label = label,
|
|
FieldType = fieldType,
|
|
Value = string.Empty,
|
|
IsCustomField = true,
|
|
IsHidden = fieldType == FieldType.Hidden || fieldType == FieldType.Password,
|
|
EnableHistory = false,
|
|
DisplayOrder = Fields.Count,
|
|
Category = FieldCategory.Custom,
|
|
IsMultiValue = false,
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a custom field by its field key (TempId or FieldDefinitionId).
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key to remove.</param>
|
|
public void RemoveCustomField(string fieldKey)
|
|
{
|
|
var field = Fields.FirstOrDefault(f => f.FieldKey == fieldKey && f.IsCustomField);
|
|
if (field != null)
|
|
{
|
|
Fields.Remove(field);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the label of a custom field.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key.</param>
|
|
/// <param name="newLabel">The new label.</param>
|
|
public void UpdateCustomFieldLabel(string fieldKey, string newLabel)
|
|
{
|
|
var field = Fields.FirstOrDefault(f => f.FieldKey == fieldKey && f.IsCustomField);
|
|
if (field != null)
|
|
{
|
|
field.Label = newLabel;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a field by its field key and clears its value.
|
|
/// </summary>
|
|
/// <param name="fieldKey">The field key to remove.</param>
|
|
public void RemoveField(string fieldKey)
|
|
{
|
|
var field = Fields.FirstOrDefault(f => f.FieldKey == fieldKey);
|
|
if (field != null)
|
|
{
|
|
if (field.IsCustomField)
|
|
{
|
|
Fields.Remove(field);
|
|
}
|
|
else
|
|
{
|
|
// For system fields, just clear the value (they can be re-added)
|
|
field.Value = string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets fields by category.
|
|
/// </summary>
|
|
/// <param name="category">The category to filter by.</param>
|
|
/// <returns>List of fields in the specified category.</returns>
|
|
public List<SystemFieldEdit> GetFieldsByCategory(FieldCategory category)
|
|
{
|
|
return Fields
|
|
.Where(f => f.Category == category && !string.IsNullOrEmpty(f.Value))
|
|
.OrderBy(f => f.DisplayOrder)
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all field values for a specific item type that don't apply to the new type.
|
|
/// </summary>
|
|
/// <param name="newItemType">The new item type.</param>
|
|
public void ClearFieldsNotApplicableToType(string newItemType)
|
|
{
|
|
foreach (var field in Fields.Where(f => !f.IsCustomField).ToList())
|
|
{
|
|
var systemField = SystemFieldRegistry.GetSystemField(field.FieldKey);
|
|
if (systemField != null && !SystemFieldRegistry.FieldAppliesToType(systemField, newItemType))
|
|
{
|
|
field.Value = string.Empty;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the field category from a field key.
|
|
/// Uses the SystemFieldRegistry to look up the category for known system fields.
|
|
/// Falls back to Custom category for unknown fields.
|
|
/// </summary>
|
|
private static FieldCategory GetCategoryFromFieldKey(string? fieldKey)
|
|
{
|
|
if (string.IsNullOrEmpty(fieldKey))
|
|
{
|
|
return FieldCategory.Custom;
|
|
}
|
|
|
|
// Look up category from SystemFieldRegistry for known system fields
|
|
var systemField = SystemFieldRegistry.GetSystemField(fieldKey);
|
|
if (systemField != null)
|
|
{
|
|
return systemField.Category;
|
|
}
|
|
|
|
// For unknown fields with a system field prefix, this might be a new system field
|
|
// not yet in the registry - treat as custom for now
|
|
return FieldCategory.Custom;
|
|
}
|
|
}
|