mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-20 07:54:10 -05:00
1286 lines
57 KiB
Plaintext
1286 lines
57 KiB
Plaintext
@page "/items/create"
|
|
@page "/items/{id:guid}/edit"
|
|
@inherits MainBase
|
|
@inject ItemService ItemService
|
|
@inject IJSRuntime JSRuntime
|
|
@inject AliasVault.Client.Services.QuickCreateStateService QuickCreateStateService
|
|
@using AliasVault.Client.Services.JsInterop.Models
|
|
@using AliasVault.Client.Main.Components.Items
|
|
@using AliasVault.Client.Main.Components.Forms
|
|
@using Microsoft.Extensions.Localization
|
|
@using AliasClientDb
|
|
@using AliasClientDb.Models
|
|
@implements IAsyncDisposable
|
|
|
|
<PageHeader
|
|
BreadcrumbItems="@BreadcrumbItems"
|
|
Title="@(EditMode ? Localizer["EditItemTitle"] : Localizer["AddItemTitle"])"
|
|
Description="@(EditMode ? Localizer["EditItemDescription"] : Localizer["AddItemDescription"])">
|
|
<CustomActions>
|
|
<ConfirmButton OnClick="TriggerFormSubmit">@Localizer["SaveItemButton"]</ConfirmButton>
|
|
<CancelButton OnClick="Cancel">@SharedLocalizer["Cancel"]</CancelButton>
|
|
</CustomActions>
|
|
</PageHeader>
|
|
|
|
@if (Loading)
|
|
{
|
|
<LoadingIndicator />
|
|
}
|
|
else
|
|
{
|
|
<EditForm @ref="EditFormRef" Model="Obj" OnValidSubmit="SaveAlias">
|
|
<DataAnnotationsValidator />
|
|
<div class="grid grid-cols-1 px-4 pt-6 md:grid-cols-2 lg:grid-cols-3 md:gap-4 dark:bg-gray-900">
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
@* Item Type Selector *@
|
|
<ItemTypeSelector
|
|
SelectedType="@Obj.ItemType"
|
|
SelectedTypeChanged="HandleItemTypeChange"
|
|
IsEditMode="@EditMode"
|
|
ShowDropdown="@ShowTypeDropdown"
|
|
ShowDropdownChanged="@((show) => ShowTypeDropdown = show)"
|
|
OnRegenerateAlias="@GenerateRandomAlias" />
|
|
|
|
@* Service/Name Section - Always shown *@
|
|
<div class="p-4 mb-4 bg-white border-2 border-primary-600 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
|
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["ServiceSectionHeader"]</h3>
|
|
<div class="grid gap-6">
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="service-name" Label="@Localizer["ServiceNameLabel"]" Placeholder="@Localizer["ServiceNamePlaceholder"]" @bind-Value="Obj.ServiceName"></EditFormRow>
|
|
<ValidationMessage For="() => Obj.ServiceName"/>
|
|
@* Folder Selector - below name field *@
|
|
<FolderSelector @bind-SelectedFolderId="Obj.FolderId" />
|
|
</div>
|
|
@* URL field - only for Login and Alias types (multi-value) *@
|
|
@if (ShouldShowField(FieldKey.LoginUrl))
|
|
{
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<MultiValueFormRow
|
|
Id="service-url"
|
|
Label="@Localizer["ServiceUrlLabel"]"
|
|
Values="@GetUrlValues()"
|
|
ValuesChanged="OnUrlValuesChanged"
|
|
OnFocus="OnFocusUrlInput" />
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@* TOTP Codes Section - Only for Login/Alias types when visible *@
|
|
@if (Show2FA && HasLoginFields())
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
<TotpCodes TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" CanRemove="@CanRemove2FASection()" OnRemove="Remove2FASection" />
|
|
</div>
|
|
}
|
|
|
|
@* Notes Section - When visible (left side for non-Note types) *@
|
|
@if (ShouldShowField(FieldKey.NotesContent) && Obj.ItemType != ItemType.Note)
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
<RemovableSection CanRemove="@CanRemoveField(FieldKey.NotesContent)" OnRemove="() => RemoveOptionalField(FieldKey.NotesContent)">
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Type="textarea" Id="notes" Label="@Localizer["NotesLabel"]" LabelStyle="EditFormRow.FormLabelStyle.Header" @bind-Value="Obj.GetField(FieldKey.NotesContent)!.Value"></EditFormRow>
|
|
</div>
|
|
</RemovableSection>
|
|
</div>
|
|
}
|
|
|
|
@* Attachments Section - When visible *@
|
|
@if (ShowAttachments)
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
<RemovableSection Title="@Localizer["AttachmentsSectionHeader"]" CanRemove="@CanRemoveAttachmentsSection()" OnRemove="RemoveAttachmentsSection">
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<AttachmentUploader
|
|
Attachments="@Obj.Attachments"
|
|
AttachmentsChanged="@HandleAttachmentsChanged" />
|
|
</div>
|
|
</RemovableSection>
|
|
</div>
|
|
}
|
|
|
|
@* Add Field Menu - Left column (visible only on lg screens when right column is also shown) *@
|
|
<div class="hidden lg:block mb-4">
|
|
<AddFieldMenu
|
|
OptionalSystemFields="@GetOptionalSystemFields()"
|
|
VisibleFieldKeys="@GetVisibleFieldKeys()"
|
|
Show2FA="@Show2FA"
|
|
ShowAttachments="@ShowAttachments"
|
|
HasLoginFields="@HasLoginFields()"
|
|
CustomFieldCount="@Obj.GetCustomFields().Count"
|
|
OnAddSystemField="AddOptionalField"
|
|
OnAddCustomField="AddCustomField"
|
|
OnAdd2FA="Add2FASection"
|
|
OnAddAttachments="AddAttachmentsSection" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-2">
|
|
@* Login Details Section - For Login and Alias types *@
|
|
@if (HasLoginFields())
|
|
{
|
|
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800 overflow-visible">
|
|
<h3 class="mb-4 text-xl font-semibold dark:text-white flex items-center gap-2">
|
|
<span>@Localizer["LoginDetailsSectionHeader"]</span>
|
|
@if (Obj.ItemType == ItemType.Login && !ShouldShowField(FieldKey.LoginEmail))
|
|
{
|
|
<button type="button" @onclick="() => AddOptionalField(FieldKey.LoginEmail)" @onclick:preventDefault="true"
|
|
class="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-full transition-colors focus:outline-none text-gray-500 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 border border-dashed border-gray-300 dark:border-gray-600 hover:border-primary-400 dark:hover:border-primary-500">
|
|
<svg class="w-2.5 h-2.5 -ml-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
</svg>
|
|
<span>@SharedLocalizer["Email"]</span>
|
|
</button>
|
|
}
|
|
</h3>
|
|
<div class="grid gap-6">
|
|
@if (EditMode && Obj.Passkeys != null && Obj.Passkeys.Any())
|
|
{
|
|
var passkey = Obj.Passkeys.First();
|
|
@* With passkey: Username, Passkey, Email, Password *@
|
|
<div class="col-span-6">
|
|
<EditUsernameFormRow Id="username" Label="@Localizer["UsernameLabel"]" @bind-Value="Obj.GetField(FieldKey.LoginUsername)!.Value" OnGenerateNewUsername="GenerateRandomUsername"></EditUsernameFormRow>
|
|
</div>
|
|
@if (!PasskeyMarkedForDeletion)
|
|
{
|
|
<div class="col-span-6">
|
|
<div class="p-3 rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-start gap-3">
|
|
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
|
</svg>
|
|
<div class="flex-1">
|
|
<div class="mb-1 flex items-center justify-between">
|
|
<span class="text-sm font-semibold text-gray-900 dark:text-white">@Localizer["PasskeyLabel"]</span>
|
|
<button type="button" @onclick="MarkPasskeyForDeletion" class="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300" title="@Localizer["DeletePasskeyButton"]">
|
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="3 6 5 6 21 6" />
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
<line x1="10" y1="11" x2="10" y2="17" />
|
|
<line x1="14" y1="11" x2="14" y2="17" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="space-y-1 mb-2">
|
|
@if (!string.IsNullOrWhiteSpace(passkey.RpId))
|
|
{
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">@Localizer["PasskeySiteLabel"]: </span>
|
|
<span class="text-sm text-gray-900 dark:text-white">@passkey.RpId</span>
|
|
</div>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(passkey.DisplayName))
|
|
{
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">@Localizer["PasskeyDisplayNameLabel"]: </span>
|
|
<span class="text-sm text-gray-900 dark:text-white">@passkey.DisplayName</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">
|
|
@Localizer["PasskeyHelpText"]
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="col-span-6">
|
|
<div class="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
|
|
<div class="flex items-start gap-3">
|
|
<svg class="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
|
</svg>
|
|
<div class="flex-1">
|
|
<div class="mb-1 flex items-center justify-between">
|
|
<span class="text-sm font-semibold text-red-900 dark:text-red-100">@Localizer["PasskeyMarkedForDeletion"]</span>
|
|
<button type="button" @onclick="UndoPasskeyDeletion" class="text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300" title="@Localizer["UndoButton"]">
|
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 7v6h6" />
|
|
<path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-red-800 dark:text-red-200">
|
|
@Localizer["PasskeyWillBeDeleted"]
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
<div class="col-span-6">
|
|
<EmailDomainField Id="email"
|
|
@bind-Value="Obj.GetField(FieldKey.LoginEmail)!.Value"
|
|
DefaultToEmailMode="@(Obj.ItemType == ItemType.Login)"
|
|
OnGenerateAlias="@(Obj.ItemType == ItemType.Alias ? HandleGenerateAliasEmail : HandleGenerateRandomEmail)"></EmailDomainField>
|
|
</div>
|
|
<div class="col-span-6">
|
|
<EditPasswordFormRow Id="password" Label="@Localizer["PasswordLabel"]" @bind-Value="Obj.GetField(FieldKey.LoginPassword)!.Value" ShowPassword="IsPasswordVisible"></EditPasswordFormRow>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
@* Without passkey: Email (optional add for Login), Username, Password *@
|
|
@if (ShouldShowField(FieldKey.LoginEmail))
|
|
{
|
|
<div class="col-span-6">
|
|
@if (CanRemoveField(FieldKey.LoginEmail))
|
|
{
|
|
<EmailDomainField Id="email"
|
|
@bind-Value="Obj.GetField(FieldKey.LoginEmail)!.Value"
|
|
DefaultToEmailMode="@(Obj.ItemType == ItemType.Login)"
|
|
OnRemove="@(() => RemoveOptionalField(FieldKey.LoginEmail))"
|
|
OnGenerateAlias="@(Obj.ItemType == ItemType.Alias ? HandleGenerateAliasEmail : HandleGenerateRandomEmail)"></EmailDomainField>
|
|
}
|
|
else
|
|
{
|
|
<EmailDomainField Id="email"
|
|
@bind-Value="Obj.GetField(FieldKey.LoginEmail)!.Value"
|
|
DefaultToEmailMode="@(Obj.ItemType == ItemType.Login)"
|
|
OnGenerateAlias="@(Obj.ItemType == ItemType.Alias ? HandleGenerateAliasEmail : HandleGenerateRandomEmail)"></EmailDomainField>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<div class="col-span-6">
|
|
<EditUsernameFormRow Id="username" Label="@Localizer["UsernameLabel"]" @bind-Value="Obj.GetField(FieldKey.LoginUsername)!.Value" OnGenerateNewUsername="GenerateRandomUsername"></EditUsernameFormRow>
|
|
</div>
|
|
<div class="col-span-6">
|
|
<EditPasswordFormRow Id="password" Label="@Localizer["PasswordLabel"]" @bind-Value="Obj.GetField(FieldKey.LoginPassword)!.Value" ShowPassword="IsPasswordVisible"></EditPasswordFormRow>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@* Alias Identity Section - For Alias type *@
|
|
@if (Obj.ItemType == ItemType.Alias)
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-2">
|
|
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
|
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["AliasSectionHeader"]</h3>
|
|
<div class="mb-4">
|
|
<Button OnClick="HandleGenerateOrClearAlias" Color="@(HasAliasValues() ? "secondary" : "primary")" AdditionalClasses="@GetToggleButtonClasses()">
|
|
<svg class='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
@if (HasAliasValues())
|
|
{
|
|
<path d="M18 6L6 18M6 6l12 12"/>
|
|
}
|
|
else
|
|
{
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
<circle cx="8" cy="8" r="1"/>
|
|
<circle cx="16" cy="8" r="1"/>
|
|
<circle cx="12" cy="12" r="1"/>
|
|
<circle cx="8" cy="16" r="1"/>
|
|
<circle cx="16" cy="16" r="1"/>
|
|
}
|
|
</svg>
|
|
@(HasAliasValues() ? Localizer["ClearAliasFieldsButton"] : Localizer["GenerateRandomAliasButton"])
|
|
</Button>
|
|
</div>
|
|
<div class="grid gap-6">
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="first-name" Label="@Localizer["FirstNameLabel"]" @bind-Value="Obj.GetField(FieldKey.AliasFirstName)!.Value"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="last-name" Label="@Localizer["LastNameLabel"]" @bind-Value="Obj.GetField(FieldKey.AliasLastName)!.Value"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="gender" Label="@Localizer["GenderLabel"]" @bind-Value="Obj.GetField(FieldKey.AliasGender)!.Value"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="birthdate" Label="@Localizer["BirthDateLabel"]" @bind-Value="Obj.GetField(FieldKey.AliasBirthdate)!.Value"></EditFormRow>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@* Credit Card Section - For CreditCard type *@
|
|
@if (Obj.ItemType == ItemType.CreditCard)
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-2">
|
|
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
|
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["CardDetailsSectionHeader"]</h3>
|
|
<div class="grid gap-6">
|
|
<div class="col-span-6">
|
|
<EditFormRow Id="cardholder-name" Label="@Localizer["CardholderNameLabel"]" @bind-Value="Obj.GetField(FieldKey.CardCardholderName)!.Value"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6">
|
|
<EditPasswordFormRow Id="card-number" Label="@Localizer["CardNumberLabel"]" @bind-Value="Obj.GetField(FieldKey.CardNumber)!.Value" ShowPassword="false" ShowGenerateButtons="false"></EditPasswordFormRow>
|
|
</div>
|
|
<div class="col-span-3">
|
|
<EditFormRow Id="expiry-month" Label="@Localizer["ExpiryMonthLabel"]" Placeholder="MM" @bind-Value="Obj.GetField(FieldKey.CardExpiryMonth)!.Value"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-3">
|
|
<EditFormRow Id="expiry-year" Label="@Localizer["ExpiryYearLabel"]" Placeholder="YYYY" @bind-Value="Obj.GetField(FieldKey.CardExpiryYear)!.Value"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditPasswordFormRow Id="card-cvv" Label="@Localizer["CardCvvLabel"]" @bind-Value="Obj.GetField(FieldKey.CardCvv)!.Value" ShowPassword="false" ShowGenerateButtons="false"></EditPasswordFormRow>
|
|
</div>
|
|
@if (ShouldShowField(FieldKey.CardPin))
|
|
{
|
|
<div class="col-span-6 sm:col-span-3 relative">
|
|
<EditPasswordFormRow Id="card-pin" Label="@Localizer["CardPinLabel"]" @bind-Value="Obj.GetField(FieldKey.CardPin)!.Value" ShowPassword="false"></EditPasswordFormRow>
|
|
@if (CanRemoveField(FieldKey.CardPin))
|
|
{
|
|
<button type="button" @onclick="() => RemoveOptionalField(FieldKey.CardPin)" @onclick:preventDefault="true"
|
|
class="absolute top-0 right-0 text-gray-400 hover:text-red-500 transition-colors" title="@Localizer["RemoveField"]">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@* Notes Section - For Note type (main content area) *@
|
|
@if (Obj.ItemType == ItemType.Note)
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-2">
|
|
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
|
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["NotesLabel"]</h3>
|
|
<div class="grid gap-6">
|
|
<div class="col-span-6">
|
|
<EditFormRow Type="textarea" Id="notes" Label="" @bind-Value="Obj.GetField(FieldKey.NotesContent)!.Value"></EditFormRow>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@* Custom Fields Section *@
|
|
@{
|
|
var customFields = Obj.GetCustomFields();
|
|
}
|
|
@if (customFields.Any())
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-2">
|
|
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
|
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["CustomFieldsSectionHeader"]</h3>
|
|
<div class="grid gap-6">
|
|
@foreach (var customField in customFields)
|
|
{
|
|
<div class="col-span-6">
|
|
<EditableFieldLabel
|
|
HtmlFor="@($"custom-{customField.TempId ?? customField.FieldKey}")"
|
|
Label="@customField.Label"
|
|
LabelChanged="@((newLabel) => Obj.UpdateCustomFieldLabel(customField.FieldKey, newLabel))"
|
|
OnDelete="@(() => Obj.RemoveCustomField(customField.FieldKey))" />
|
|
@if (customField.FieldType == FieldType.TextArea)
|
|
{
|
|
<div class="relative">
|
|
<textarea id="@($"custom-{customField.TempId ?? customField.FieldKey}")" style="height: 200px;" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" @bind="customField.Value"></textarea>
|
|
</div>
|
|
}
|
|
else if (customField.IsHidden || customField.FieldType == FieldType.Hidden || customField.FieldType == FieldType.Password)
|
|
{
|
|
<EditPasswordFormRow Id="@($"custom-{customField.TempId ?? customField.FieldKey}")" Label="" @bind-Value="customField.Value" ShowPassword="false"></EditPasswordFormRow>
|
|
}
|
|
else
|
|
{
|
|
<div class="relative">
|
|
<input type="text" id="@($"custom-{customField.TempId ?? customField.FieldKey}")" autocomplete="off" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" @bind="customField.Value" autocapitalize="off" autocorrect="off">
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@* Add Field Menu *@
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-2 mb-4">
|
|
<AddFieldMenu
|
|
OptionalSystemFields="@GetOptionalSystemFields()"
|
|
VisibleFieldKeys="@GetVisibleFieldKeys()"
|
|
Show2FA="@Show2FA"
|
|
ShowAttachments="@ShowAttachments"
|
|
HasLoginFields="@HasLoginFields()"
|
|
CustomFieldCount="@Obj.GetCustomFields().Count"
|
|
OnAddSystemField="AddOptionalField"
|
|
OnAddCustomField="AddCustomField"
|
|
OnAdd2FA="Add2FASection"
|
|
OnAddAttachments="AddAttachmentsSection" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="hidden">@Localizer["SaveItemButton"]</button>
|
|
</EditForm>
|
|
}
|
|
|
|
@code {
|
|
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Items.AddEdit", "AliasVault.Client");
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Credentials ID.
|
|
/// </summary>
|
|
[Parameter]
|
|
public Guid? Id { get; set; }
|
|
|
|
private bool EditMode { get; set; }
|
|
private EditForm EditFormRef { get; set; } = null!;
|
|
private bool Loading { get; set; } = true;
|
|
private bool IsPasswordVisible { get; set; } = false;
|
|
private bool PasskeyMarkedForDeletion { get; set; } = false;
|
|
private bool ShowTypeDropdown { get; set; } = false;
|
|
private bool Show2FA { get; set; } = false;
|
|
private bool ShowAttachments { get; set; } = false;
|
|
private bool Show2FAAddedManually { get; set; } = false;
|
|
private bool ShowAttachmentsAddedManually { get; set; } = false;
|
|
private ItemEdit Obj { get; set; } = new();
|
|
private IJSObjectReference? Module;
|
|
|
|
/// <summary>
|
|
/// Fields that were manually added via the + menu.
|
|
/// </summary>
|
|
private HashSet<string> ManuallyAddedFields { get; set; } = [];
|
|
|
|
/// <summary>
|
|
/// Fields that initially had values when loaded (for edit mode).
|
|
/// </summary>
|
|
private HashSet<string> InitiallyVisibleFields { get; set; } = [];
|
|
|
|
// Track last generated values to protect manual entries
|
|
private string? LastGeneratedUsername { get; set; }
|
|
private string? LastGeneratedPassword { get; set; }
|
|
private string? LastGeneratedEmail { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
async ValueTask IAsyncDisposable.DisposeAsync()
|
|
{
|
|
QuickCreateStateService.OnChange -= OnQuickCreateStateChanged;
|
|
await KeyboardShortcutService.UnregisterShortcutAsync("gc");
|
|
if (Module is not null)
|
|
{
|
|
await Module.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnInitialized()
|
|
{
|
|
if (Id.HasValue)
|
|
{
|
|
EditMode = true;
|
|
}
|
|
else
|
|
{
|
|
EditMode = false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
|
|
if (EditMode)
|
|
{
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["ViewItemBreadcrumb"], Url = $"/items/{Id}" });
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["EditItemBreadcrumb"] });
|
|
}
|
|
else
|
|
{
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["AddNewItemBreadcrumb"] });
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
|
|
if (firstRender)
|
|
{
|
|
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/modules/newIdentityWidget.js");
|
|
|
|
// Subscribe to quick create state changes so we can reinitialize
|
|
// when the user triggers the widget while already on this page.
|
|
QuickCreateStateService.OnChange += OnQuickCreateStateChanged;
|
|
|
|
if (EditMode)
|
|
{
|
|
await LoadExistingCredential();
|
|
}
|
|
else
|
|
{
|
|
await ApplyQuickCreateState();
|
|
}
|
|
|
|
Loading = false;
|
|
StateHasChanged();
|
|
|
|
if (!EditMode)
|
|
{
|
|
await JsInteropService.FocusElementById("service-name");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the quick create state change event when the user triggers the
|
|
/// CreateNewIdentityWidget while already on the AddEdit page.
|
|
/// </summary>
|
|
private async void OnQuickCreateStateChanged()
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
// Ensure we are in create mode since quick create always creates new items.
|
|
EditMode = false;
|
|
|
|
await ApplyQuickCreateState();
|
|
Loading = false;
|
|
StateHasChanged();
|
|
await JsInteropService.FocusElementById("service-name");
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new credential and applies any prefilled data from the QuickCreateStateService.
|
|
/// </summary>
|
|
private async Task ApplyQuickCreateState()
|
|
{
|
|
CreateNewCredential();
|
|
|
|
// Reset manually added fields tracking for the fresh form.
|
|
ManuallyAddedFields = [];
|
|
InitiallyVisibleFields = [];
|
|
|
|
// Use the state service to pre-fill form data
|
|
if (!string.IsNullOrEmpty(QuickCreateStateService.ServiceName))
|
|
{
|
|
Obj.ServiceName = QuickCreateStateService.ServiceName;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(QuickCreateStateService.ServiceUrl))
|
|
{
|
|
Obj.SetFieldValue(FieldKey.LoginUrl, QuickCreateStateService.ServiceUrl);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(QuickCreateStateService.ItemType))
|
|
{
|
|
await HandleItemTypeChange(QuickCreateStateService.ItemType);
|
|
}
|
|
|
|
if (QuickCreateStateService.FolderId.HasValue)
|
|
{
|
|
Obj.FolderId = QuickCreateStateService.FolderId;
|
|
}
|
|
|
|
// Clear the state after using it
|
|
QuickCreateStateService.ClearState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the current item type has login-related fields.
|
|
/// </summary>
|
|
private bool HasLoginFields()
|
|
{
|
|
return Obj.ItemType == ItemType.Login || Obj.ItemType == ItemType.Alias;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a field should be shown based on item type and visibility rules.
|
|
/// </summary>
|
|
private bool ShouldShowField(string fieldKey)
|
|
{
|
|
// Check if field applies to current item type
|
|
var fieldDef = SystemFieldRegistry.GetSystemField(fieldKey);
|
|
if (fieldDef == null || !SystemFieldRegistry.FieldAppliesToType(fieldDef, Obj.ItemType))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if manually added
|
|
if (ManuallyAddedFields.Contains(fieldKey))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check if initially visible (edit mode)
|
|
if (InitiallyVisibleFields.Contains(fieldKey))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check if it has a value
|
|
if (Obj.HasFieldValue(fieldKey))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Check if shown by default for this item type
|
|
var config = fieldDef.ApplicableToTypes.GetValueOrDefault(Obj.ItemType);
|
|
return config?.ShowByDefault ?? false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a field can be removed (is an optional field).
|
|
/// </summary>
|
|
private bool CanRemoveField(string fieldKey)
|
|
{
|
|
// A field can be removed if it's an optional field for the current item type
|
|
var fieldDef = SystemFieldRegistry.GetSystemField(fieldKey);
|
|
if (fieldDef == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if it's an optional field (not shown by default)
|
|
var config = fieldDef.ApplicableToTypes.GetValueOrDefault(Obj.ItemType);
|
|
return config != null && !config.ShowByDefault;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the optional system fields for the current item type.
|
|
/// </summary>
|
|
private IEnumerable<SystemFieldDefinition> GetOptionalSystemFields()
|
|
{
|
|
return SystemFieldRegistry.GetOptionalFieldsForItemType(Obj.ItemType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the currently visible field keys.
|
|
/// </summary>
|
|
private HashSet<string> GetVisibleFieldKeys()
|
|
{
|
|
var keys = new HashSet<string>();
|
|
|
|
foreach (var field in SystemFieldRegistry.GetFieldsForItemType(Obj.ItemType))
|
|
{
|
|
if (ShouldShowField(field.FieldKey))
|
|
{
|
|
keys.Add(field.FieldKey);
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an optional field.
|
|
/// </summary>
|
|
private void AddOptionalField(string fieldKey)
|
|
{
|
|
ManuallyAddedFields.Add(fieldKey);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an optional field (hides it and clears the value).
|
|
/// </summary>
|
|
private void RemoveOptionalField(string fieldKey)
|
|
{
|
|
ManuallyAddedFields.Remove(fieldKey);
|
|
InitiallyVisibleFields.Remove(fieldKey);
|
|
Obj.SetFieldValue(fieldKey, string.Empty);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the 2FA section.
|
|
/// </summary>
|
|
private void Add2FASection()
|
|
{
|
|
Show2FA = true;
|
|
Show2FAAddedManually = true;
|
|
|
|
// Restore any soft-deleted TOTP codes (undo previous removal)
|
|
foreach (var totpCode in Obj.TotpCodes)
|
|
{
|
|
if (totpCode.IsDeleted)
|
|
{
|
|
totpCode.IsDeleted = false;
|
|
totpCode.UpdatedAt = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the 2FA section and marks existing TOTP codes for deletion.
|
|
/// </summary>
|
|
private void Remove2FASection()
|
|
{
|
|
Show2FA = false;
|
|
Show2FAAddedManually = false;
|
|
|
|
// Mark all TOTP codes for deletion (soft delete for existing, remove new ones)
|
|
foreach (var totpCode in Obj.TotpCodes.ToList())
|
|
{
|
|
if (totpCode.Id != Guid.Empty)
|
|
{
|
|
// Existing code - mark for soft delete
|
|
totpCode.IsDeleted = true;
|
|
totpCode.UpdatedAt = DateTime.UtcNow;
|
|
}
|
|
else
|
|
{
|
|
// New code - just remove from list
|
|
Obj.TotpCodes.Remove(totpCode);
|
|
}
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the 2FA section can be removed.
|
|
/// Only allow removal when there's at least one TOTP code registered.
|
|
/// </summary>
|
|
private bool CanRemove2FASection()
|
|
{
|
|
return Obj.TotpCodes.Any(t => !t.IsDeleted);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the attachments section.
|
|
/// </summary>
|
|
private void AddAttachmentsSection()
|
|
{
|
|
ShowAttachments = true;
|
|
ShowAttachmentsAddedManually = true;
|
|
|
|
// Restore any soft-deleted attachments (undo previous removal)
|
|
foreach (var attachment in Obj.Attachments)
|
|
{
|
|
if (attachment.IsDeleted)
|
|
{
|
|
attachment.IsDeleted = false;
|
|
attachment.UpdatedAt = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the attachments section and marks existing attachments for deletion.
|
|
/// </summary>
|
|
private void RemoveAttachmentsSection()
|
|
{
|
|
ShowAttachments = false;
|
|
ShowAttachmentsAddedManually = false;
|
|
|
|
// Mark all attachments for deletion (soft delete for existing, remove new ones)
|
|
foreach (var attachment in Obj.Attachments.ToList())
|
|
{
|
|
if (attachment.Id != Guid.Empty)
|
|
{
|
|
// Existing attachment - mark for soft delete
|
|
attachment.IsDeleted = true;
|
|
attachment.UpdatedAt = DateTime.UtcNow;
|
|
}
|
|
else
|
|
{
|
|
// New attachment - just remove from list
|
|
Obj.Attachments.Remove(attachment);
|
|
}
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the attachments section can be removed (always true when visible).
|
|
/// </summary>
|
|
private bool CanRemoveAttachmentsSection()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a custom field.
|
|
/// </summary>
|
|
private void AddCustomField((string Label, string FieldType) args)
|
|
{
|
|
Obj.AddCustomField(args.Label, args.FieldType);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles item type change.
|
|
/// Clears field values that don't apply to the new item type.
|
|
/// </summary>
|
|
private async Task HandleItemTypeChange(string newType)
|
|
{
|
|
if (Obj.ItemType == newType)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldType = Obj.ItemType;
|
|
Obj.ItemType = newType;
|
|
|
|
// Clear all system fields that don't apply to the new type
|
|
foreach (var fieldDef in SystemFieldRegistry.Fields.Values)
|
|
{
|
|
if (!SystemFieldRegistry.FieldAppliesToType(fieldDef, newType))
|
|
{
|
|
Obj.SetFieldValue(fieldDef.FieldKey, string.Empty);
|
|
}
|
|
}
|
|
|
|
// Clear manually added fields that don't apply to new type
|
|
var fieldsToRemove = ManuallyAddedFields
|
|
.Where(fk => {
|
|
var def = SystemFieldRegistry.GetSystemField(fk);
|
|
return def != null && !SystemFieldRegistry.FieldAppliesToType(def, newType);
|
|
})
|
|
.ToList();
|
|
foreach (var fk in fieldsToRemove)
|
|
{
|
|
ManuallyAddedFields.Remove(fk);
|
|
}
|
|
|
|
// Clear initially visible fields that don't apply to new type
|
|
var initialFieldsToRemove = InitiallyVisibleFields
|
|
.Where(fk => {
|
|
var def = SystemFieldRegistry.GetSystemField(fk);
|
|
return def != null && !SystemFieldRegistry.FieldAppliesToType(def, newType);
|
|
})
|
|
.ToList();
|
|
foreach (var fk in initialFieldsToRemove)
|
|
{
|
|
InitiallyVisibleFields.Remove(fk);
|
|
}
|
|
|
|
// Generate alias for Alias type in create mode
|
|
if (newType == ItemType.Alias && !EditMode && !HasAliasValues())
|
|
{
|
|
await GenerateRandomAlias();
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads an existing credential for editing.
|
|
/// </summary>
|
|
private async Task LoadExistingCredential()
|
|
{
|
|
if (Id is null)
|
|
{
|
|
NavigateAwayWithError(Localizer["ItemNotExistError"]);
|
|
return;
|
|
}
|
|
|
|
var item = await ItemService.LoadEntryAsync(Id.Value);
|
|
if (item is null)
|
|
{
|
|
NavigateAwayWithError(Localizer["ItemNotExistError"]);
|
|
return;
|
|
}
|
|
|
|
Obj = ItemEdit.FromEntity(item);
|
|
|
|
// Track initially visible fields
|
|
foreach (var fieldDef in SystemFieldRegistry.GetFieldsForItemType(Obj.ItemType))
|
|
{
|
|
if (Obj.HasFieldValue(fieldDef.FieldKey))
|
|
{
|
|
InitiallyVisibleFields.Add(fieldDef.FieldKey);
|
|
}
|
|
}
|
|
|
|
// Show sections that have content
|
|
Show2FA = Obj.TotpCodes.Any();
|
|
ShowAttachments = Obj.Attachments.Any();
|
|
|
|
// Set default URL if not set
|
|
if (!Obj.HasFieldValue(FieldKey.LoginUrl))
|
|
{
|
|
Obj.SetFieldValue(FieldKey.LoginUrl, ItemService.DefaultServiceUrl);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new item object.
|
|
/// </summary>
|
|
private Item CreateNewItemObject()
|
|
{
|
|
var item = new Item
|
|
{
|
|
ItemType = AliasClientDb.Models.ItemType.Login,
|
|
FieldValues = new List<FieldValue>(),
|
|
Attachments = new List<Attachment>(),
|
|
TotpCodes = new List<TotpCode>(),
|
|
Passkeys = new List<Passkey>()
|
|
};
|
|
|
|
// Don't pre-populate email for Login type - it's an optional field
|
|
// Email will be populated when user switches to Alias type or manually adds the field
|
|
|
|
return item;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new item object for editing.
|
|
/// </summary>
|
|
private void CreateNewCredential()
|
|
{
|
|
Obj = ItemEdit.FromEntity(CreateNewItemObject());
|
|
|
|
Obj.SetFieldValue(FieldKey.AliasBirthdate, string.Empty);
|
|
Obj.SetFieldValue(FieldKey.LoginUrl, ItemService.DefaultServiceUrl);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an error message and navigates to the home page.
|
|
/// </summary>
|
|
private void NavigateAwayWithError(string errorMessage)
|
|
{
|
|
GlobalNotificationService.AddErrorMessage(errorMessage);
|
|
NavigationManager.NavigateTo("/items", false, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the URL values for the multi-value field.
|
|
/// Ensures at least one entry exists.
|
|
/// </summary>
|
|
private List<string> GetUrlValues()
|
|
{
|
|
var field = Obj.GetField(FieldKey.LoginUrl);
|
|
if (field == null)
|
|
{
|
|
return new List<string> { string.Empty };
|
|
}
|
|
|
|
// Return Values if populated, otherwise convert single Value
|
|
if (field.Values != null && field.Values.Count > 0)
|
|
{
|
|
return field.Values;
|
|
}
|
|
|
|
return string.IsNullOrEmpty(field.Value)
|
|
? new List<string> { string.Empty }
|
|
: new List<string> { field.Value };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles URL values change from the multi-value component.
|
|
/// </summary>
|
|
private void OnUrlValuesChanged(List<string> values)
|
|
{
|
|
Obj.SetFieldValues(FieldKey.LoginUrl, values);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// When the URL input is focused, place cursor at the end of the default URL.
|
|
/// </summary>
|
|
private void OnFocusUrlInput((int Index, FocusEventArgs Args) e)
|
|
{
|
|
var urlValues = GetUrlValues();
|
|
if (e.Index >= urlValues.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var currentUrl = urlValues[e.Index];
|
|
if (currentUrl != ItemService.DefaultServiceUrl)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Task.Delay(1).ContinueWith(_ =>
|
|
{
|
|
JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('service-url-{e.Index}').setSelectionRange({ItemService.DefaultServiceUrl.Length}, {ItemService.DefaultServiceUrl.Length})");
|
|
});
|
|
}
|
|
|
|
private void HandleAttachmentsChanged(List<Attachment> updatedAttachments)
|
|
{
|
|
Obj.Attachments = updatedAttachments;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void HandleTotpCodesChanged(List<TotpCode> updatedTotpCodes)
|
|
{
|
|
Obj.TotpCodes = updatedTotpCodes;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task HandleGenerateOrClearAlias()
|
|
{
|
|
if (HasAliasValues())
|
|
{
|
|
ClearAliasFields();
|
|
}
|
|
else
|
|
{
|
|
await GenerateRandomAlias();
|
|
}
|
|
}
|
|
|
|
private void ClearAliasFields()
|
|
{
|
|
Obj.SetFieldValue(FieldKey.AliasFirstName, string.Empty);
|
|
Obj.SetFieldValue(FieldKey.AliasLastName, string.Empty);
|
|
Obj.SetFieldValue(FieldKey.AliasGender, string.Empty);
|
|
Obj.SetFieldValue(FieldKey.AliasBirthdate, string.Empty);
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private bool HasAliasValues()
|
|
{
|
|
return !string.IsNullOrWhiteSpace(Obj.GetFieldValue(FieldKey.AliasFirstName)) ||
|
|
!string.IsNullOrWhiteSpace(Obj.GetFieldValue(FieldKey.AliasLastName)) ||
|
|
!string.IsNullOrWhiteSpace(Obj.GetFieldValue(FieldKey.AliasGender)) ||
|
|
!string.IsNullOrWhiteSpace(Obj.GetFieldValue(FieldKey.AliasBirthdate));
|
|
}
|
|
|
|
private string GetToggleButtonClasses()
|
|
{
|
|
var baseClasses = "flex items-center justify-center gap-1";
|
|
if (HasAliasValues())
|
|
{
|
|
return $"{baseClasses} bg-gray-500 hover:bg-gray-600 text-white";
|
|
}
|
|
return baseClasses;
|
|
}
|
|
|
|
private async Task GenerateRandomAlias()
|
|
{
|
|
string currentUsername = Obj.GetFieldValue(FieldKey.LoginUsername);
|
|
string currentPassword = Obj.GetFieldValue(FieldKey.LoginPassword);
|
|
string currentEmail = Obj.GetFieldValue(FieldKey.LoginEmail);
|
|
|
|
var generatedItem = await ItemService.GenerateRandomIdentityAsync(Obj.ToEntity());
|
|
var generatedObj = ItemEdit.FromEntity(generatedItem);
|
|
|
|
// Keep the current values if user has modified them
|
|
Obj.SetFieldValue(FieldKey.LoginUsername, currentUsername);
|
|
Obj.SetFieldValue(FieldKey.LoginPassword, currentPassword);
|
|
Obj.SetFieldValue(FieldKey.LoginEmail, currentEmail);
|
|
|
|
// Always update alias fields
|
|
Obj.SetFieldValue(FieldKey.AliasFirstName, generatedObj.GetFieldValue(FieldKey.AliasFirstName));
|
|
Obj.SetFieldValue(FieldKey.AliasLastName, generatedObj.GetFieldValue(FieldKey.AliasLastName));
|
|
Obj.SetFieldValue(FieldKey.AliasGender, generatedObj.GetFieldValue(FieldKey.AliasGender));
|
|
Obj.SetFieldValue(FieldKey.AliasBirthdate, generatedObj.GetFieldValue(FieldKey.AliasBirthdate));
|
|
|
|
var generatedUsername = generatedObj.GetFieldValue(FieldKey.LoginUsername);
|
|
var generatedPassword = generatedObj.GetFieldValue(FieldKey.LoginPassword);
|
|
var generatedEmail = generatedObj.GetFieldValue(FieldKey.LoginEmail);
|
|
|
|
if (string.IsNullOrWhiteSpace(currentUsername) || currentUsername == LastGeneratedUsername)
|
|
{
|
|
Obj.SetFieldValue(FieldKey.LoginUsername, generatedUsername);
|
|
LastGeneratedUsername = generatedUsername;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(currentPassword) || currentPassword == LastGeneratedPassword)
|
|
{
|
|
Obj.SetFieldValue(FieldKey.LoginPassword, generatedPassword);
|
|
LastGeneratedPassword = generatedPassword;
|
|
IsPasswordVisible = true;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(currentEmail) || currentEmail == LastGeneratedEmail || currentEmail.StartsWith("@"))
|
|
{
|
|
Obj.SetFieldValue(FieldKey.LoginEmail, generatedEmail);
|
|
LastGeneratedEmail = generatedEmail;
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate a new random username.
|
|
/// </summary>
|
|
private async Task GenerateRandomUsername()
|
|
{
|
|
var aliasFirstName = Obj.GetFieldValue(FieldKey.AliasFirstName);
|
|
var aliasLastName = Obj.GetFieldValue(FieldKey.AliasLastName);
|
|
var aliasBirthDate = Obj.GetFieldValue(FieldKey.AliasBirthdate);
|
|
|
|
AliasVaultIdentity identity;
|
|
if (string.IsNullOrWhiteSpace(aliasFirstName) && string.IsNullOrWhiteSpace(aliasLastName) && string.IsNullOrWhiteSpace(aliasBirthDate))
|
|
{
|
|
var randomIdentity = await ItemService.GenerateRandomIdentityAsync(CreateNewItemObject());
|
|
var randomIdentityEdit = ItemEdit.FromEntity(randomIdentity);
|
|
|
|
identity = new AliasVaultIdentity
|
|
{
|
|
FirstName = randomIdentityEdit.GetFieldValue(FieldKey.AliasFirstName),
|
|
LastName = randomIdentityEdit.GetFieldValue(FieldKey.AliasLastName),
|
|
BirthDate = randomIdentityEdit.GetFieldValue(FieldKey.AliasBirthdate),
|
|
Gender = randomIdentityEdit.GetFieldValue(FieldKey.AliasGender),
|
|
NickName = randomIdentityEdit.GetFieldValue(FieldKey.LoginUsername),
|
|
};
|
|
}
|
|
else
|
|
{
|
|
identity = new AliasVaultIdentity
|
|
{
|
|
FirstName = aliasFirstName,
|
|
LastName = aliasLastName,
|
|
BirthDate = aliasBirthDate,
|
|
Gender = Obj.GetFieldValue(FieldKey.AliasGender),
|
|
NickName = Obj.GetFieldValue(FieldKey.LoginUsername),
|
|
};
|
|
}
|
|
|
|
Obj.SetFieldValue(FieldKey.LoginUsername, await JsInteropService.GenerateRandomUsernameAsync(identity));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate an identity-based email alias (for Alias type email field).
|
|
/// Uses the current alias field values (first name, last name, birthdate) to derive the email prefix,
|
|
/// so the email stays consistent with the filled-in persona fields.
|
|
/// </summary>
|
|
private async Task HandleGenerateAliasEmail()
|
|
{
|
|
var firstName = Obj.GetFieldValue(FieldKey.AliasFirstName);
|
|
var lastName = Obj.GetFieldValue(FieldKey.AliasLastName);
|
|
|
|
string prefix;
|
|
if (string.IsNullOrWhiteSpace(firstName) && string.IsNullOrWhiteSpace(lastName))
|
|
{
|
|
// No alias identity fields filled in, fall back to random prefix.
|
|
prefix = await JsInteropService.GenerateRandomStringEmailPrefixAsync();
|
|
}
|
|
else
|
|
{
|
|
var identity = new AliasVaultIdentity
|
|
{
|
|
FirstName = firstName,
|
|
LastName = lastName,
|
|
BirthDate = Obj.GetFieldValue(FieldKey.AliasBirthdate),
|
|
Gender = Obj.GetFieldValue(FieldKey.AliasGender),
|
|
NickName = Obj.GetFieldValue(FieldKey.LoginUsername),
|
|
};
|
|
|
|
prefix = await JsInteropService.GenerateRandomEmailPrefixAsync(identity);
|
|
}
|
|
|
|
var defaultEmailDomain = DbService.Settings.DefaultEmailDomain;
|
|
var email = !string.IsNullOrEmpty(defaultEmailDomain) ? $"{prefix}@{defaultEmailDomain}" : prefix;
|
|
|
|
Obj.SetFieldValue(FieldKey.LoginEmail, email);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate a random-string email alias (for Login type email field).
|
|
/// Uses random characters instead of identity-based prefixes since Login type
|
|
/// has no persona fields to base the email on.
|
|
/// </summary>
|
|
private async Task HandleGenerateRandomEmail()
|
|
{
|
|
var prefix = await JsInteropService.GenerateRandomStringEmailPrefixAsync();
|
|
var defaultEmailDomain = DbService.Settings.DefaultEmailDomain;
|
|
var email = !string.IsNullOrEmpty(defaultEmailDomain) ? $"{prefix}@{defaultEmailDomain}" : prefix;
|
|
|
|
Obj.SetFieldValue(FieldKey.LoginEmail, email);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancel the edit operation.
|
|
/// </summary>
|
|
private void Cancel()
|
|
{
|
|
NavigationManager.NavigateTo("/items/" + Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trigger the form submit.
|
|
/// </summary>
|
|
private async Task TriggerFormSubmit()
|
|
{
|
|
if (EditFormRef.EditContext?.Validate() == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await SaveAlias();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marks the passkey for deletion.
|
|
/// </summary>
|
|
private void MarkPasskeyForDeletion()
|
|
{
|
|
PasskeyMarkedForDeletion = true;
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Undoes the passkey deletion mark.
|
|
/// </summary>
|
|
private void UndoPasskeyDeletion()
|
|
{
|
|
PasskeyMarkedForDeletion = false;
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save the item to the database.
|
|
/// </summary>
|
|
private async Task SaveAlias()
|
|
{
|
|
GlobalLoadingSpinner.Show(Localizer["SavingVaultMessage"]);
|
|
StateHasChanged();
|
|
|
|
// Delete passkeys if marked for deletion
|
|
if (PasskeyMarkedForDeletion && Obj.Passkeys != null && Obj.Passkeys.Any())
|
|
{
|
|
var context = await DbService.GetDbContextAsync();
|
|
foreach (var passkey in Obj.Passkeys)
|
|
{
|
|
await ItemService.DeletePasskeyAsync(passkey.Id);
|
|
}
|
|
Obj.Passkeys.Clear();
|
|
}
|
|
|
|
if (EditMode)
|
|
{
|
|
if (Id is not null)
|
|
{
|
|
Id = await ItemService.UpdateEntryAsync(Obj.ToEntity());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Id = await ItemService.InsertEntryAsync(Obj.ToEntity());
|
|
}
|
|
|
|
GlobalLoadingSpinner.Hide();
|
|
StateHasChanged();
|
|
|
|
if (Id is null || Id == Guid.Empty)
|
|
{
|
|
GlobalNotificationService.AddErrorMessage(Localizer["ErrorSavingItem"], true);
|
|
return;
|
|
}
|
|
|
|
if (EditMode)
|
|
{
|
|
GlobalNotificationService.AddSuccessMessage(Localizer["ItemUpdatedSuccess"]);
|
|
}
|
|
else
|
|
{
|
|
GlobalNotificationService.AddSuccessMessage(Localizer["ItemCreatedSuccess"]);
|
|
}
|
|
|
|
NavigationManager.NavigateTo("/items/" + Id);
|
|
}
|
|
|
|
}
|