Files
aliasvault/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor
2024-10-07 14:34:53 +02:00

445 lines
17 KiB
Plaintext

@page "/credentials/create"
@page "/credentials/{id:guid}/edit"
@inherits MainBase
@inject CredentialService CredentialService
@using System.Globalization
@using System.Text.Json
@using System.Text.Json.Serialization
@using AliasVault.Generators.Identity
@using AliasVault.Generators.Identity.Implementations.Factories
@using AliasVault.Generators.Identity.Models
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@(EditMode ? "Edit credentials" : "Add credentials")"
Description="@(EditMode ? "Edit the existing credentials entry below." : "Create a new credentials entry below.")">
<CustomActions>
<ConfirmButton OnClick="TriggerFormSubmit">Save Credentials</ConfirmButton>
<CancelButton OnClick="Cancel">Cancel</CancelButton>
</CustomActions>
</PageHeader>
@if (Loading)
{
<LoadingIndicator />
}
else
{
<EditForm @ref="EditFormRef" Model="Obj" OnValidSubmit="SaveAlias">
<DataAnnotationsValidator />
<div class="grid grid-cols-3 px-4 pt-6 lg:gap-4 dark:bg-gray-900">
<div class="col-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">Service</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="service-name" Label="Service Name" @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="Service URL" @bind-Value="Obj.ServiceUrl"></EditFormRow>
</div>
</div>
</div>
<div class="col">
<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">Notes</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Type="textarea" Id="notes" Label="Notes" @bind-Value="Obj.Notes"></EditFormRow>
</div>
</div>
</div>
</div>
<div class="col">
<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">Attachments</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 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">Login credentials</h3>
<div class="mb-4">
<button type="button" @onclick="GenerateRandomAlias" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">Generate Random Alias</button>
</div>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditEmailFormRow Id="email" Label="Email" @bind-Value="Obj.Alias.Email"></EditEmailFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<div class="relative">
<EditFormRow Id="username" Label="Username" @bind-Value="Obj.Username"></EditFormRow>
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomUsername">New Username</button>
</div>
</div>
<div class="col-span-6 sm:col-span-3">
<div class="relative">
<EditFormRow Id="password" Label="Password" @bind-Value="Obj.Password.Value"></EditFormRow>
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">New Password</button>
</div>
</div>
</div>
</div>
<div class="col">
<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">Alias</h3>
<div class="grid gap-6">
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="first-name" Label="First Name" @bind-Value="Obj.Alias.FirstName"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="last-name" Label="Last Name" @bind-Value="Obj.Alias.LastName"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="nickname" Label="Nick Name" @bind-Value="Obj.Alias.NickName"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="gender" Label="Gender" @bind-Value="Obj.Alias.Gender"></EditFormRow>
</div>
<div class="col-span-6 sm:col-span-3">
<EditFormRow Id="birthdate" Label="Birth Date" @bind-Value="Obj.AliasBirthDate"></EditFormRow>
<ValidationMessage For="() => Obj.AliasBirthDate"/>
</div>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="hidden">Save Credentials</button>
</EditForm>
}
@code {
/// <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 CredentialEdit Obj { get; set; } = new();
private IJSObjectReference? Module;
/// <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 = "View credentials entry", Url = $"/credentials/{Id}" });
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Edit credential" });
}
else
{
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Add new credential" });
}
}
/// <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)
{
if (Id is null)
{
// Error loading alias.
GlobalNotificationService.AddErrorMessage("This credential does not exist (anymore). Please try again.");
NavigationManager.NavigateTo("/", false, true);
return;
}
// Load existing Obj, retrieve from service
var alias = await CredentialService.LoadEntryAsync(Id.Value);
if (alias is null)
{
// Error loading alias.
GlobalNotificationService.AddErrorMessage("This credential does not exist (anymore). Please try again.");
NavigationManager.NavigateTo("/", false, true);
return;
}
Obj = CredentialToCredentialEdit(alias);
if (Obj.ServiceUrl is null)
{
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
}
}
else
{
// Create new Obj
var alias = new Credential();
alias.Alias = new Alias();
alias.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
alias.Service = new Service();
alias.Passwords = new List<Password> { new Password() };
Obj = CredentialToCredentialEdit(alias);
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
}
Loading = false;
StateHasChanged();
if (!EditMode)
{
// When creating a new identity: start with focus on the service name input.
await JsInteropService.FocusElementById("service-name");
}
}
}
/// <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 async Task GenerateRandomAlias()
{
GlobalLoadingSpinner.Show();
StateHasChanged();
Obj = CredentialToCredentialEdit(await CredentialService.GenerateRandomIdentity(CredentialEditToCredential(Obj)));
GlobalLoadingSpinner.Hide();
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.
Identity identity;
if (Obj.Alias.FirstName is null && Obj.Alias.LastName is null && Obj.Alias.BirthDate == DateTime.MinValue)
{
identity = await IdentityGeneratorFactory.CreateIdentityGenerator(DbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
}
else
{
// Assemble identity model with the current values
identity = new Identity
{
FirstName = Obj.Alias.FirstName ?? string.Empty,
LastName = Obj.Alias.LastName ?? string.Empty,
BirthDate = Obj.Alias.BirthDate,
Gender = Obj.Alias.Gender == Gender.Female.ToString() ? Gender.Female : Gender.Male,
NickName = Obj.Alias.NickName ?? string.Empty,
};
}
var generator = new UsernameEmailGenerator();
Obj.Username = generator.GenerateUsername(identity);
}
/// <summary>
/// Generate a new random password.
/// </summary>
private void GenerateRandomPassword()
{
Obj.Password.Value = CredentialService.GenerateRandomPassword();
}
private async Task SaveAlias()
{
GlobalLoadingSpinner.Show();
StateHasChanged();
if (EditMode)
{
if (Id is not null)
{
Id = await CredentialService.UpdateEntryAsync(CredentialEditToCredential(Obj));
}
}
else
{
Id = await CredentialService.InsertEntryAsync(CredentialEditToCredential(Obj));
}
GlobalLoadingSpinner.Hide();
StateHasChanged();
if (Id is null || Id == Guid.Empty)
{
// Error saving.
GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true);
return;
}
// No error, add success message.
if (EditMode)
{
GlobalNotificationService.AddSuccessMessage("Credentials updated successfully.");
}
else
{
GlobalNotificationService.AddSuccessMessage("Credentials created successfully.");
}
NavigationManager.NavigateTo("/credentials/" + Id);
}
/// <summary>
/// Helper method to convert a Credential object to a CredentialEdit object.
/// </summary>
private CredentialEdit CredentialToCredentialEdit(Credential alias)
{
// Create a deep copy of the alias object to prevent changes to the original object
// when editing the alias in the form. We only want to save the changes when the user
// clicks the save button.
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve,
MaxDepth = 128 // Adjust this value as needed
};
// Create a deep copy of the credential object
var aliasJson = JsonSerializer.Serialize(alias, options);
var aliasCopy = JsonSerializer.Deserialize<Credential>(aliasJson, options)!;
return new CredentialEdit
{
Id = aliasCopy.Id,
Notes = aliasCopy.Notes ?? string.Empty,
Username = aliasCopy.Username ?? string.Empty,
ServiceName = aliasCopy.Service.Name ?? string.Empty,
ServiceUrl = aliasCopy.Service.Url,
ServiceLogo = aliasCopy.Service.Logo,
Password = aliasCopy.Passwords.FirstOrDefault() ?? new Password
{
Value = string.Empty,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
},
Alias = aliasCopy.Alias,
AliasBirthDate = aliasCopy.Alias.BirthDate.ToString("yyyy-MM-dd"),
Attachments = aliasCopy.Attachments.ToList(),
CreateDate = aliasCopy.CreatedAt,
LastUpdate = aliasCopy.UpdatedAt
};
}
/// <summary>
/// Helper method to convert a CredentialEdit object to a Credential object.
/// </summary>
private Credential CredentialEditToCredential(CredentialEdit alias)
{
var credential = new Credential()
{
Id = alias.Id,
Notes = alias.Notes,
Username = alias.Username,
Service = new Service
{
Name = alias.ServiceName,
Url = alias.ServiceUrl,
Logo = alias.ServiceLogo,
},
Passwords = new List<Password>
{
alias.Password,
},
Alias = alias.Alias,
Attachments = alias.Attachments,
};
if (string.IsNullOrWhiteSpace(alias.AliasBirthDate))
{
credential.Alias.BirthDate = DateTime.MinValue;
}
else
{
credential.Alias.BirthDate = DateTime.Parse(alias.AliasBirthDate, new CultureInfo("en-US"));
}
return credential;
}
/// <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();
}
}