mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-02 10:22:45 -05:00
662 lines
30 KiB
Plaintext
662 lines
30 KiB
Plaintext
@page "/credentials/create"
|
|
@page "/credentials/{id:guid}/edit"
|
|
@inherits MainBase
|
|
@inject CredentialService CredentialService
|
|
@inject IJSRuntime JSRuntime
|
|
@inject AliasVault.Client.Services.QuickCreateStateService QuickCreateStateService
|
|
@using AliasVault.Client.Services.JsInterop.Models
|
|
@using Microsoft.Extensions.Localization
|
|
@implements IAsyncDisposable
|
|
|
|
<PageHeader
|
|
BreadcrumbItems="@BreadcrumbItems"
|
|
Title="@(EditMode ? Localizer["EditCredentialTitle"] : Localizer["AddCredentialTitle"])"
|
|
Description="@(EditMode ? Localizer["EditCredentialDescription"] : Localizer["AddCredentialDescription"])">
|
|
<CustomActions>
|
|
<ConfirmButton OnClick="TriggerFormSubmit">@Localizer["SaveCredentialButton"]</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">
|
|
<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"/>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="service-url" OnFocus="OnFocusUrlInput" Label="@Localizer["ServiceUrlLabel"]" @bind-Value="Obj.ServiceUrl"></EditFormRow>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (EditMode && Id.HasValue)
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
<TotpCodes TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
<TotpCodes TotpCodeList="@Obj.TotpCodes" TotpCodesChanged="HandleTotpCodesChanged" />
|
|
</div>
|
|
}
|
|
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
<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">
|
|
<div class="grid gap-6">
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Type="textarea" Id="notes" Label="@Localizer["NotesLabel"]" LabelStyle="EditFormRow.FormLabelStyle.Header" @bind-Value="Obj.Notes"></EditFormRow>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-span-1 md:col-span-1 lg:col-span-1">
|
|
<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["AttachmentsSectionHeader"]</h3>
|
|
<div class="grid gap-6">
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<AttachmentUploader
|
|
Attachments="@Obj.Attachments"
|
|
AttachmentsChanged="@HandleAttachmentsChanged" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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["LoginCredentialsSectionHeader"]</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.Username" 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">
|
|
<EditEmailFormRow Id="email" Label="@Localizer["EmailLabel"]" @bind-Value="Obj.Alias.Email"></EditEmailFormRow>
|
|
</div>
|
|
<div class="col-span-6">
|
|
<EditPasswordFormRow Id="password" Label="@Localizer["PasswordLabel"]" @bind-Value="Obj.Password.Value" ShowPassword="IsPasswordVisible"></EditPasswordFormRow>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
@* Without passkey: Email, Username, Password *@
|
|
<div class="col-span-6">
|
|
<EditEmailFormRow Id="email" Label="@Localizer["EmailLabel"]" @bind-Value="Obj.Alias.Email"></EditEmailFormRow>
|
|
</div>
|
|
<div class="col-span-6">
|
|
<EditUsernameFormRow Id="username" Label="@Localizer["UsernameLabel"]" @bind-Value="Obj.Username" OnGenerateNewUsername="GenerateRandomUsername"></EditUsernameFormRow>
|
|
</div>
|
|
<div class="col-span-6">
|
|
<EditPasswordFormRow Id="password" Label="@Localizer["PasswordLabel"]" @bind-Value="Obj.Password.Value" ShowPassword="IsPasswordVisible"></EditPasswordFormRow>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<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.Alias.FirstName"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="last-name" Label="@Localizer["LastNameLabel"]" @bind-Value="Obj.Alias.LastName"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="nickname" Label="@Localizer["NickNameLabel"]" @bind-Value="Obj.Alias.NickName"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="gender" Label="@Localizer["GenderLabel"]" @bind-Value="Obj.Alias.Gender"></EditFormRow>
|
|
</div>
|
|
<div class="col-span-6 sm:col-span-3">
|
|
<EditFormRow Id="birthdate" Label="@Localizer["BirthDateLabel"]" @bind-Value="Obj.AliasBirthDate"></EditFormRow>
|
|
<ValidationMessage For="() => Obj.AliasBirthDate"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="hidden">@Localizer["SaveCredentialButton"]</button>
|
|
</EditForm>
|
|
}
|
|
|
|
@code {
|
|
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Credentials.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 CredentialEdit Obj { get; set; } = new();
|
|
private IJSObjectReference? Module;
|
|
|
|
// 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()
|
|
{
|
|
await KeyboardShortcutService.UnregisterShortcutAsync("gc");
|
|
if (Module is not null)
|
|
{
|
|
await Module.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnInitialized()
|
|
{
|
|
if (Id.HasValue)
|
|
{
|
|
// Edit mode
|
|
EditMode = true;
|
|
}
|
|
else
|
|
{
|
|
// Add mode
|
|
EditMode = false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
|
|
if (EditMode)
|
|
{
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["ViewCredentialBreadcrumb"], Url = $"/credentials/{Id}" });
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["EditCredentialBreadcrumb"] });
|
|
}
|
|
else
|
|
{
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["AddNewCredentialBreadcrumb"] });
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
|
|
if (firstRender)
|
|
{
|
|
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/modules/newIdentityWidget.js");
|
|
|
|
if (EditMode)
|
|
{
|
|
await LoadExistingCredential();
|
|
}
|
|
else
|
|
{
|
|
CreateNewCredential();
|
|
|
|
// Use the state service to pre-fill form data
|
|
if (!string.IsNullOrEmpty(QuickCreateStateService.ServiceName))
|
|
{
|
|
Obj.ServiceName = QuickCreateStateService.ServiceName;
|
|
}
|
|
if (!string.IsNullOrEmpty(QuickCreateStateService.ServiceUrl))
|
|
{
|
|
Obj.ServiceUrl = QuickCreateStateService.ServiceUrl;
|
|
}
|
|
|
|
// Clear the state after using it
|
|
QuickCreateStateService.ClearState();
|
|
}
|
|
|
|
Loading = false;
|
|
StateHasChanged();
|
|
|
|
if (!EditMode)
|
|
{
|
|
// When creating a new alias: start with focus on the service name input.
|
|
await JsInteropService.FocusElementById("service-name");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads an existing credential for editing.
|
|
/// </summary>
|
|
private async Task LoadExistingCredential()
|
|
{
|
|
if (Id is null)
|
|
{
|
|
NavigateAwayWithError(Localizer["CredentialNotExistError"]);
|
|
return;
|
|
}
|
|
|
|
// Load existing Obj, retrieve from service
|
|
var alias = await CredentialService.LoadEntryAsync(Id.Value);
|
|
if (alias is null)
|
|
{
|
|
NavigateAwayWithError(Localizer["CredentialNotExistError"]);
|
|
return;
|
|
}
|
|
|
|
Obj = CredentialEdit.FromEntity(alias);
|
|
|
|
// If BirthDate is MinValue, set AliasBirthDate to empty string
|
|
// TODO: after date field in alias data model is made optional and
|
|
// all min values have been replaced with null, we can remove this check.
|
|
if (Obj.Alias.BirthDate == DateTime.MinValue)
|
|
{
|
|
Obj.AliasBirthDate = string.Empty;
|
|
}
|
|
|
|
if (Obj.ServiceUrl is null)
|
|
{
|
|
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new credential object.
|
|
/// </summary>
|
|
private Credential CreateNewCredentialObject()
|
|
{
|
|
var credential = new Credential();
|
|
credential.Alias = new Alias();
|
|
credential.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
|
|
credential.Service = new Service();
|
|
credential.Passwords = new List<Password> { new Password() };
|
|
credential.TotpCodes = new List<TotpCode>();
|
|
|
|
return credential;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new credential object.
|
|
/// </summary>
|
|
private void CreateNewCredential()
|
|
{
|
|
Obj = CredentialEdit.FromEntity(CreateNewCredentialObject());
|
|
|
|
// Always set AliasBirthDate to empty for new credentials
|
|
// TODO: after date field in alias data model is made optional and
|
|
// all min values have been replaced with null, we can remove this check.
|
|
Obj.AliasBirthDate = string.Empty;
|
|
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an error message and navigates to the home page.
|
|
/// </summary>
|
|
private void NavigateAwayWithError(string errorMessage)
|
|
{
|
|
GlobalNotificationService.AddErrorMessage(errorMessage);
|
|
NavigationManager.NavigateTo("/credentials", false, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// When the URL input is focused, place cursor at the end of the default URL to allow for easy typing.
|
|
/// </summary>
|
|
private void OnFocusUrlInput(FocusEventArgs e)
|
|
{
|
|
if (Obj.ServiceUrl != CredentialService.DefaultServiceUrl)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Use a small delay to ensure the focus is set after the browser's default behavior.
|
|
Task.Delay(1).ContinueWith(_ =>
|
|
{
|
|
JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('service-url').setSelectionRange({CredentialService.DefaultServiceUrl.Length}, {CredentialService.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.Alias.FirstName = string.Empty;
|
|
Obj.Alias.LastName = string.Empty;
|
|
Obj.Alias.NickName = string.Empty;
|
|
Obj.Alias.Gender = string.Empty;
|
|
Obj.AliasBirthDate = string.Empty;
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private bool HasAliasValues()
|
|
{
|
|
return !string.IsNullOrWhiteSpace(Obj.Alias.FirstName) ||
|
|
!string.IsNullOrWhiteSpace(Obj.Alias.LastName) ||
|
|
!string.IsNullOrWhiteSpace(Obj.Alias.NickName) ||
|
|
!string.IsNullOrWhiteSpace(Obj.Alias.Gender) ||
|
|
!string.IsNullOrWhiteSpace(Obj.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()
|
|
{
|
|
// Store current values BEFORE generating, as the service might modify them
|
|
string currentUsername = Obj.Username ?? string.Empty;
|
|
string currentPassword = Obj.Password.Value ?? string.Empty;
|
|
string currentEmail = Obj.Alias.Email ?? string.Empty;
|
|
|
|
// Generate random identity
|
|
var generatedCredential = await CredentialService.GenerateRandomIdentityAsync(Obj.ToEntity());
|
|
var generatedObj = CredentialEdit.FromEntity(generatedCredential);
|
|
|
|
// Restore the original values to prevent service calls above from modifying them
|
|
Obj.Username = currentUsername;
|
|
Obj.Password.Value = currentPassword;
|
|
Obj.Alias.Email = currentEmail;
|
|
|
|
// Apply generated values, respecting manual entries
|
|
Obj.Alias.FirstName = generatedObj.Alias.FirstName;
|
|
Obj.Alias.LastName = generatedObj.Alias.LastName;
|
|
Obj.Alias.NickName = generatedObj.Alias.NickName;
|
|
Obj.Alias.Gender = generatedObj.Alias.Gender;
|
|
Obj.Alias.BirthDate = generatedObj.Alias.BirthDate;
|
|
Obj.AliasBirthDate = generatedObj.AliasBirthDate;
|
|
|
|
// Only overwrite username if it's empty or matches the last generated value
|
|
if (string.IsNullOrWhiteSpace(currentUsername) || currentUsername == LastGeneratedUsername)
|
|
{
|
|
Obj.Username = generatedObj.Username;
|
|
LastGeneratedUsername = generatedObj.Username;
|
|
}
|
|
|
|
// Only overwrite password if it's empty or matches the last generated value
|
|
if (string.IsNullOrWhiteSpace(currentPassword) || currentPassword == LastGeneratedPassword)
|
|
{
|
|
Obj.Password.Value = generatedObj.Password.Value;
|
|
LastGeneratedPassword = generatedObj.Password.Value;
|
|
IsPasswordVisible = true;
|
|
}
|
|
|
|
// Only overwrite email if it's empty or (for new credentials) matches the last generated value or starts with @ which is the default email pattern
|
|
if (string.IsNullOrWhiteSpace(currentEmail) || currentEmail == LastGeneratedEmail || currentEmail.StartsWith("@"))
|
|
{
|
|
Obj.Alias.Email = generatedObj.Alias.Email;
|
|
LastGeneratedEmail = generatedObj.Alias.Email;
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate a new random username based on existing identity, or if no identity is present,
|
|
/// generate a new random identity.
|
|
/// </summary>
|
|
private async Task GenerateRandomUsername()
|
|
{
|
|
// If current object is null, then we create a new random identity.
|
|
AliasVaultIdentity identity;
|
|
if (Obj.Alias.FirstName is null && Obj.Alias.LastName is null && Obj.Alias.BirthDate == DateTime.MinValue)
|
|
{
|
|
// Create new Credential object to avoid modifying the original object
|
|
var randomIdentity = await CredentialService.GenerateRandomIdentityAsync(CreateNewCredentialObject());
|
|
|
|
identity = new AliasVaultIdentity
|
|
{
|
|
FirstName = randomIdentity.Alias.FirstName ?? string.Empty,
|
|
LastName = randomIdentity.Alias.LastName ?? string.Empty,
|
|
BirthDate = randomIdentity.Alias.BirthDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
|
|
Gender = randomIdentity.Alias.Gender,
|
|
NickName = randomIdentity.Alias.NickName ?? string.Empty,
|
|
};
|
|
}
|
|
else
|
|
{
|
|
// Assemble identity model with the current values
|
|
identity = new AliasVaultIdentity
|
|
{
|
|
FirstName = Obj.Alias.FirstName ?? string.Empty,
|
|
LastName = Obj.Alias.LastName ?? string.Empty,
|
|
BirthDate = Obj.Alias.BirthDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
|
|
Gender = Obj.Alias.Gender,
|
|
NickName = Obj.Alias.NickName ?? string.Empty,
|
|
};
|
|
}
|
|
|
|
Obj.Username = await JsInteropService.GenerateRandomUsernameAsync(identity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancel the edit operation and navigate back to the credentials view.
|
|
/// </summary>
|
|
private void Cancel()
|
|
{
|
|
NavigationManager.NavigateTo("/credentials/" + 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 alias 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 CredentialService.DeletePasskeyAsync(passkey.Id);
|
|
}
|
|
Obj.Passkeys.Clear();
|
|
}
|
|
|
|
if (EditMode)
|
|
{
|
|
if (Id is not null)
|
|
{
|
|
Id = await CredentialService.UpdateEntryAsync(Obj.ToEntity());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Id = await CredentialService.InsertEntryAsync(Obj.ToEntity());
|
|
}
|
|
|
|
GlobalLoadingSpinner.Hide();
|
|
StateHasChanged();
|
|
|
|
if (Id is null || Id == Guid.Empty)
|
|
{
|
|
// Error saving.
|
|
GlobalNotificationService.AddErrorMessage(Localizer["ErrorSavingCredentials"], true);
|
|
return;
|
|
}
|
|
|
|
// No error, add success message.
|
|
if (EditMode)
|
|
{
|
|
GlobalNotificationService.AddSuccessMessage(Localizer["CredentialUpdatedSuccess"]);
|
|
}
|
|
else
|
|
{
|
|
GlobalNotificationService.AddSuccessMessage(Localizer["CredentialCreatedSuccess"]);
|
|
}
|
|
|
|
NavigationManager.NavigateTo("/credentials/" + Id);
|
|
}
|
|
}
|