mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-12 11:48:39 -04:00
Merge pull request #159 from lanedirt/142-design-new-client-datamodel-structure-for-credentialsaliases-with-simplified-user-flow
Add quick create new identity popup
This commit is contained in:
@@ -12,8 +12,8 @@ using AliasGenerators.Identity;
|
||||
using AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Dutch identity generator which implements IIdentityGenerator and generates
|
||||
/// random dutch identities.
|
||||
/// Abstract identity generator which implements IIdentityGenerator and generates
|
||||
/// random identities for a certain language.
|
||||
/// </summary>
|
||||
public abstract class IdentityGenerator : IIdentityGenerator
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
// We subscribe to the OnChange event of the PortalMessageService to update the UI when a new message is added
|
||||
// We subscribe to the OnChange event of the GlobalNotificationService to update the UI when a new message is added
|
||||
RefreshAddMessages();
|
||||
GlobalNotificationService.OnChange += RefreshAddMessages;
|
||||
_onChangeSubscribed = true;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" id="@Id" 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 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged">
|
||||
<input type="text" id="@Id" @onfocus="OnFocusEvent" 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 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged">
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<FocusEventArgs> OnFocus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
@@ -46,4 +52,12 @@
|
||||
Value = e.Value?.ToString() ?? string.Empty;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
private async Task OnFocusEvent(FocusEventArgs e)
|
||||
{
|
||||
if (OnFocus.HasDelegate)
|
||||
{
|
||||
await OnFocus.InvokeAsync(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (Module != null)
|
||||
if (Module is not null)
|
||||
{
|
||||
await Module.InvokeVoidAsync("unregisterClickOutsideHandler");
|
||||
await Module.DisposeAsync();
|
||||
@@ -69,9 +69,9 @@
|
||||
/// </summary>
|
||||
private async Task LoadModuleAsync()
|
||||
{
|
||||
if (Module == null)
|
||||
if (Module is null)
|
||||
{
|
||||
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/clickOutsideHandler.js");
|
||||
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/modules/clickOutsideHandler.js");
|
||||
ObjRef = DotNetObjectReference.Create(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inherits AliasVault.Client.Main.Pages.MainBase
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject CredentialService CredentialService
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<button @ref="buttonRef" @onclick="TogglePopup" id="quickIdentityButton" class="px-4 ms-5 py-2 text-sm font-medium text-white bg-gradient-to-r from-primary-500 to-primary-600 hover:from-primary-600 hover:to-primary-700 focus:outline-none dark:from-primary-400 dark:to-primary-500 dark:hover:from-primary-500 dark:hover:to-primary-600 rounded-md shadow-sm transition duration-150 ease-in-out transform hover:scale-105 active:scale-95 focus:shadow-outline">
|
||||
+ New identity
|
||||
</button>
|
||||
|
||||
@if (IsPopupVisible)
|
||||
{
|
||||
<ClickOutsideHandler OnClose="ClosePopup" ContentId="quickIdentityPopup,quickIdentityButton">
|
||||
<div id="quickIdentityPopup" class="absolute z-50 mt-2 p-4 bg-white rounded-lg shadow-xl border border-gray-300"
|
||||
style="@PopupStyle">
|
||||
<h3 class="text-lg font-semibold mb-4">Create New Identity</h3>
|
||||
<EditForm Model="Model" OnValidSubmit="CreateIdentity">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="mb-4">
|
||||
<EditFormRow Id="serviceName" Label="Service Name" @bind-Value="Model.ServiceName"></EditFormRow>
|
||||
<ValidationMessage For="() => Model.ServiceName"/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<EditFormRow Id="serviceUrl" Label="Service URL" OnFocus="OnFocusUrlInput" @bind-Value="Model.ServiceUrl"></EditFormRow>
|
||||
<ValidationMessage For="() => Model.ServiceUrl"/>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<button id="quickIdentitySubmit" type="submit" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<a href="#" @onclick="OpenAdvancedMode" @onclick:preventDefault class="text-sm text-blue-500 hover:text-blue-700">
|
||||
Create via advanced mode
|
||||
</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</ClickOutsideHandler>
|
||||
}
|
||||
|
||||
@code {
|
||||
private const string DefaultServiceUrl = "https://";
|
||||
private bool IsPopupVisible = false;
|
||||
private bool IsCreating = false;
|
||||
private CreateModel Model = new();
|
||||
private string PopupStyle { get; set; } = "";
|
||||
private ElementReference buttonRef;
|
||||
private IJSObjectReference? Module;
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
await KeyboardShortcutService.UnregisterShortcutAsync("gc");
|
||||
if (Module is not null)
|
||||
{
|
||||
await Module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await KeyboardShortcutService.RegisterShortcutAsync("gc", ShowPopup);
|
||||
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/modules/newIdentityWidget.js");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 (Model.ServiceUrl != 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('serviceUrl').setSelectionRange({DefaultServiceUrl.Length}, {DefaultServiceUrl.Length})");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TogglePopup()
|
||||
{
|
||||
IsPopupVisible = !IsPopupVisible;
|
||||
if (IsPopupVisible)
|
||||
{
|
||||
await ShowPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowPopup()
|
||||
{
|
||||
IsPopupVisible = true;
|
||||
|
||||
// Clear the input fields
|
||||
Model = new();
|
||||
Model.ServiceUrl = DefaultServiceUrl;
|
||||
|
||||
await UpdatePopupStyle();
|
||||
await Task.Delay(100); // Give time for the DOM to update
|
||||
await JSRuntime.InvokeVoidAsync("focusElement", "serviceName");
|
||||
}
|
||||
|
||||
private void ClosePopup()
|
||||
{
|
||||
IsPopupVisible = false;
|
||||
}
|
||||
|
||||
private async Task UpdatePopupStyle()
|
||||
{
|
||||
var windowWidth = await JSRuntime.InvokeAsync<int>("getWindowWidth");
|
||||
var buttonRect = await JSRuntime.InvokeAsync<BoundingClientRect>("getElementRect", buttonRef);
|
||||
|
||||
var popupWidth = Math.Min(400, windowWidth - 20); // 20px for some padding
|
||||
var leftPosition = Math.Max(0, Math.Min(buttonRect.Left, windowWidth - popupWidth - 10));
|
||||
|
||||
PopupStyle = $"width: {popupWidth}px; left: {leftPosition}px; top: {buttonRect.Bottom}px;";
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CreateIdentity()
|
||||
{
|
||||
if (IsCreating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsCreating = true;
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
var credential = new Credential();
|
||||
credential.Alias = new Alias();
|
||||
credential.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
|
||||
credential.Service = new Service();
|
||||
credential.Service.Name = Model.ServiceName;
|
||||
|
||||
if (Model.ServiceUrl != DefaultServiceUrl)
|
||||
{
|
||||
credential.Service.Url = Model.ServiceUrl;
|
||||
}
|
||||
|
||||
credential.Passwords = new List<Password> { new() };
|
||||
await CredentialService.GenerateRandomIdentity(credential);
|
||||
|
||||
var id = await CredentialService.InsertEntryAsync(credential);
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
// Error saving.
|
||||
IsCreating = false;
|
||||
GlobalLoadingSpinner.Hide();
|
||||
GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// No error, add success message.
|
||||
GlobalNotificationService.AddSuccessMessage("Credentials created successfully.");
|
||||
|
||||
NavigationManager.NavigateTo("/credentials/" + id);
|
||||
|
||||
IsCreating = false;
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
ClosePopup();
|
||||
}
|
||||
|
||||
private void OpenAdvancedMode()
|
||||
{
|
||||
NavigationManager.NavigateTo("/credentials/create");
|
||||
ClosePopup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bounding client rectangle returned from JavaScript.
|
||||
/// </summary>
|
||||
private sealed class BoundingClientRect
|
||||
{
|
||||
public double Left { get; set; }
|
||||
public double Top { get; set; }
|
||||
public double Right { get; set; }
|
||||
public double Bottom { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Local model for the form with support for validation.
|
||||
/// </summary>
|
||||
private sealed class CreateModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The service name.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Display(Name = "Service Name")]
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The service URL.
|
||||
/// </summary>
|
||||
[Display(Name = "Service URL")]
|
||||
public string ServiceUrl { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
@implements IDisposable
|
||||
@inject DbService DbService
|
||||
|
||||
@if (Loading)
|
||||
{
|
||||
<div class="flex items-center justify-center">
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" Spinning="false" />
|
||||
}
|
||||
<div class="ms-2">
|
||||
@if (Loading)
|
||||
{
|
||||
<div class="flex items-center justify-center">
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" Spinning="false" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<p>Message: @DbService.GetState().CurrentState.Message</p>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<footer class="md:flex md:items-center md:justify-between px-4 2xl:px-0 py-6 md:py-10">
|
||||
<footer class="md:flex md:items-center md:justify-between px-4 2xl:px-0 py-6 md:py-10">
|
||||
<p class="text-sm text-center text-gray-500 mb-4 md:mb-0">
|
||||
© 2024 AliasVault. All rights reserved.
|
||||
</p>
|
||||
@@ -8,3 +8,9 @@
|
||||
<li><a href="https://github.com/lanedirt/AliasVault" target="_blank" class="text-sm font-normal text-gray-500 hover:underline dark:text-gray-400">GitHub</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<div class="text-center text-gray-400 text-sm pt-4 pb-2">@RandomQuote</div>
|
||||
|
||||
@code {
|
||||
private string RandomQuote => "Tip: You can use the 'gc' keyboard shortcut to quickly create a new identity.";
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -19,43 +19,42 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end items-center lg:order-2">
|
||||
<CreateNewIdentityWidget />
|
||||
|
||||
<DbStatusIndicator />
|
||||
|
||||
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
<div id="tooltip-toggle" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip" data-popper-placement="bottom" style="position: absolute; inset: 0px auto auto 0px; margin: 0px; transform: translate3d(1377px, 60px, 0px);">
|
||||
Toggle dark mode
|
||||
<div class="tooltip-arrow" data-popper-arrow="" style="position: absolute; left: 0px; transform: translate3d(68.5px, 0px, 0px);"></div>
|
||||
</div>
|
||||
|
||||
<button @onclick="ToggleMenu" type="button" class="flex mx-3 text-sm bg-gray-800 rounded-full md:mr-0 flex-shrink-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="userMenuDropdownButton" aria-expanded="false" data-dropdown-toggle="userMenuDropdown">
|
||||
<button @onclick="ToggleMenu" type="button" class="flex ms-3 text-sm bg-gray-800 rounded-full md:mr-0 flex-shrink-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="userMenuDropdownButton" aria-expanded="false" data-dropdown-toggle="userMenuDropdown">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<img class="w-8 h-8 rounded-full" src="/img/avatar.webp" alt="user photo">
|
||||
</button>
|
||||
|
||||
@if (IsMenuOpen)
|
||||
{
|
||||
<div class="absolute top-10 right-0 z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600" id="userMenuDropdown" data-popper-placement="bottom">
|
||||
<div class="py-3 px-4">
|
||||
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@Username</span>
|
||||
</div>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
|
||||
<li>
|
||||
<a href="/settings/general" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">General settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings/vault" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Vault settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<a href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="absolute top-10 right-0 z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600 @(IsMenuOpen ? "block" : "hidden")" id="userMenuDropdown" data-popper-placement="bottom">
|
||||
<div class="py-3 px-4">
|
||||
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@Username</span>
|
||||
</div>
|
||||
}
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
|
||||
<li>
|
||||
<a href="/settings/general" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">General settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings/vault" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Vault settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="w-full text-start py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">
|
||||
Toggle dark mode
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5 align-middle inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5 align-middle inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<a href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button @onclick="ToggleMobileMenu" type="button" id="toggleMobileMenuButton" class="items-center p-2 text-gray-500 rounded-lg md:ml-2 lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600">
|
||||
<span class="sr-only">Open menu</span>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
@page "/add-credentials"
|
||||
@page "/credentials/create"
|
||||
@page "/credentials/{id:guid}/edit"
|
||||
@inherits MainBase
|
||||
@inject CredentialService CredentialService
|
||||
@using System.Globalization
|
||||
@using AliasGenerators.Identity.Implementations
|
||||
@using AliasGenerators.Identity.Models
|
||||
@using AliasGenerators.Password
|
||||
@using AliasGenerators.Password.Implementations
|
||||
|
||||
@if (EditMode)
|
||||
{
|
||||
@@ -135,10 +131,6 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@@ -218,16 +210,14 @@ else
|
||||
// Create new Obj
|
||||
var alias = new Credential();
|
||||
alias.Alias = new Alias();
|
||||
alias.Alias.Email = "@" + GetDefaultEmailDomain();
|
||||
alias.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
|
||||
alias.Service = new Service();
|
||||
alias.Passwords = new List<Password> { new Password() };
|
||||
|
||||
Obj = CredentialToCredentialEdit(alias);
|
||||
}
|
||||
|
||||
// Hide loading spinner
|
||||
Loading = false;
|
||||
// Force re-render invoke so the charts can be rendered
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -243,23 +233,7 @@ else
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
// Generate a random identity using the IIdentityGenerator implementation.
|
||||
var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(DbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
|
||||
|
||||
// Generate random values for the Identity properties
|
||||
Obj.Username = identity.NickName;
|
||||
Obj.Alias.FirstName = identity.FirstName;
|
||||
Obj.Alias.LastName = identity.LastName;
|
||||
Obj.Alias.NickName = identity.NickName;
|
||||
Obj.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female";
|
||||
Obj.AliasBirthDate = identity.BirthDate.ToString("yyyy-MM-dd");
|
||||
|
||||
// Set the email
|
||||
var emailDomain = GetDefaultEmailDomain();
|
||||
Obj.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}";
|
||||
|
||||
// Generate password
|
||||
GenerateRandomPassword();
|
||||
Obj = CredentialToCredentialEdit(await CredentialService.GenerateRandomIdentity(CredentialEditToCredential(Obj)));
|
||||
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
@@ -267,9 +241,7 @@ else
|
||||
|
||||
private void GenerateRandomPassword()
|
||||
{
|
||||
// Generate a random password using a IPasswordGenerator implementation.
|
||||
IPasswordGenerator passwordGenerator = new SpamOkPasswordGenerator();
|
||||
Obj.Password.Value = passwordGenerator.GenerateRandomPassword();
|
||||
Obj.Password.Value = CredentialService.GenerateRandomPassword();
|
||||
}
|
||||
|
||||
private async Task SaveAlias()
|
||||
@@ -277,8 +249,6 @@ else
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
// Sanity check for unittest. Delete later if not needed.
|
||||
// Try to parse birthdate as datetime. if it fails, set it to empty.
|
||||
if (EditMode)
|
||||
{
|
||||
if (Id is not null)
|
||||
@@ -314,6 +284,9 @@ else
|
||||
NavigationManager.NavigateTo("/credentials/" + Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a Credential object to a CredentialEdit object.
|
||||
/// </summary>
|
||||
private CredentialEdit CredentialToCredentialEdit(Credential alias)
|
||||
{
|
||||
return new CredentialEdit
|
||||
@@ -338,6 +311,9 @@ else
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a CredentialEdit object to a Credential object.
|
||||
/// </summary>
|
||||
private Credential CredentialEditToCredential(CredentialEdit alias)
|
||||
{
|
||||
var credential = new Credential()
|
||||
@@ -370,29 +346,4 @@ else
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default email domain based on settings and available domains.
|
||||
/// </summary>
|
||||
private string GetDefaultEmailDomain()
|
||||
{
|
||||
var defaultDomain = DbService.Settings.DefaultEmailDomain;
|
||||
|
||||
// Function to check if a domain is valid
|
||||
bool IsValidDomain(string domain) =>
|
||||
!string.IsNullOrEmpty(domain) &&
|
||||
domain != "DISABLED.TLD" &&
|
||||
(Config.PublicEmailDomains.Contains(domain) || Config.PrivateEmailDomains.Contains(domain));
|
||||
|
||||
// Get the first valid domain from private or public domains
|
||||
string GetFirstValidDomain() =>
|
||||
Config.PrivateEmailDomains.Find(IsValidDomain) ??
|
||||
Config.PublicEmailDomains.FirstOrDefault() ??
|
||||
"example.com";
|
||||
|
||||
// Use the default domain if it's valid, otherwise get the first valid domain
|
||||
string domainToUse = IsValidDomain(defaultDomain) ? defaultDomain : GetFirstValidDomain();
|
||||
|
||||
return domainToUse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Credentials</h1>
|
||||
<a href="/add-credentials" 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">
|
||||
+ Add new credentials
|
||||
</a>
|
||||
</div>
|
||||
<p>Find all of your credentials below.</p>
|
||||
</div>
|
||||
|
||||
@@ -116,16 +116,15 @@ else
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await LoadEntryAsync();
|
||||
}
|
||||
await base.OnParametersSetAsync();
|
||||
await LoadEntryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the credentials entry.
|
||||
/// </summary>
|
||||
private async Task LoadEntryAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
@@ -60,6 +60,12 @@ public class MainBase : OwningComponentBase
|
||||
[Inject]
|
||||
public DbService DbService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the KeyboardShortcutService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public KeyboardShortcutService KeyboardShortcutService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AuthService.
|
||||
/// </summary>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Vault settings" });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "General settings" });
|
||||
|
||||
DefaultEmailDomain = DbService.Settings.DefaultEmailDomain;
|
||||
AutoEmailRefresh = DbService.Settings.AutoEmailRefresh;
|
||||
|
||||
@@ -43,6 +43,7 @@ builder.Services.AddLogging(logging =>
|
||||
|
||||
logging.AddFilter("Microsoft.AspNetCore.Identity.DataProtectorTokenProvider", LogLevel.Error);
|
||||
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Error);
|
||||
logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Error);
|
||||
});
|
||||
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
@@ -67,6 +68,7 @@ builder.Services.AddScoped<CredentialService>();
|
||||
builder.Services.AddScoped<DbService>();
|
||||
builder.Services.AddScoped<GlobalNotificationService>();
|
||||
builder.Services.AddScoped<GlobalLoadingService>();
|
||||
builder.Services.AddScoped<KeyboardShortcutService>();
|
||||
builder.Services.AddScoped<JsInteropService>();
|
||||
builder.Services.AddSingleton<ClipboardCopyService>();
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AliasClientDb;
|
||||
using AliasGenerators.Identity.Implementations;
|
||||
using AliasGenerators.Identity.Models;
|
||||
using AliasGenerators.Password;
|
||||
using AliasGenerators.Password.Implementations;
|
||||
using AliasVault.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Identity = AliasGenerators.Identity.Models.Identity;
|
||||
@@ -21,8 +25,73 @@ using Identity = AliasGenerators.Identity.Models.Identity;
|
||||
/// <summary>
|
||||
/// Service class for alias operations.
|
||||
/// </summary>
|
||||
public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
public class CredentialService(HttpClient httpClient, DbService dbService, Config config)
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a random password for a credential.
|
||||
/// </summary>
|
||||
/// <returns>Random password.</returns>
|
||||
public static string GenerateRandomPassword()
|
||||
{
|
||||
// Generate a random password using a IPasswordGenerator implementation.
|
||||
var passwordGenerator = new SpamOkPasswordGenerator();
|
||||
return passwordGenerator.GenerateRandomPassword();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random identity for a credential.
|
||||
/// </summary>
|
||||
/// <param name="credential">The credential object to update.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task<Credential> GenerateRandomIdentity(Credential credential)
|
||||
{
|
||||
// Generate a random identity using the IIdentityGenerator implementation.
|
||||
var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(dbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
|
||||
|
||||
// Generate random values for the Identity properties
|
||||
credential.Username = identity.NickName;
|
||||
credential.Alias.FirstName = identity.FirstName;
|
||||
credential.Alias.LastName = identity.LastName;
|
||||
credential.Alias.NickName = identity.NickName;
|
||||
credential.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female";
|
||||
credential.Alias.BirthDate = identity.BirthDate;
|
||||
|
||||
// Set the email
|
||||
var emailDomain = GetDefaultEmailDomain();
|
||||
credential.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}";
|
||||
|
||||
// Generate password
|
||||
credential.Passwords.First().Value = GenerateRandomPassword();
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default email domain based on settings and available domains.
|
||||
/// </summary>
|
||||
/// <returns>Default email domain.</returns>
|
||||
public string GetDefaultEmailDomain()
|
||||
{
|
||||
var defaultDomain = dbService.Settings.DefaultEmailDomain;
|
||||
|
||||
// Function to check if a domain is valid
|
||||
bool IsValidDomain(string domain) =>
|
||||
!string.IsNullOrEmpty(domain) &&
|
||||
domain != "DISABLED.TLD" &&
|
||||
(config.PublicEmailDomains.Contains(domain) || config.PrivateEmailDomains.Contains(domain));
|
||||
|
||||
// Get the first valid domain from private or public domains
|
||||
string GetFirstValidDomain() =>
|
||||
config.PrivateEmailDomains.Find(IsValidDomain) ??
|
||||
config.PublicEmailDomains.FirstOrDefault() ??
|
||||
"example.com";
|
||||
|
||||
// Use the default domain if it's valid, otherwise get the first valid domain
|
||||
string domainToUse = IsValidDomain(defaultDomain) ? defaultDomain : GetFirstValidDomain();
|
||||
|
||||
return domainToUse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate random identity by calling the IdentityGenerator API.
|
||||
/// </summary>
|
||||
|
||||
@@ -20,14 +20,14 @@ public class GlobalNotificationService
|
||||
public event Action? OnChange;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets success messages that should be displayed to the user.
|
||||
/// Gets success messages that should be displayed to the user.
|
||||
/// </summary>
|
||||
protected List<string> SuccessMessages { get; set; } = [];
|
||||
protected List<string> SuccessMessages { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets error messages that should be displayed to the user.
|
||||
/// Gets error messages that should be displayed to the user.
|
||||
/// </summary>
|
||||
protected List<string> ErrorMessages { get; set; } = [];
|
||||
protected List<string> ErrorMessages { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a success message to the list of messages that should be displayed to the user.
|
||||
|
||||
118
src/AliasVault.Client/Services/KeyboardShortcutService.cs
Normal file
118
src/AliasVault.Client/Services/KeyboardShortcutService.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="KeyboardShortcutService.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Services;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
/// Service class for alias operations.
|
||||
/// </summary>
|
||||
public class KeyboardShortcutService : IAsyncDisposable
|
||||
{
|
||||
private readonly DotNetObjectReference<CallbackWrapper> _dotNetHelper;
|
||||
private readonly NavigationManager _navigationManager;
|
||||
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyboardShortcutService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime">IJSRuntime instance.</param>
|
||||
/// <param name="navigationManager">NavigationManager instance.</param>
|
||||
public KeyboardShortcutService(IJSRuntime jsRuntime, NavigationManager navigationManager)
|
||||
{
|
||||
_dotNetHelper = DotNetObjectReference.Create(new CallbackWrapper());
|
||||
_navigationManager = navigationManager;
|
||||
|
||||
moduleTask = new Lazy<Task<IJSObjectReference>>(() => jsRuntime.InvokeAsync<IJSObjectReference>(
|
||||
"import", "./js/modules/keyboardShortcuts.js").AsTask());
|
||||
|
||||
_ = RegisterStaticShortcuts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a keyboard shortcut with the given keys and callback.
|
||||
/// </summary>
|
||||
/// <param name="keys">The keyboard keys.</param>
|
||||
/// <param name="callback">Callback when shortcut is pressed.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task RegisterShortcutAsync(string keys, Func<Task> callback)
|
||||
{
|
||||
_dotNetHelper.Value.RegisterCallback(keys, callback);
|
||||
var module = await moduleTask.Value;
|
||||
await module.InvokeVoidAsync("registerShortcut", keys, _dotNetHelper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a keyboard shortcut with the given keys.
|
||||
/// </summary>
|
||||
/// <param name="keys">The keyboard keys.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task UnregisterShortcutAsync(string keys)
|
||||
{
|
||||
_dotNetHelper.Value.UnregisterCallback(keys);
|
||||
var module = await moduleTask.Value;
|
||||
await module!.InvokeVoidAsync("unregisterShortcut", keys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the service.
|
||||
/// </summary>
|
||||
/// <returns>ValueTask.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
await module.InvokeVoidAsync("unregisterAllShortcuts");
|
||||
await module.DisposeAsync();
|
||||
|
||||
_dotNetHelper.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers static shortcuts that are always available.
|
||||
/// </summary>
|
||||
private async Task RegisterStaticShortcuts()
|
||||
{
|
||||
// Global shortcut: Go home
|
||||
await RegisterShortcutAsync("gh", () =>
|
||||
{
|
||||
_navigationManager.NavigateTo("/");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper class for callback functions that are invoked from JavaScript.
|
||||
/// </summary>
|
||||
private sealed class CallbackWrapper
|
||||
{
|
||||
private readonly Dictionary<string, Func<Task>> _callbacks = new Dictionary<string, Func<Task>>();
|
||||
|
||||
public void RegisterCallback(string keys, Func<Task> callback)
|
||||
{
|
||||
_callbacks[keys] = callback;
|
||||
}
|
||||
|
||||
public void UnregisterCallback(string keys)
|
||||
{
|
||||
_callbacks.Remove(keys);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task Invoke(string keys)
|
||||
{
|
||||
if (_callbacks.TryGetValue(keys, out var callback))
|
||||
{
|
||||
await callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
@using AliasVault.Client.Main.Components.Forms
|
||||
@using AliasVault.Client.Main.Components.Layout
|
||||
@using AliasVault.Client.Main.Components.Loading
|
||||
@using AliasVault.Client.Main.Components.Widgets
|
||||
@using AliasVault.Client.Main.Models
|
||||
@using AliasVault.Client.Services
|
||||
@using AliasVault.Client.Services.Auth
|
||||
|
||||
@@ -566,14 +566,6 @@ video {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
@@ -612,6 +604,10 @@ video {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.right-1 {
|
||||
right: 0.25rem;
|
||||
}
|
||||
|
||||
.right-10 {
|
||||
right: 2.5rem;
|
||||
}
|
||||
@@ -620,26 +616,10 @@ video {
|
||||
top: 2.5rem;
|
||||
}
|
||||
|
||||
.right-4 {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.right-1 {
|
||||
right: 0.25rem;
|
||||
}
|
||||
|
||||
.right-5 {
|
||||
right: 1.25rem;
|
||||
}
|
||||
|
||||
.top-20 {
|
||||
top: 5rem;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.z-30 {
|
||||
z-index: 30;
|
||||
}
|
||||
@@ -648,10 +628,6 @@ video {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.z-20 {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
@@ -664,11 +640,6 @@ video {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.mx-3 {
|
||||
margin-left: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.mx-4 {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
@@ -716,6 +687,10 @@ video {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
@@ -744,6 +719,18 @@ video {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.ms-2 {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.ms-3 {
|
||||
margin-inline-start: 0.75rem;
|
||||
}
|
||||
|
||||
.ms-5 {
|
||||
margin-inline-start: 1.25rem;
|
||||
}
|
||||
|
||||
.mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
@@ -752,6 +739,10 @@ video {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -764,18 +755,6 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -816,6 +795,10 @@ video {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
@@ -840,18 +823,10 @@ video {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.h-\[700px\] {
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -868,6 +843,14 @@ video {
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.w-3\/4 {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
@@ -896,20 +879,12 @@ video {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.w-3\/4 {
|
||||
width: 75%;
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.min-w-full {
|
||||
@@ -936,16 +911,6 @@ video {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: .5;
|
||||
@@ -956,31 +921,14 @@ video {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-ping {
|
||||
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(-25%);
|
||||
animation-timing-function: cubic-bezier(0.8,0,1,1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0,0,0.2,1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1s infinite;
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
@@ -1069,6 +1017,12 @@ video {
|
||||
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
@@ -1099,12 +1053,6 @@ video {
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||
@@ -1221,16 +1169,6 @@ video {
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(252 165 165 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-primary-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(248 185 99 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-primary-100 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(253 222 133 / var(--tw-border-opacity));
|
||||
@@ -1266,6 +1204,11 @@ video {
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
@@ -1276,21 +1219,26 @@ video {
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-900 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 253 244 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-green-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(248 185 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
@@ -1316,21 +1264,6 @@ video {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(248 185 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -1339,6 +1272,20 @@ video {
|
||||
--tw-bg-opacity: 0.75;
|
||||
}
|
||||
|
||||
.bg-gradient-to-r {
|
||||
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
||||
}
|
||||
|
||||
.from-primary-500 {
|
||||
--tw-gradient-from: #f49541 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.to-primary-600 {
|
||||
--tw-gradient-to: #d68338 var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.fill-primary-600 {
|
||||
fill: #d68338;
|
||||
}
|
||||
@@ -1355,6 +1302,10 @@ video {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
@@ -1363,10 +1314,6 @@ video {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -1392,6 +1339,11 @@ video {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.px-7 {
|
||||
padding-left: 1.75rem;
|
||||
padding-right: 1.75rem;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
@@ -1422,9 +1374,8 @@ video {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.px-7 {
|
||||
padding-left: 1.75rem;
|
||||
padding-right: 1.75rem;
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pr-10 {
|
||||
@@ -1443,6 +1394,14 @@ video {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.pt-6 {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
@@ -1451,10 +1410,6 @@ video {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1463,6 +1418,10 @@ video {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-start {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1618,15 +1577,6 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.opacity-25 {
|
||||
opacity: 0.25;
|
||||
}
|
||||
@@ -1663,15 +1613,21 @@ video {
|
||||
outline-width: 0px;
|
||||
}
|
||||
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-colors {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.duration-150 {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
@@ -1679,8 +1635,14 @@ video {
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
.duration-300 {
|
||||
transition-duration: 300ms;
|
||||
.ease-in-out {
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.hover\:scale-105:hover {
|
||||
--tw-scale-x: 1.05;
|
||||
--tw-scale-y: 1.05;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-600:hover {
|
||||
@@ -1703,6 +1665,11 @@ video {
|
||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-300:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-50:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
@@ -1718,6 +1685,11 @@ video {
|
||||
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-primary-400:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(246 167 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-primary-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
|
||||
@@ -1738,14 +1710,24 @@ video {
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-300:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
.hover\:from-primary-600:hover {
|
||||
--tw-gradient-from: #d68338 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.hover\:bg-primary-400:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(246 167 82 / var(--tw-bg-opacity));
|
||||
.hover\:to-primary-700:hover {
|
||||
--tw-gradient-to: #b8702f var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.hover\:text-blue-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(29 78 216 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-500:hover {
|
||||
@@ -1778,50 +1760,45 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.focus\:border-primary-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:border-blue-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:border-primary-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:outline-none:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.focus\:ring-4:focus {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-2:focus {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-4:focus {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-blue-300:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-200:focus {
|
||||
.focus\:ring-blue-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-300:focus {
|
||||
@@ -1829,6 +1806,11 @@ video {
|
||||
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-400:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-green-300:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(134 239 172 / var(--tw-ring-opacity));
|
||||
@@ -1849,25 +1831,16 @@ video {
|
||||
--tw-ring-color: rgb(252 165 165 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-indigo-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-400:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-blue-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-offset-2:focus {
|
||||
--tw-ring-offset-width: 2px;
|
||||
}
|
||||
|
||||
.active\:scale-95:active {
|
||||
--tw-scale-x: .95;
|
||||
--tw-scale-y: .95;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
|
||||
.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
|
||||
@@ -1937,6 +1910,16 @@ video {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
|
||||
.dark\:from-primary-400:is(.dark *) {
|
||||
--tw-gradient-from: #f6a752 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(246 167 82 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.dark\:to-primary-500:is(.dark *) {
|
||||
--tw-gradient-to: #f49541 var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.dark\:text-blue-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(96 165 250 / var(--tw-text-opacity));
|
||||
@@ -2011,6 +1994,11 @@ video {
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-500:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
@@ -2041,9 +2029,19 @@ video {
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-500:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
.dark\:hover\:from-primary-500:hover:is(.dark *) {
|
||||
--tw-gradient-from: #f49541 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position);
|
||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||
}
|
||||
|
||||
.dark\:hover\:to-primary-600:hover:is(.dark *) {
|
||||
--tw-gradient-to: #d68338 var(--tw-gradient-to-position);
|
||||
}
|
||||
|
||||
.dark\:hover\:text-blue-200:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(191 219 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-primary-500:hover:is(.dark *) {
|
||||
@@ -2056,9 +2054,9 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-blue-200:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(191 219 254 / var(--tw-text-opacity));
|
||||
.dark\:focus\:border-blue-500:focus:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-primary-500:focus:is(.dark *) {
|
||||
@@ -2066,9 +2064,14 @@ video {
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-blue-500:focus:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
.dark\:focus\:ring-blue-500:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-blue-600:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-blue-800:focus:is(.dark *) {
|
||||
@@ -2121,16 +2124,6 @@ video {
|
||||
--tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-blue-500:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-blue-600:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:col-span-3 {
|
||||
grid-column: span 3 / span 3;
|
||||
@@ -2148,10 +2141,6 @@ video {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sm\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
|
||||
@@ -47,8 +47,7 @@
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="js/dark-mode.js?v=@CacheBuster"></script>
|
||||
<script src="js/cryptoInterop.js?v=@CacheBuster"></script>
|
||||
<script src="js/crypto.js?v=@CacheBuster"></script>
|
||||
<script src="js/utilities.js?v=@CacheBuster"></script>
|
||||
<script src="_framework/blazor.webassembly.js?v=@CacheBuster"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
function initDarkModeSwitcher() {
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggleDarkIcon === null && themeToggleLightIcon === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default to light mode if not set.
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
|
||||
let event = new Event('dark-mode');
|
||||
|
||||
themeToggleBtn.addEventListener('click', function () {
|
||||
// toggle icons
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
|
||||
// if set via local storage previously
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
}
|
||||
// if NOT set via local storage previously
|
||||
} else if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
// clickOutsideHandler.js
|
||||
let currentHandler = null;
|
||||
let currentDotNetHelper = null;
|
||||
|
||||
export function registerClickOutsideHandler(dotNetHelper, contentId, methodName) {
|
||||
export function registerClickOutsideHandler(dotNetHelper, contentIds, methodName) {
|
||||
unregisterClickOutsideHandler();
|
||||
|
||||
currentDotNetHelper = dotNetHelper;
|
||||
currentHandler = (event) => {
|
||||
const content = document.getElementById(contentId);
|
||||
if (!content) return;
|
||||
const idArray = Array.isArray(contentIds) ? contentIds : contentIds.split(',').map(id => id.trim());
|
||||
|
||||
currentHandler = (event) => {
|
||||
const isOutside = idArray.every(id => {
|
||||
const content = document.getElementById(id);
|
||||
return !content?.contains(event.target);
|
||||
});
|
||||
|
||||
const isOutside = !content.contains(event.target);
|
||||
const isEscapeKey = event.type === 'keydown' && event.key === 'Escape';
|
||||
|
||||
if (isOutside || isEscapeKey) {
|
||||
@@ -0,0 +1,40 @@
|
||||
let shortcuts = {};
|
||||
let lastKeyPressed = '';
|
||||
let lastKeyPressTime = 0;
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
export function handleKeyPress(event) {
|
||||
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
if (currentTime - this.lastKeyPressTime > 500) {
|
||||
lastKeyPressed = '';
|
||||
}
|
||||
|
||||
lastKeyPressed += key;
|
||||
lastKeyPressTime = currentTime;
|
||||
|
||||
const shortcut = shortcuts[lastKeyPressed];
|
||||
if (shortcut) {
|
||||
event.preventDefault();
|
||||
shortcut.dotNetHelper.invokeMethodAsync('Invoke', lastKeyPressed);
|
||||
lastKeyPressed = '';
|
||||
}
|
||||
}
|
||||
|
||||
export function registerShortcut(keys, dotNetHelper) {
|
||||
shortcuts[keys.toLowerCase()] = { dotNetHelper: dotNetHelper };
|
||||
}
|
||||
|
||||
export function unregisterShortcut(keys) {
|
||||
delete shortcuts[keys.toLowerCase()];
|
||||
}
|
||||
|
||||
export function unregisterAllShortcuts() {
|
||||
shortcuts = {};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
window.getWindowWidth = function() {
|
||||
return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
};
|
||||
window.getElementRect = function(element) {
|
||||
if (element) {
|
||||
const rect = {
|
||||
left: element.offsetLeft,
|
||||
top: element.offsetTop,
|
||||
right: element.offsetLeft + element.offsetWidth,
|
||||
bottom: element.offsetTop + element.offsetHeight,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight
|
||||
};
|
||||
let parent = element.offsetParent;
|
||||
while (parent) {
|
||||
rect.left += parent.offsetLeft;
|
||||
rect.top += parent.offsetTop;
|
||||
parent = parent.offsetParent;
|
||||
}
|
||||
rect.right = rect.left + rect.width;
|
||||
rect.bottom = rect.top + rect.height;
|
||||
return rect;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -10,10 +10,6 @@ function downloadFileFromStream(fileName, contentStreamReference) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
window.initTopMenu = function() {
|
||||
initDarkModeSwitcher();
|
||||
};
|
||||
|
||||
window.topMenuClickOutsideHandler = (dotNetHelper) => {
|
||||
document.addEventListener('click', (event) => {
|
||||
const menu = document.getElementById('userMenuDropdown');
|
||||
@@ -43,3 +39,69 @@ window.clipboardCopy = {
|
||||
window.blazorNavigate = (url) => {
|
||||
Blazor.navigateTo(url);
|
||||
};
|
||||
|
||||
window.focusElement = (elementId) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
function initDarkModeSwitcher() {
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggleDarkIcon === null && themeToggleLightIcon === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default to light mode if not set.
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
|
||||
let event = new Event('dark-mode');
|
||||
|
||||
themeToggleBtn.addEventListener('click', function () {
|
||||
// toggle icons
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
|
||||
// if set via local storage previously
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
}
|
||||
// if NOT set via local storage previously
|
||||
} else if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
window.initTopMenu = function() {
|
||||
initDarkModeSwitcher();
|
||||
};
|
||||
|
||||
|
||||
@@ -170,8 +170,8 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
/// <returns>Async task.</returns>
|
||||
protected async Task CreateCredentialEntry(Dictionary<string, string>? formValues = null)
|
||||
{
|
||||
await NavigateUsingBlazorRouter("add-credentials");
|
||||
await WaitForUrlAsync("add-credentials", "Add credentials");
|
||||
await NavigateUsingBlazorRouter("credentials/create");
|
||||
await WaitForUrlAsync("credentials/create", "Add credentials");
|
||||
|
||||
// Check if a button with text "Generate Random Identity" appears
|
||||
var generateButton = Page.Locator("text=Generate Random Identity");
|
||||
@@ -183,11 +183,11 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
|
||||
var submitButton = Page.Locator("text=Save Credentials").First;
|
||||
await submitButton.ClickAsync();
|
||||
await WaitForUrlAsync("credentials/**", "Login credentials");
|
||||
await WaitForUrlAsync("credentials/**", "View credentials entry");
|
||||
|
||||
// Check if the credential was created
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("Login credentials"), "Credential not created.");
|
||||
Assert.That(pageContent, Does.Contain("View credentials entry"), "Credential not created.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -160,7 +160,13 @@ public abstract class PlaywrightTest
|
||||
await Page.WaitForURLAsync("**/" + relativeUrl, new PageWaitForURLOptions() { Timeout = TestDefaults.DefaultTimeout });
|
||||
|
||||
// Wait for actual content to load (web API calls, etc.)
|
||||
await Page.WaitForSelectorAsync("text=" + waitForText, new PageWaitForSelectorOptions() { Timeout = TestDefaults.DefaultTimeout });
|
||||
await Page.GetByText(waitForText, new PageGetByTextOptions { Exact = false })
|
||||
.First
|
||||
.WaitForAsync(new LocatorWaitForOptions
|
||||
{
|
||||
Timeout = TestDefaults.DefaultTimeout,
|
||||
State = WaitForSelectorState.Attached,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -36,7 +36,7 @@ public class AuthTests : AdminPlaywrightTest
|
||||
await submitButton.ClickAsync();
|
||||
|
||||
// Wait for current page to refresh and confirm message shows.
|
||||
await WaitForUrlAsync("account/manage/change-password", "Error");
|
||||
await WaitForUrlAsync("account/manage/change-password", "Incorrect");
|
||||
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("Error: Incorrect password."), "No error shown after submitting change password field with wrong old password.");
|
||||
|
||||
@@ -49,6 +49,39 @@ public class CredentialTest : ClientPlaywrightTest
|
||||
Assert.That(pageContent, Does.Contain(serviceName), "Created credential service name does not appear on alias page.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if creating a new credential entry works with quick create widget.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task CreateCredentialWidgetTest()
|
||||
{
|
||||
// Navigate to homepage
|
||||
await NavigateUsingBlazorRouter("credentials");
|
||||
await WaitForUrlAsync("credentials", "Credentials");
|
||||
|
||||
// Create a new alias with service name = "Test Service".
|
||||
var serviceName = "Test Service Widget";
|
||||
|
||||
var widgetButton = Page.Locator("button[id='quickIdentityButton']");
|
||||
Assert.That(widgetButton, Is.Not.Null, "Create new identity widget button not found.");
|
||||
await widgetButton.ClickAsync();
|
||||
|
||||
await InputHelper.FillInputFields(new Dictionary<string, string>
|
||||
{
|
||||
{ "serviceName", serviceName },
|
||||
});
|
||||
|
||||
var submitButton = Page.Locator("button[id='quickIdentitySubmit']");
|
||||
await submitButton.ClickAsync();
|
||||
|
||||
await WaitForUrlAsync("credentials/**", "View credentials entry");
|
||||
|
||||
// Check that the service name is present in the content.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain(serviceName), "Created credential service name does not appear on alias page.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if editing a created credential entry works.
|
||||
/// </summary>
|
||||
@@ -97,8 +130,8 @@ public class CredentialTest : ClientPlaywrightTest
|
||||
// Create a new alias with service name = "Test Service".
|
||||
var serviceName = "Test Service";
|
||||
|
||||
await NavigateUsingBlazorRouter("add-credentials");
|
||||
await WaitForUrlAsync("add-credentials", "Add credentials");
|
||||
await NavigateUsingBlazorRouter("credentials/create");
|
||||
await WaitForUrlAsync("credentials/create", "Add credentials");
|
||||
|
||||
await InputHelper.FillInputFields(
|
||||
fieldValues: new Dictionary<string, string>
|
||||
|
||||
@@ -23,7 +23,7 @@ public class GeneralSettingsTest : ClientPlaywrightTest
|
||||
public async Task MutateDefaultEmailDomainTest()
|
||||
{
|
||||
await NavigateUsingBlazorRouter("settings/general");
|
||||
await WaitForUrlAsync("settings/general", "General settings");
|
||||
await WaitForUrlAsync("settings/general", "Configure general");
|
||||
|
||||
// Check if the expected content is present.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
@@ -34,8 +34,8 @@ public class GeneralSettingsTest : ClientPlaywrightTest
|
||||
await defaultEmailDomainField.SelectOptionAsync("example2.tld");
|
||||
|
||||
// Go to new credential create page and assert that the default email domain is visible on the page.
|
||||
await NavigateUsingBlazorRouter("add-credentials");
|
||||
await WaitForUrlAsync("add-credentials", "Add credentials");
|
||||
await NavigateUsingBlazorRouter("credentials/create");
|
||||
await WaitForUrlAsync("credentials/create", "Add credentials");
|
||||
|
||||
var defaultEmailDomainText = await Page.TextContentAsync("body");
|
||||
Assert.That(defaultEmailDomainText, Does.Contain("example2.tld"), "Default email domain not visible on add credentials page.");
|
||||
|
||||
Reference in New Issue
Block a user