@@ -148,7 +138,7 @@ else
@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")
@entry.ExpireDate.ToString("yyyy-MM-dd HH:mm")
- RevokeRefreshToken(entry)" class="px-3 py-2 text-xs font-medium text-center text-white bg-red-700 rounded-lg hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">Revoke
+ Revoke
}
@@ -219,11 +209,11 @@ else
if (firstRender)
{
- await LoadEntryAsync();
+ await RefreshData();
}
}
- private async Task LoadEntryAsync()
+ private async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
@@ -294,7 +284,7 @@ else
{
DbContext.AliasVaultUserRefreshTokens.Remove(token);
await DbContext.SaveChangesAsync();
- await LoadEntryAsync();
+ await RefreshData();
}
}
@@ -310,7 +300,7 @@ else
{
User.TwoFactorEnabled = true;
await DbContext.SaveChangesAsync();
- await LoadEntryAsync();
+ await RefreshData();
}
}
@@ -327,7 +317,7 @@ else
{
User.TwoFactorEnabled = false;
await DbContext.SaveChangesAsync();
- await LoadEntryAsync();
+ await RefreshData();
}
}
@@ -348,7 +338,7 @@ else
.ForEachAsync(x => DbContext.UserTokens.Remove(x));
await DbContext.SaveChangesAsync();
- await LoadEntryAsync();
+ await RefreshData();
}
}
@@ -397,7 +387,7 @@ Do you want to proceed with the restoration?")) {
await DbContext.SaveChangesAsync();
// Reload the page.
- await LoadEntryAsync();
+ await RefreshData();
}
}
}
diff --git a/src/AliasVault.Admin/Main/_Imports.razor b/src/AliasVault.Admin/Main/_Imports.razor
index 50d9a4ba8..07ecb91bf 100644
--- a/src/AliasVault.Admin/Main/_Imports.razor
+++ b/src/AliasVault.Admin/Main/_Imports.razor
@@ -20,6 +20,8 @@
@using AliasVault.RazorComponents
@using AliasVault.RazorComponents.Alerts
@using AliasVault.RazorComponents.Buttons
+@using AliasVault.RazorComponents.Headings
+@using AliasVault.RazorComponents.Models
@using AliasVault.Admin.Main.Models
@using AliasVault.Admin.Main.Pages
@using AliasVault.Admin.Services
diff --git a/src/AliasVault.Admin/tailwind.config.js b/src/AliasVault.Admin/tailwind.config.js
index d02ab3413..0f275c0da 100644
--- a/src/AliasVault.Admin/tailwind.config.js
+++ b/src/AliasVault.Admin/tailwind.config.js
@@ -4,6 +4,7 @@ module.exports = {
'./**/*.html',
'./**/*.razor',
'../Shared/AliasVault.RazorComponents/**/*.razor',
+ '../Shared/AliasVault.RazorComponents/**/*.cs',
],
safelist: [
'w-64',
diff --git a/src/AliasVault.Admin/wwwroot/css/tailwind.css b/src/AliasVault.Admin/wwwroot/css/tailwind.css
index df534f23f..ea97227db 100644
--- a/src/AliasVault.Admin/wwwroot/css/tailwind.css
+++ b/src/AliasVault.Admin/wwwroot/css/tailwind.css
@@ -600,6 +600,10 @@ video {
border-width: 0;
}
+.visible {
+ visibility: visible;
+}
+
.invisible {
visibility: hidden;
}
@@ -675,10 +679,6 @@ video {
margin-bottom: 1rem;
}
-.mb-5 {
- margin-bottom: 1.25rem;
-}
-
.mb-6 {
margin-bottom: 1.5rem;
}
@@ -687,10 +687,6 @@ video {
margin-bottom: 2rem;
}
-.ml-1 {
- margin-left: 0.25rem;
-}
-
.ml-2 {
margin-left: 0.5rem;
}
@@ -751,6 +747,14 @@ video {
margin-top: 2rem;
}
+.ms-1 {
+ margin-inline-start: 0.25rem;
+}
+
+.ml-1 {
+ margin-left: 0.25rem;
+}
+
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
@@ -822,6 +826,10 @@ video {
height: 100%;
}
+.h-3 {
+ height: 0.75rem;
+}
+
.w-1\/2 {
width: 50%;
}
@@ -874,6 +882,10 @@ video {
width: 100%;
}
+.w-3 {
+ width: 0.75rem;
+}
+
.max-w-2xl {
max-width: 42rem;
}
@@ -952,10 +964,6 @@ video {
align-items: flex-start;
}
-.items-end {
- align-items: flex-end;
-}
-
.items-center {
align-items: center;
}
@@ -988,6 +996,12 @@ video {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
+.space-x-3 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(0.75rem * var(--tw-space-x-reverse));
+ margin-left: calc(0.75rem * 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));
@@ -1024,6 +1038,12 @@ video {
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
+.space-y-3 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
+}
+
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@@ -1542,6 +1562,11 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity));
}
+.text-gray-600 {
+ --tw-text-opacity: 1;
+ color: rgb(75 85 99 / var(--tw-text-opacity));
+}
+
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
@@ -1719,11 +1744,6 @@ video {
background-color: rgb(154 93 38 / var(--tw-bg-opacity));
}
-.hover\:bg-red-700:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(185 28 28 / var(--tw-bg-opacity));
-}
-
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
@@ -1780,11 +1800,6 @@ video {
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-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
@@ -1894,11 +1909,6 @@ video {
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
-.dark\:bg-red-500:is(.dark *) {
- --tw-bg-opacity: 1;
- background-color: rgb(239 68 68 / var(--tw-bg-opacity));
-}
-
.dark\:bg-red-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
@@ -2047,11 +2057,6 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
-.dark\:hover\:bg-red-600:hover:is(.dark *) {
- --tw-bg-opacity: 1;
- background-color: rgb(220 38 38 / var(--tw-bg-opacity));
-}
-
.dark\:hover\:bg-red-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
@@ -2122,11 +2127,6 @@ video {
--tw-ring-color: rgb(153 27 27 / var(--tw-ring-opacity));
}
-.dark\:focus\:ring-red-900:focus:is(.dark *) {
- --tw-ring-opacity: 1;
- --tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity));
-}
-
@media (min-width: 640px) {
.sm\:flex {
display: flex;
@@ -2194,6 +2194,10 @@ video {
margin-right: 1.5rem;
}
+ .md\:inline {
+ display: inline;
+ }
+
.md\:flex {
display: flex;
}
diff --git a/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor b/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor
index 83268dda4..0eaa59288 100644
--- a/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor
+++ b/src/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor
@@ -5,7 +5,7 @@
@implements IAsyncDisposable
- + New identity
+ + New Alias
@if (IsPopupVisible)
@@ -13,7 +13,7 @@
-
Create New Identity
+
Create New Alias
diff --git a/src/AliasVault.Client/Main/Layout/Footer.razor b/src/AliasVault.Client/Main/Layout/Footer.razor
index 2afbbeb5d..9dd9de3d1 100644
--- a/src/AliasVault.Client/Main/Layout/Footer.razor
+++ b/src/AliasVault.Client/Main/Layout/Footer.razor
@@ -23,7 +23,7 @@
@code {
private static readonly string[] Quotes =
[
- "Tip: Use the g+c (go create) keyboard shortcut to quickly create a new identity.",
+ "Tip: Use the g+c (go create) keyboard shortcut to quickly create a new alias.",
"Tip: Use the g+f (go find) keyboard shortcut to focus the search field.",
"Tip: Use the g+h (go home) keyboard shortcut to go to the homepage.",
"Tip: Use the g+l (go lock) keyboard shortcut to lock the vault.",
diff --git a/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor b/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor
index 262b1adc9..efcb2592c 100644
--- a/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor
+++ b/src/AliasVault.Client/Main/Pages/Credentials/AddEdit.razor
@@ -11,39 +11,15 @@
@inject IJSRuntime JSRuntime
@implements IAsyncDisposable
-@if (EditMode)
-{
-
Edit credentials
-}
-else {
-
Add credentials
-}
-
-
-
-
-
- @if (EditMode)
- {
-
Edit credentials
- }
- else {
-
Add credentials
- }
-
- Save Credentials
- Cancel
-
-
- @if (EditMode)
- {
-
Edit the existing credentials entry below.
- }
- else {
-
Create a new credentials entry below.
- }
-
-
+
+
+ Save Credentials
+ Cancel
+
+
@if (Loading)
{
@@ -96,7 +72,7 @@ else
Login credentials
- Generate Random Alias
+ Generate Random Alias
@@ -252,7 +228,7 @@ else
if (!EditMode)
{
- // When creating a new identity: start with focus on the service name input.
+ // When creating a new alias: start with focus on the service name input.
await JsInteropService.FocusElementById("service-name");
}
}
diff --git a/src/AliasVault.Client/Main/Pages/Credentials/Delete.razor b/src/AliasVault.Client/Main/Pages/Credentials/Delete.razor
index 6864ce3cf..3c737e5a5 100644
--- a/src/AliasVault.Client/Main/Pages/Credentials/Delete.razor
+++ b/src/AliasVault.Client/Main/Pages/Credentials/Delete.razor
@@ -4,15 +4,11 @@
Delete credentials entry
-
-
-
-
-
Delete credentials
-
-
You can delete a credentials entry below.
-
-
+
+
@if (IsLoading)
{
@@ -55,8 +51,8 @@ else
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
- BreadcrumbItems.Add(new BreadcrumbItem { Url = "credentials/" + Id, DisplayName = "View Credentials Entry" });
- BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete credentials" });
+ BreadcrumbItems.Add(new BreadcrumbItem { Url = "credentials/" + Id, DisplayName = "View credentials entry" });
+ BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete credential" });
}
///
diff --git a/src/AliasVault.Client/Main/Pages/Credentials/Home.razor b/src/AliasVault.Client/Main/Pages/Credentials/Home.razor
index 757a28f78..2023e6336 100644
--- a/src/AliasVault.Client/Main/Pages/Credentials/Home.razor
+++ b/src/AliasVault.Client/Main/Pages/Credentials/Home.razor
@@ -4,16 +4,14 @@
Home
-
-
-
-
-
Credentials
-
-
-
Find all of your credentials below.
-
-
+
+
+
+
+
@if (IsLoading)
{
diff --git a/src/AliasVault.Client/Main/Pages/Credentials/View.razor b/src/AliasVault.Client/Main/Pages/Credentials/View.razor
index 61712752c..f2aa12008 100644
--- a/src/AliasVault.Client/Main/Pages/Credentials/View.razor
+++ b/src/AliasVault.Client/Main/Pages/Credentials/View.razor
@@ -10,24 +10,24 @@
}
else
{
+
+
+
+
+
+
+
-
-
-
-
-
View credentials entry
-
-
-
-
-
@@ -48,7 +48,7 @@ else
}
- @if (Alias.Attachments != null && Alias.Attachments.Count > 0)
+ @if (Alias.Attachments.Count > 0)
{
}
diff --git a/src/AliasVault.Client/Main/Pages/Emails/Home.razor b/src/AliasVault.Client/Main/Pages/Emails/Home.razor
index 453d6c789..84819127f 100644
--- a/src/AliasVault.Client/Main/Pages/Emails/Home.razor
+++ b/src/AliasVault.Client/Main/Pages/Emails/Home.razor
@@ -17,16 +17,14 @@
}
-
-
-
-
-
Emails
-
-
-
Below you can find all recent emails sent to one of the email addresses used in your credentials.
-
-
+
+
+
+
+
@if (IsLoading)
{
@@ -36,7 +34,7 @@ else if (NoEmailClaims)
{
-
You are not using any private email addresses (yet). Create a new identity and use a private email address supported by AliasVault. All emails received by these private email addresses will show up here.
+
You are not using any private email addresses (yet). Create a new alias and use a private email address supported by AliasVault. All emails received by these private email addresses will show up here.
}
diff --git a/src/AliasVault.Client/Main/Pages/MainBase.cs b/src/AliasVault.Client/Main/Pages/MainBase.cs
index 4dcff94ab..b99dae4de 100644
--- a/src/AliasVault.Client/Main/Pages/MainBase.cs
+++ b/src/AliasVault.Client/Main/Pages/MainBase.cs
@@ -9,6 +9,7 @@ namespace AliasVault.Client.Main.Pages;
using AliasVault.Client.Services;
using AliasVault.Client.Services.Auth;
+using AliasVault.RazorComponents.Models;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
diff --git a/src/AliasVault.Client/Main/Pages/Settings/General.razor b/src/AliasVault.Client/Main/Pages/Settings/General.razor
index c359c0279..e651985f7 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/General.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/General.razor
@@ -3,13 +3,11 @@
General settings
-
-
-
-
General settings
-
Configure general AliasVault settings.
-
-
+
+
Email Settings
diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
index 21f5adc71..52b8b9e65 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
@@ -11,7 +11,7 @@
-
Change password
+
Change password
Changing your master password also changes the vault encryption keys. It is advised to periodically change your master password to keep your vaults secure.
diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Components/ActiveSessionsSection.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/ActiveSessionsSection.razor
index 0de9e5ded..8aa81b54f 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/Security/Components/ActiveSessionsSection.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/ActiveSessionsSection.razor
@@ -36,8 +36,7 @@
@session.CreatedAt.ToLocalTime().ToString("g")
@session.ExpireDate.ToLocalTime().ToString("g")
- RevokeSession(session.Id)"
- class="font-medium text-red-600 dark:text-red-500 hover:underline">Revoke (log out)
+ Revoke
}
diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Disable2Fa.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Disable2Fa.razor
index d8ed9bb3a..a65862c40 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/Security/Disable2Fa.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Disable2Fa.razor
@@ -13,7 +13,7 @@ else
-
Disable two-factor authentication
+
Disable two-factor authentication
Disabling two-factor authentication means you will be able to login with only your password.
diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Enable2Fa.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Enable2Fa.razor
index 5f3b4a35c..2c360051f 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/Security/Enable2Fa.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Enable2Fa.razor
@@ -8,7 +8,7 @@
-
Enable two-factor authentication
+
Enable two-factor authentication
Enable two-factor authentication to increase the security of your vaults.
diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor
index 2c1a72e3e..3ac386699 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Security.razor
@@ -4,16 +4,14 @@
Security settings
-
-
-
-
-
Security settings
-
-
-
Configure security settings.
-
-
+
+
+
+
+
diff --git a/src/AliasVault.Client/Main/Pages/Settings/Vault.razor b/src/AliasVault.Client/Main/Pages/Settings/Vault.razor
index 550a687eb..1e7c671d0 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/Vault.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/Vault.razor
@@ -5,28 +5,20 @@
Vault settings
-
-
-
-
-
Vault settings
-
-
On this page you can configure your vault settings.
-
-
+
+
Export vault
-
- Export vault to unencrypted SQLite file
-
+ Export vault to unencrypted SQLite file
-
- Export vault to unencrypted CSV file
-
+ Export vault to unencrypted CSV file
@@ -70,13 +62,13 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
{
try
{
- // Decode the base64 string to a byte array
+ // Decode the base64 string to a byte array.
byte[] fileBytes = Convert.FromBase64String(await DbService.ExportSqliteToBase64Async());
- // Create a memory stream from the byte array
+ // Create a memory stream from the byte array.
using (MemoryStream memoryStream = new MemoryStream(fileBytes))
{
- // Invoke JavaScript to initiate the download
+ // Invoke JavaScript to initiate the download.
await JsInteropService.DownloadFileFromStream("aliasvault-client.sqlite", memoryStream.ToArray());
}
}
@@ -94,10 +86,10 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
var csvBytes = CsvImportExport.CredentialCsvService.ExportCredentialsToCsv(credentials);
- // Create a memory stream from the byte array
+ // Create a memory stream from the byte array.
using (MemoryStream memoryStream = new MemoryStream(csvBytes))
{
- // Invoke JavaScript to initiate the download
+ // Invoke JavaScript to initiate the download.
await JsInteropService.DownloadFileFromStream("aliasvault-client.csv", memoryStream.ToArray());
}
}
@@ -127,7 +119,7 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
var fileContent = System.Text.Encoding.UTF8.GetString(buffer);
var importedCredentials = CsvImportExport.CredentialCsvService.ImportCredentialsFromCsv(fileContent);
- // Loop through the imported credentials and actually add them to the database
+ // Loop through the imported credentials and actually add them to the database.
foreach (var importedCredential in importedCredentials)
{
await CredentialService.InsertEntryAsync(importedCredential, false);
diff --git a/src/AliasVault.Client/Main/Pages/Test/Test1.razor b/src/AliasVault.Client/Main/Pages/Test/Test1.razor
index cd4408a0e..e82467dd0 100644
--- a/src/AliasVault.Client/Main/Pages/Test/Test1.razor
+++ b/src/AliasVault.Client/Main/Pages/Test/Test1.razor
@@ -4,15 +4,11 @@
Test webapi call 1
-
-
-
-
-
Test webapi call 1
-
-
Test webapi call 1.
-
-
+
+
@if (IsLoading)
{
diff --git a/src/AliasVault.Client/Main/Pages/Test/Test2.razor b/src/AliasVault.Client/Main/Pages/Test/Test2.razor
index d6dfac6ce..a633ec842 100644
--- a/src/AliasVault.Client/Main/Pages/Test/Test2.razor
+++ b/src/AliasVault.Client/Main/Pages/Test/Test2.razor
@@ -4,15 +4,11 @@
Test webapi call 2
-
-
-
-
-
Test webapi call 2
-
-
Test webapi call 2.
-
-
+
+
@if (IsLoading)
{
diff --git a/src/AliasVault.Client/Main/Pages/Welcome.razor b/src/AliasVault.Client/Main/Pages/Welcome.razor
index edd2000ea..05eee94e8 100644
--- a/src/AliasVault.Client/Main/Pages/Welcome.razor
+++ b/src/AliasVault.Client/Main/Pages/Welcome.razor
@@ -1,7 +1,7 @@
@page "/welcome"
@inherits MainBase
-
+
Welcome to AliasVault
It looks like you are new here. The instructions on this page will help to get you started.
@@ -11,7 +11,7 @@
How do I use AliasVault?
-
Create a random identity with an associated email address. To get started, simply click the "+ New Identity" button in the top right corner.
+
Create a random identity with an associated email address. To get started, simply click the "+ New Alias" button in the top right corner.
What is the purpose of AliasVault?
@@ -28,7 +28,7 @@
Get Started Now
- Go ahead and create a new login by clicking "+ New Identity" in the top right. Or explore these options:
+ Go ahead and create a new login by clicking "+ New Alias" in the top right. Or explore these options:
diff --git a/src/AliasVault.Client/_Imports.razor b/src/AliasVault.Client/_Imports.razor
index 6793fed06..c52d02bb5 100644
--- a/src/AliasVault.Client/_Imports.razor
+++ b/src/AliasVault.Client/_Imports.razor
@@ -27,6 +27,8 @@
@using AliasVault.RazorComponents
@using AliasVault.RazorComponents.Alerts
@using AliasVault.RazorComponents.Buttons
+@using AliasVault.RazorComponents.Headings
+@using AliasVault.RazorComponents.Models
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Blazored.LocalStorage
diff --git a/src/AliasVault.Client/tailwind.config.js b/src/AliasVault.Client/tailwind.config.js
index d02ab3413..4b67e5747 100644
--- a/src/AliasVault.Client/tailwind.config.js
+++ b/src/AliasVault.Client/tailwind.config.js
@@ -3,6 +3,7 @@ module.exports = {
content: [
'./**/*.html',
'./**/*.razor',
+ '../Shared/AliasVault.RazorComponents/**/*.cs',
'../Shared/AliasVault.RazorComponents/**/*.razor',
],
safelist: [
diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css
index b88d6885c..4b0fb961d 100644
--- a/src/AliasVault.Client/wwwroot/css/tailwind.css
+++ b/src/AliasVault.Client/wwwroot/css/tailwind.css
@@ -554,40 +554,6 @@ video {
--tw-contain-style: ;
}
-.container {
- width: 100%;
-}
-
-@media (min-width: 640px) {
- .container {
- max-width: 640px;
- }
-}
-
-@media (min-width: 768px) {
- .container {
- max-width: 768px;
- }
-}
-
-@media (min-width: 1024px) {
- .container {
- max-width: 1024px;
- }
-}
-
-@media (min-width: 1280px) {
- .container {
- max-width: 1280px;
- }
-}
-
-@media (min-width: 1536px) {
- .container {
- max-width: 1536px;
- }
-}
-
.sr-only {
position: absolute;
width: 1px;
@@ -600,6 +566,10 @@ video {
border-width: 0;
}
+.visible {
+ visibility: visible;
+}
+
.static {
position: static;
}
@@ -717,10 +687,6 @@ video {
margin-bottom: 1rem;
}
-.mb-5 {
- margin-bottom: 1.25rem;
-}
-
.mb-6 {
margin-bottom: 1.5rem;
}
@@ -797,6 +763,10 @@ video {
margin-top: 2rem;
}
+.ml-1 {
+ margin-left: 0.25rem;
+}
+
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
@@ -1072,6 +1042,12 @@ video {
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
+.space-x-3 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(0.75rem * var(--tw-space-x-reverse));
+ margin-left: calc(0.75rem * 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));
@@ -1517,10 +1493,6 @@ video {
padding-right: 0.75rem;
}
-.pt-10 {
- padding-top: 2.5rem;
-}
-
.pt-16 {
padding-top: 4rem;
}
@@ -1913,11 +1885,6 @@ video {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
-.hover\:bg-red-700:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(185 28 28 / var(--tw-bg-opacity));
-}
-
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
@@ -2119,11 +2086,6 @@ video {
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
-.dark\:bg-green-500:is(.dark *) {
- --tw-bg-opacity: 1;
- background-color: rgb(34 197 94 / var(--tw-bg-opacity));
-}
-
.dark\:bg-green-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
@@ -2139,11 +2101,6 @@ video {
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
}
-.dark\:bg-red-500:is(.dark *) {
- --tw-bg-opacity: 1;
- background-color: rgb(239 68 68 / var(--tw-bg-opacity));
-}
-
.dark\:bg-red-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
@@ -2267,11 +2224,6 @@ video {
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
-.dark\:hover\:bg-green-600:hover:is(.dark *) {
- --tw-bg-opacity: 1;
- background-color: rgb(22 163 74 / var(--tw-bg-opacity));
-}
-
.dark\:hover\:bg-green-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
@@ -2287,11 +2239,6 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
-.dark\:hover\:bg-red-600:hover:is(.dark *) {
- --tw-bg-opacity: 1;
- background-color: rgb(220 38 38 / var(--tw-bg-opacity));
-}
-
.dark\:hover\:bg-red-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
@@ -2569,11 +2516,6 @@ video {
.lg\:gap-4 {
gap: 1rem;
}
-
- .lg\:px-0 {
- padding-left: 0px;
- padding-right: 0px;
- }
}
@media (min-width: 1280px) {
diff --git a/src/Databases/AliasServerDb/AliasServerDbContext.cs b/src/Databases/AliasServerDb/AliasServerDbContext.cs
index 31e71426b..9309f30ab 100644
--- a/src/Databases/AliasServerDb/AliasServerDbContext.cs
+++ b/src/Databases/AliasServerDb/AliasServerDbContext.cs
@@ -133,17 +133,20 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
- foreach (var entity in modelBuilder.Model.GetEntityTypes())
- {
- foreach (var property in entity.GetProperties())
- {
- // NOTE: This is a workaround for SQLite. Add conditional check if SQLite is used.
- // NOTE: SQL server doesn't need this override.
- // SQLite does not support varchar(max) so we use TEXT.
- if (property.ClrType == typeof(string) && property.GetMaxLength() == null)
+ // NOTE: This is a workaround for SQLite. Add conditional check if SQLite is used.
+ // NOTE: SQL server doesn't need this override.
+ if (Database.IsSqlite())
+ {
+ foreach (var entity in modelBuilder.Model.GetEntityTypes())
+ {
+ foreach (var property in entity.GetProperties())
{
- property.SetColumnType("TEXT");
+ // SQLite does not support varchar(max) so we use TEXT.
+ if (property.ClrType == typeof(string) && property.GetMaxLength() == null)
+ {
+ property.SetColumnType("TEXT");
+ }
}
}
}
diff --git a/src/Databases/AliasServerDb/Log.cs b/src/Databases/AliasServerDb/Log.cs
index 6e0370660..90a22146b 100644
--- a/src/Databases/AliasServerDb/Log.cs
+++ b/src/Databases/AliasServerDb/Log.cs
@@ -54,7 +54,7 @@ public class Log
///
/// Gets or sets the timestamp of the log entry.
///
- public DateTimeOffset TimeStamp { get; set; }
+ public DateTime TimeStamp { get; set; }
///
/// Gets or sets the exception associated with the log entry.
diff --git a/src/Databases/AliasServerDb/Migrations/20241007153012_DatetimeOffsetToDateTime.Designer.cs b/src/Databases/AliasServerDb/Migrations/20241007153012_DatetimeOffsetToDateTime.Designer.cs
new file mode 100644
index 000000000..2467b8299
--- /dev/null
+++ b/src/Databases/AliasServerDb/Migrations/20241007153012_DatetimeOffsetToDateTime.Designer.cs
@@ -0,0 +1,831 @@
+//
+using System;
+using AliasServerDb;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace AliasServerDb.Migrations
+{
+ [DbContext(typeof(AliasServerDbContext))]
+ [Migration("20241007153012_DatetimeOffsetToDateTime")]
+ partial class DatetimeOffsetToDateTime
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Proxies:ChangeTracking", false)
+ .HasAnnotation("Proxies:CheckEquality", false)
+ .HasAnnotation("Proxies:LazyLoading", true);
+
+ modelBuilder.Entity("AliasServerDb.AdminRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AdminRoles");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AdminUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastPasswordChanged")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AdminUsers");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AliasVaultRoles");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordChangedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AliasVaultUsers");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceIdentifier")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("ExpireDate")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("IpAddress")
+ .HasMaxLength(45)
+ .HasColumnType("TEXT");
+
+ b.Property("PreviousTokenValue")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AliasVaultUserRefreshTokens");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AuthLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AdditionalInfo")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Browser")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Country")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceType")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("EventType")
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("FailureReason")
+ .HasColumnType("INTEGER");
+
+ b.Property("IpAddress")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsSuccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSuspiciousActivity")
+ .HasColumnType("INTEGER");
+
+ b.Property("OperatingSystem")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("RequestPath")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Timestamp")
+ .HasColumnType("TEXT");
+
+ b.Property("UserAgent")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex(new[] { "EventType" }, "IX_EventType");
+
+ b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
+
+ b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
+
+ b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
+ .IsDescending(false, false, true);
+
+ b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
+ .IsDescending(false, true);
+
+ b.ToTable("AuthLogs");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Email", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("DateSystem")
+ .HasColumnType("TEXT");
+
+ b.Property("EncryptedSymmetricKey")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("From")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("FromDomain")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("FromLocal")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("MessageHtml")
+ .HasColumnType("TEXT");
+
+ b.Property("MessagePlain")
+ .HasColumnType("TEXT");
+
+ b.Property("MessagePreview")
+ .HasColumnType("TEXT");
+
+ b.Property("MessageSource")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("PushNotificationSent")
+ .HasColumnType("INTEGER");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("To")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ToDomain")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("ToLocal")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserEncryptionKeyId")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Visible")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("DateSystem");
+
+ b.HasIndex("PushNotificationSent");
+
+ b.HasIndex("ToLocal");
+
+ b.HasIndex("UserEncryptionKeyId");
+
+ b.HasIndex("Visible");
+
+ b.ToTable("Emails");
+ });
+
+ modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Bytes")
+ .IsRequired()
+ .HasColumnType("BLOB");
+
+ b.Property("Date")
+ .HasColumnType("TEXT");
+
+ b.Property("EmailId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Filename")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Filesize")
+ .HasColumnType("INTEGER");
+
+ b.Property("MimeType")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EmailId");
+
+ b.ToTable("EmailAttachments");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Log", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Application")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Exception")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Level")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("TEXT");
+
+ b.Property("LogEvent")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("LogEvent");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("MessageTemplate")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Properties")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("SourceContext")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("TimeStamp")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Application");
+
+ b.HasIndex("TimeStamp");
+
+ b.ToTable("Logs", (string)null);
+ });
+
+ modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AddressDomain")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AddressLocal")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Address")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserEmailClaims");
+ });
+
+ modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("IsPrimary")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublicKey")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserEncryptionKeys");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Vault", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CredentialsCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmailClaimsCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("EncryptionSettings")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("EncryptionType")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("FileSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("RevisionNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("Salt")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("VaultBlob")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Verifier")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("Version")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Vaults");
+ });
+
+ modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CurrentStatus")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("DesiredStatus")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Heartbeat")
+ .HasColumnType("TEXT");
+
+ b.Property("ServiceName")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("varchar");
+
+ b.HasKey("Id");
+
+ b.ToTable("WorkerServiceStatuses");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("FriendlyName")
+ .HasColumnType("TEXT");
+
+ b.Property("Xml")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataProtectionKeys");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("RoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("UserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.ToTable("UserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.ToTable("UserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("UserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
+ {
+ b.HasOne("AliasServerDb.AliasVaultUser", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Email", b =>
+ {
+ b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
+ .WithMany("Emails")
+ .HasForeignKey("UserEncryptionKeyId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("EncryptionKey");
+ });
+
+ modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
+ {
+ b.HasOne("AliasServerDb.Email", "Email")
+ .WithMany("Attachments")
+ .HasForeignKey("EmailId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Email");
+ });
+
+ modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
+ {
+ b.HasOne("AliasServerDb.AliasVaultUser", "User")
+ .WithMany("EmailClaims")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
+ {
+ b.HasOne("AliasServerDb.AliasVaultUser", "User")
+ .WithMany("EncryptionKeys")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Vault", b =>
+ {
+ b.HasOne("AliasServerDb.AliasVaultUser", "User")
+ .WithMany("Vaults")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
+ {
+ b.Navigation("EmailClaims");
+
+ b.Navigation("EncryptionKeys");
+
+ b.Navigation("Vaults");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Email", b =>
+ {
+ b.Navigation("Attachments");
+ });
+
+ modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
+ {
+ b.Navigation("Emails");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Databases/AliasServerDb/Migrations/20241007153012_DatetimeOffsetToDateTime.cs b/src/Databases/AliasServerDb/Migrations/20241007153012_DatetimeOffsetToDateTime.cs
new file mode 100644
index 000000000..c835ed734
--- /dev/null
+++ b/src/Databases/AliasServerDb/Migrations/20241007153012_DatetimeOffsetToDateTime.cs
@@ -0,0 +1,23 @@
+//
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace AliasServerDb.Migrations
+{
+ ///
+ public partial class DatetimeOffsetToDateTime : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+
+ }
+ }
+}
diff --git a/src/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs b/src/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs
index 1a09bd173..a31749918 100644
--- a/src/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs
+++ b/src/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs
@@ -452,7 +452,7 @@ namespace AliasServerDb.Migrations
.IsRequired()
.HasColumnType("TEXT");
- b.Property("TimeStamp")
+ b.Property("TimeStamp")
.HasColumnType("TEXT");
b.HasKey("Id");
diff --git a/src/Shared/AliasVault.RazorComponents/AliasVault.RazorComponents.csproj b/src/Shared/AliasVault.RazorComponents/AliasVault.RazorComponents.csproj
index ae089f7b6..0d2a96e6b 100644
--- a/src/Shared/AliasVault.RazorComponents/AliasVault.RazorComponents.csproj
+++ b/src/Shared/AliasVault.RazorComponents/AliasVault.RazorComponents.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/Button.razor b/src/Shared/AliasVault.RazorComponents/Buttons/Button.razor
index a89ebbdbc..d49a123f4 100644
--- a/src/Shared/AliasVault.RazorComponents/Buttons/Button.razor
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/Button.razor
@@ -1,4 +1,5 @@
-
@ChildContent
@@ -23,6 +24,12 @@
[Parameter]
public bool IsDisabled { get; set; }
+ ///
+ /// Specifies the type of the button. Default is "button".
+ ///
+ [Parameter]
+ public string Type { get; set; } = "button";
+
///
/// The color theme of the button.
///
@@ -35,6 +42,12 @@
[Parameter]
public string AdditionalClasses { get; set; } = "";
+ ///
+ /// The display class of the button. Defaults to inline.
+ ///
+ [Parameter]
+ public string Display { get; set; } = "inline";
+
///
/// Handles the button click event.
///
@@ -52,17 +65,9 @@
/// A string containing the CSS classes for the button.
private string GetButtonClasses()
{
- var baseClasses = "flex center items-center px-3 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-4";
- var colorClasses = Color switch
- {
- "primary" => "bg-primary-700 hover:bg-primary-800 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
- "danger" => "bg-red-700 hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800",
- "success" => "bg-green-700 hover:bg-green-800 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800",
- "secondary" => "bg-secondary-700 hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800",
- _ => "bg-gray-700 hover:bg-gray-800 focus:ring-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
- };
- var disabledClasses = IsDisabled ? "bg-gray-400 cursor-not-allowed" : "";
+ var colorClasses = ButtonStyles.GetColorClasses(Color);
+ var disabledClasses = IsDisabled ? ButtonStyles.DisabledClasses : "";
- return $"{baseClasses} {colorClasses} {disabledClasses} {AdditionalClasses}".Trim();
+ return $"{Display} {ButtonStyles.BaseClasses} {colorClasses} {disabledClasses} {AdditionalClasses}".Trim();
}
}
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/ButtonStyles.cs b/src/Shared/AliasVault.RazorComponents/Buttons/ButtonStyles.cs
new file mode 100644
index 000000000..2eb231f54
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/ButtonStyles.cs
@@ -0,0 +1,38 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.RazorComponents.Buttons;
+
+///
+/// A static class that provides CSS classes for buttons.
+///
+public static class ButtonStyles
+{
+ ///
+ /// Gets the base CSS classes for buttons.
+ ///
+ public static string BaseClasses => "center items-center px-3 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-4";
+
+ ///
+ /// Gets the CSS classes for a disabled button.
+ ///
+ public static string DisabledClasses => "bg-gray-400 cursor-not-allowed";
+
+ ///
+ /// Gets the color-specific CSS classes for a button based on the provided color.
+ ///
+ /// The color name for the button (e.g., "primary", "danger", "success", "secondary").
+ /// A string containing the appropriate CSS classes for the specified color.
+ public static string GetColorClasses(string color) => color switch
+ {
+ "primary" => "bg-primary-700 hover:bg-primary-800 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
+ "secondary" => "bg-gray-700 hover:bg-gray-800 focus:ring-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800",
+ "danger" => "bg-red-700 hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800",
+ "success" => "bg-green-700 hover:bg-green-800 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800",
+ _ => "bg-gray-700 hover:bg-gray-800 focus:ring-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800",
+ };
+}
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/CancelButton.razor b/src/Shared/AliasVault.RazorComponents/Buttons/CancelButton.razor
new file mode 100644
index 000000000..5e222bdfe
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/CancelButton.razor
@@ -0,0 +1,26 @@
+
+ @ChildContent
+
+
+@code {
+ ///
+ /// The event to call in the parent when the cancel button is clicked.
+ ///
+ [Parameter]
+ public EventCallback OnClick { get; set; }
+
+ ///
+ /// The content to be displayed inside the button.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ ///
+ /// Handles the button click event.
+ ///
+ private async Task HandleClick()
+ {
+ await OnClick.InvokeAsync();
+ }
+}
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/ConfirmButton.razor b/src/Shared/AliasVault.RazorComponents/Buttons/ConfirmButton.razor
new file mode 100644
index 000000000..273d11c06
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/ConfirmButton.razor
@@ -0,0 +1,26 @@
+
+ @ChildContent
+
+
+@code {
+ ///
+ /// The event to call in the parent when the confirm button is clicked.
+ ///
+ [Parameter]
+ public EventCallback OnClick { get; set; }
+
+ ///
+ /// The content to be displayed inside the button.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ ///
+ /// Handles the button click event.
+ ///
+ private async Task HandleClick()
+ {
+ await OnClick.InvokeAsync();
+ }
+}
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/DeleteButton.razor b/src/Shared/AliasVault.RazorComponents/Buttons/DeleteButton.razor
index 0452707e2..b6d115637 100644
--- a/src/Shared/AliasVault.RazorComponents/Buttons/DeleteButton.razor
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/DeleteButton.razor
@@ -1,4 +1,5 @@
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/LinkButton.razor b/src/Shared/AliasVault.RazorComponents/Buttons/LinkButton.razor
new file mode 100644
index 000000000..268603211
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/LinkButton.razor
@@ -0,0 +1,46 @@
+
+ @Text @if (AdditionalText.Length > 0) { @AdditionalText }
+
+
+@code {
+ ///
+ /// Gets or sets the URL that the hyperlink points to.
+ ///
+ [Parameter]
+ public string Href { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the main text of the button.
+ ///
+ [Parameter]
+ public string Text { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the additional text that appears on larger screens.
+ ///
+ [Parameter]
+ public string AdditionalText { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the color theme of the button.
+ ///
+ [Parameter]
+ public string Color { get; set; } = "primary";
+
+ ///
+ /// Gets or sets additional CSS classes to apply to the button.
+ ///
+ [Parameter]
+ public string AdditionalClasses { get; set; } = string.Empty;
+
+ ///
+ /// Gets the CSS classes for the link button based on the color and additional classes.
+ ///
+ /// A string containing the CSS classes for the link button.
+ private string GetButtonClasses()
+ {
+ var colorClasses = ButtonStyles.GetColorClasses(Color);
+
+ return $"{ButtonStyles.BaseClasses} {colorClasses} {AdditionalClasses}".Trim();
+ }
+}
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/RefreshButton.razor b/src/Shared/AliasVault.RazorComponents/Buttons/RefreshButton.razor
index c2280ad82..1e9485c0e 100644
--- a/src/Shared/AliasVault.RazorComponents/Buttons/RefreshButton.razor
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/RefreshButton.razor
@@ -1,9 +1,10 @@
@using System.Timers
+ IsDisabled="@IsRefreshing"
+ Display="flex"
+ Color="@Color"
+ AdditionalClasses="@AdditionalClasses">
diff --git a/src/Shared/AliasVault.RazorComponents/Buttons/SubmitButton.razor b/src/Shared/AliasVault.RazorComponents/Buttons/SubmitButton.razor
new file mode 100644
index 000000000..e4d0f04b4
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Buttons/SubmitButton.razor
@@ -0,0 +1,13 @@
+
+ @ChildContent
+
+
+@code {
+ ///
+ /// The content to be displayed inside the button.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+}
diff --git a/src/AliasVault.Client/Main/Components/Layout/Breadcrumb.razor b/src/Shared/AliasVault.RazorComponents/Headings/Breadcrumb.razor
similarity index 97%
rename from src/AliasVault.Client/Main/Components/Layout/Breadcrumb.razor
rename to src/Shared/AliasVault.RazorComponents/Headings/Breadcrumb.razor
index e40d4516d..ef3a4b6a5 100644
--- a/src/AliasVault.Client/Main/Components/Layout/Breadcrumb.razor
+++ b/src/Shared/AliasVault.RazorComponents/Headings/Breadcrumb.razor
@@ -1,6 +1,4 @@
-@inherits ComponentBase
-
-
+
diff --git a/src/Shared/AliasVault.RazorComponents/Headings/H1.razor b/src/Shared/AliasVault.RazorComponents/Headings/H1.razor
new file mode 100644
index 000000000..f193275b1
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Headings/H1.razor
@@ -0,0 +1,9 @@
+@ChildContent
+
+@code {
+ ///
+ /// The content to be displayed inside the heading.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+}
diff --git a/src/Shared/AliasVault.RazorComponents/Headings/PageHeader.razor b/src/Shared/AliasVault.RazorComponents/Headings/PageHeader.razor
new file mode 100644
index 000000000..2d09cf1f4
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Headings/PageHeader.razor
@@ -0,0 +1,41 @@
+
+
+
+
+
@Title
+ @if (CustomActions != null)
+ {
+
+ @CustomActions
+
+ }
+
+
@Description
+
+
+
+@code {
+ ///
+ /// Gets or sets the breadcrumb items for the header.
+ ///
+ [Parameter]
+ public List BreadcrumbItems { get; set; } = new List();
+
+ ///
+ /// Gets or sets the title of the header.
+ ///
+ [Parameter]
+ public string Title { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the description text below the title.
+ ///
+ [Parameter]
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// The caller can provide a custom action button section to be displayed on the right side of the header.
+ ///
+ [Parameter]
+ public RenderFragment? CustomActions { get; set; }
+}
diff --git a/src/AliasVault.Client/Main/Models/BreadcrumbItem.cs b/src/Shared/AliasVault.RazorComponents/Models/BreadcrumbItem.cs
similarity index 94%
rename from src/AliasVault.Client/Main/Models/BreadcrumbItem.cs
rename to src/Shared/AliasVault.RazorComponents/Models/BreadcrumbItem.cs
index 6d2c55f1b..c3e5a085b 100644
--- a/src/AliasVault.Client/Main/Models/BreadcrumbItem.cs
+++ b/src/Shared/AliasVault.RazorComponents/Models/BreadcrumbItem.cs
@@ -5,7 +5,7 @@
//
//-----------------------------------------------------------------------
-namespace AliasVault.Client.Main.Models;
+namespace AliasVault.RazorComponents.Models;
///
/// Represents a breadcrumb item for the breadcrumb component.
diff --git a/src/AliasVault.Admin/Main/Models/BreadcrumbItem.cs b/src/Shared/AliasVault.RazorComponents/Tables/SortDirection.cs
similarity index 58%
rename from src/AliasVault.Admin/Main/Models/BreadcrumbItem.cs
rename to src/Shared/AliasVault.RazorComponents/Tables/SortDirection.cs
index 2e18b9a56..7b258556e 100644
--- a/src/AliasVault.Admin/Main/Models/BreadcrumbItem.cs
+++ b/src/Shared/AliasVault.RazorComponents/Tables/SortDirection.cs
@@ -1,24 +1,24 @@
//-----------------------------------------------------------------------
-//
+//
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
//
//-----------------------------------------------------------------------
-namespace AliasVault.Admin.Main.Models;
+namespace AliasVault.RazorComponents.Tables;
///
-/// Breadcrumb item model.
+/// An enum that represents the direction of a sort.
///
-public class BreadcrumbItem
+public enum SortDirection
{
///
- /// Gets or sets the display name.
+ /// The sort is ascending.
///
- public string? DisplayName { get; set; }
+ Ascending,
///
- /// Gets or sets the URL.
+ /// The sort is descending.
///
- public string? Url { get; set; }
+ Descending,
}
diff --git a/src/Shared/AliasVault.RazorComponents/Tables/SortableTable.razor b/src/Shared/AliasVault.RazorComponents/Tables/SortableTable.razor
new file mode 100644
index 000000000..d46a49dd7
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Tables/SortableTable.razor
@@ -0,0 +1,85 @@
+
+
+
+ @foreach (var column in Columns)
+ {
+
+ @if (column.Sortable)
+ {
+ OnSort(column.PropertyName)">
+ @column.Title
+
+ @if (SortColumn == column.PropertyName)
+ {
+ @if (SortDirection == SortDirection.Ascending)
+ {
+
+
+
+ }
+ else
+ {
+
+
+
+ }
+ }
+
+
+ }
+ else
+ {
+ @column.Title
+ }
+
+ }
+
+
+
+ @ChildContent
+
+
+
+@code {
+ ///
+ /// Gets or sets the columns to display in the table.
+ ///
+ [Parameter] public List Columns { get; set; } = new();
+
+ ///
+ /// Gets or sets the child content of the table.
+ ///
+ [Parameter] public RenderFragment ChildContent { get; set; } = null!;
+
+ ///
+ /// Gets or sets the column to sort by.
+ ///
+ [Parameter] public string SortColumn { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the direction to sort by.
+ ///
+ [Parameter] public SortDirection SortDirection { get; set; }
+
+ ///
+ /// Gets or sets the event to invoke when the sort changes.
+ ///
+ [Parameter] public EventCallback<(string, SortDirection)> OnSortChanged { get; set; }
+
+ private async Task OnSort(string columnName)
+ {
+ if (SortColumn == columnName)
+ {
+ SortDirection = SortDirection == SortDirection.Ascending
+ ? SortDirection.Descending
+ : SortDirection.Ascending;
+ }
+ else
+ {
+ SortColumn = columnName;
+ SortDirection = SortDirection.Ascending;
+ }
+
+ await OnSortChanged.InvokeAsync((SortColumn, SortDirection));
+ }
+}
diff --git a/src/Shared/AliasVault.RazorComponents/Tables/TableColumn.cs b/src/Shared/AliasVault.RazorComponents/Tables/TableColumn.cs
new file mode 100644
index 000000000..0ba5a6793
--- /dev/null
+++ b/src/Shared/AliasVault.RazorComponents/Tables/TableColumn.cs
@@ -0,0 +1,29 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.RazorComponents.Tables;
+
+///
+/// A class that represents a column in a table.
+///
+public class TableColumn
+{
+ ///
+ /// Gets or sets the title of the column.
+ ///
+ public required string Title { get; set; }
+
+ ///
+ /// Gets or sets the name of the property to bind to.
+ ///
+ public required string PropertyName { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the column is sortable.
+ ///
+ public bool Sortable { get; set; } = true;
+}
diff --git a/src/Shared/AliasVault.RazorComponents/_Imports.razor b/src/Shared/AliasVault.RazorComponents/_Imports.razor
index 77285129d..d6862780f 100644
--- a/src/Shared/AliasVault.RazorComponents/_Imports.razor
+++ b/src/Shared/AliasVault.RazorComponents/_Imports.razor
@@ -1 +1,3 @@
-@using Microsoft.AspNetCore.Components.Web
+@using AliasVault.RazorComponents.Models
+@using AliasVault.RazorComponents.Tables
+@using Microsoft.AspNetCore.Components.Web
diff --git a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs
index f00219079..986394ab8 100644
--- a/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs
+++ b/src/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs
@@ -253,8 +253,8 @@ public class ClientPlaywrightTest : PlaywrightTest
await Page.ClickAsync("text=" + credentialName);
// Wait for the credential details page to load.
- await WaitForUrlAsync("credentials/**", "Delete credentials entry");
- await Page.ClickAsync("text=Delete credentials entry");
+ await WaitForUrlAsync("credentials/**", "Delete");
+ await Page.ClickAsync("text=Delete");
// Wait for the delete credential page to load.
await WaitForUrlAsync("credentials/**/delete", "You can delete a credentials entry below");
diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs
index 0c16a2122..dd5ea367a 100644
--- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs
+++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/CredentialTests.cs
@@ -100,7 +100,7 @@ public class CredentialTests : ClientPlaywrightTest
Assert.That(pageContent, Does.Contain(serviceNameBefore), "Created credential service name does not appear on login page.");
// Click the edit button.
- var editButton = Page.Locator("text=Edit credentials entry").First;
+ var editButton = Page.Locator("text=Edit").First;
await editButton.ClickAsync();
await WaitForUrlAsync("edit", "Save Credentials");
@@ -113,7 +113,7 @@ public class CredentialTests : ClientPlaywrightTest
var submitButton = Page.Locator("text=Save Credentials").First;
await submitButton.ClickAsync();
- await WaitForUrlAsync("credentials/**", "Delete credentials entry");
+ await WaitForUrlAsync("credentials/**", "Delete");
pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Credentials updated"), "Credential update confirmation message not shown.");
diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/JwtTokenTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/JwtTokenTests.cs
index 381205530..48db7736f 100644
--- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/JwtTokenTests.cs
+++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard1/JwtTokenTests.cs
@@ -36,7 +36,7 @@ public class JwtTokenTests : ClientPlaywrightTest
await WaitForUrlAsync("test/2", "Test 2 OK");
var pageContent = await Page.TextContentAsync("body");
- Assert.That(pageContent, Does.Contain("Test webapi call 2."), "No page content after refreshing access token.");
+ Assert.That(pageContent, Does.Contain("Test webapi call 2"), "No page content after refreshing access token.");
}
///
@@ -74,7 +74,7 @@ public class JwtTokenTests : ClientPlaywrightTest
await WaitForUrlAsync(startUrl, "Test 1 OK");
var pageContent = await Page.TextContentAsync("body");
- Assert.That(pageContent, Does.Contain("Test webapi call 1."), "No index content after unlocking database with a expired JWT token.");
+ Assert.That(pageContent, Does.Contain("Test webapi call 1"), "No index content after unlocking database with a expired JWT token.");
}
///
diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs
index e8c300368..dedad1ec7 100644
--- a/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs
+++ b/src/Tests/AliasVault.E2ETests/Tests/Client/Shard5/UnlockTests.cs
@@ -30,10 +30,10 @@ public class UnlockTests : ClientPlaywrightTest
await RefreshPageAndUnlockVault();
// Check if we get redirected back to the page we were trying to access.
- await WaitForUrlAsync(startUrl, "Test webapi call 1.");
+ await WaitForUrlAsync(startUrl, "Test webapi call 1");
var pageContent = await Page.TextContentAsync("body");
- Assert.That(pageContent, Does.Contain("Test webapi call 1."), "No index content after unlocking database.");
+ Assert.That(pageContent, Does.Contain("Test 1 OK."), "No index content after unlocking database.");
}
///
@@ -69,9 +69,9 @@ public class UnlockTests : ClientPlaywrightTest
await submitButton.ClickAsync();
// Check if we get redirected back to the page we were trying to access.
- await WaitForUrlAsync(startUrl, "Test webapi call 1.");
+ await WaitForUrlAsync(startUrl, "Test webapi call 1");
var pageContent = await Page.TextContentAsync("body");
- Assert.That(pageContent, Does.Contain("Test webapi call 1."), "No index content after unlocking database.");
+ Assert.That(pageContent, Does.Contain("Test 1 OK."), "No index content after unlocking database.");
}
}