Merge pull request #440 from lanedirt/349-add-statistics-to-admin

Add statistics to admin
This commit is contained in:
Leendert de Borst
2024-12-04 10:53:23 +01:00
committed by GitHub
19 changed files with 460 additions and 117 deletions

View File

@@ -52,7 +52,7 @@
</div>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
<li>
<a href="account/manage" 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">Account settings</a>
<a href="account/manage/change-password" 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">Account settings</a>
</li>
</ul>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">

View File

@@ -0,0 +1,49 @@
//-----------------------------------------------------------------------
// <copyright file="UserEmailClaimWithCount.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.Admin.Main.Models;
/// <summary>
/// User email claim view model with count.
/// </summary>
public class UserEmailClaimWithCount
{
/// <summary>
/// Gets or sets the id.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the address.
/// </summary>
public string Address { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the address local.
/// </summary>
public string AddressLocal { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the address domain.
/// </summary>
public string AddressDomain { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the created at timestamp.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the updated at timestamp.
/// </summary>
public DateTime UpdatedAt { get; set; }
/// <summary>
/// Gets or sets the email count.
/// </summary>
public int EmailCount { get; set; }
}

View File

@@ -8,7 +8,7 @@
<LayoutPageTitle>Change password</LayoutPageTitle>
<div class="max-w-2xl mx-auto">
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Change password</h3>
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
<DataAnnotationsValidator/>

View File

@@ -1,67 +0,0 @@
@page "/account/manage"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@inject UserManager<AdminUser> UserManager
<LayoutPageTitle>Profile</LayoutPageTitle>
<div class="max-w-2xl mx-auto">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Profile</h3>
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" class="space-y-6">
<DataAnnotationsValidator/>
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
<div>
<label for="username" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Username</label>
<input type="text" value="@username" id="username" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-gray-100 cursor-not-allowed dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400" placeholder="Please choose your username." disabled/>
</div>
<div>
<label for="phone-number" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Phone number</label>
<InputText @bind-Value="Input.PhoneNumber" id="phone-number" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="Please enter your phone number."/>
<ValidationMessage For="() => Input.PhoneNumber" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
</div>
<div>
<SubmitButton>Save</SubmitButton>
</div>
</EditForm>
</div>
@code {
private string? username;
private string? phoneNumber;
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
username = await UserManager.GetUserNameAsync(UserService.User());
phoneNumber = await UserManager.GetPhoneNumberAsync(UserService.User());
Input.PhoneNumber ??= phoneNumber;
}
private async Task OnValidSubmitAsync()
{
if (Input.PhoneNumber != phoneNumber)
{
var setPhoneResult = await UserManager.SetPhoneNumberAsync(UserService.User(), Input.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
GlobalNotificationService.AddErrorMessage("Phone number could not be set", true);
}
}
GlobalNotificationService.AddSuccessMessage("Your profile has been updated", true);
}
private sealed class InputModel
{
[Phone]
[Display(Name = "Phone number")]
public string? PhoneNumber { get; set; }
}
}

View File

@@ -1,15 +1,13 @@
@page "/account/manage/2fa"
@using Microsoft.AspNetCore.Identity
@inject UserManager<AdminUser> UserManager
@inject SignInManager<AdminUser> SignInManager
<LayoutPageTitle>Two-factor authentication (2FA)</LayoutPageTitle>
@if (is2FaEnabled)
{
<div class="mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Two-factor authentication (2FA)</h3>
@if (recoveryCodesLeft == 0)
@@ -41,7 +39,7 @@
</div>
}
<div class="mt-6 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg">
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Authenticator app</h4>
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
@if (!hasAuthenticator)

View File

@@ -5,11 +5,10 @@
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="Manage account"
Description="Manage your profile here.">
Description="Manage security settings for the admin account here.">
</PageHeader>
<div class="container mx-auto px-4 py-8">
<hr class="mb-6 border-t border-gray-300"/>
<div class="mx-auto px-4 py-8">
<div class="flex flex-col md:flex-row">
<div class="w-full md:w-1/4 mb-6 md:mb-0">
<ManageNavMenu/>

View File

@@ -4,12 +4,9 @@
<ul class="flex flex-col space-y-1">
<li>
<NavLink href="account/manage" Match="NavLinkMatch.All" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Profile</NavLink>
<NavLink href="account/manage/change-password" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">Password</NavLink>
</li>
<li>
<NavLink href="account/manage/change-password" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Password</NavLink>
</li>
<li>
<NavLink href="account/manage/2fa" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Two-factor authentication</NavLink>
<NavLink href="account/manage/2fa" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">Two-factor authentication</NavLink>
</li>
</ul>

View File

@@ -0,0 +1,137 @@
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Active users</h3>
<button
@onclick="ToggleUserNames"
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
@(ShowUserNames ? "Hide names" : "Show names")
</button>
</div>
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 24 hours</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last24Hours</h4>
@if (ShowUserNames)
{
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
<ul>
@foreach (var user in UserStats.Last24HourUsers)
{
<li>@user</li>
}
</ul>
</div>
}
</div>
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 7 days</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last7Days</h4>
@if (ShowUserNames)
{
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
<ul>
@foreach (var user in UserStats.Last7DayUsers)
{
<li>@user</li>
}
</ul>
</div>
}
</div>
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last14Days</h4>
@if (ShowUserNames)
{
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
<ul>
@foreach (var user in UserStats.Last14DayUsers)
{
<li>@user</li>
}
</ul>
</div>
}
</div>
</div>
}
</div>
@code {
private bool IsLoading { get; set; } = true;
private UserStatistics UserStats { get; set; } = new();
private bool ShowUserNames { get; set; }
/// <summary>
/// Refreshes the data displayed on the card.
/// </summary>
public async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
var now = DateTime.UtcNow;
var last24Hours = now.AddHours(-24);
var last7Days = now.AddDays(-7);
var last14Days = now.AddDays(-14);
// Get user statistics
var (count24h, users24h) = await GetActiveUserCount(last24Hours);
var (count7d, users7d) = await GetActiveUserCount(last7Days);
var (count14d, users14d) = await GetActiveUserCount(last14Days);
UserStats = new UserStatistics
{
Last24Hours = count24h,
Last7Days = count7d,
Last14Days = count14d,
Last24HourUsers = users24h,
Last7DayUsers = users7d,
Last14DayUsers = users14d
};
IsLoading = false;
StateHasChanged();
}
private async Task<(int count, List<string> users)> GetActiveUserCount(DateTime since)
{
// Get unique users who either:
// 1. Have successful auth logs
// 2. Have updated their vault
var activeUsers = await DbContext.AuthLogs
.Where(l => l.Timestamp >= since && l.IsSuccess)
.Select(l => l.Username)
.Union(
DbContext.Vaults
.Where(v => v.UpdatedAt >= since)
.Select(v => v.User.UserName!)
)
.Distinct()
.ToListAsync();
return (activeUsers.Count, activeUsers);
}
private void ToggleUserNames()
{
ShowUserNames = !ShowUserNames;
StateHasChanged();
}
private sealed class UserStatistics
{
public int Last24Hours { get; set; }
public int Last7Days { get; set; }
public int Last14Days { get; set; }
public List<string> Last24HourUsers { get; set; } = new();
public List<string> Last7DayUsers { get; set; } = new();
public List<string> Last14DayUsers { get; set; } = new();
}
}

View File

@@ -0,0 +1,64 @@
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Recent emails received</h3>
</div>
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-primary-50 dark:bg-gray-700/50 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 24 hours</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Last24Hours</h4>
</div>
<div class="bg-primary-50 dark:bg-gray-700/50 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 7 days</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Last7Days</h4>
</div>
<div class="bg-primary-50 dark:bg-gray-700/50 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Last14Days</h4>
</div>
</div>
}
</div>
@code {
private bool IsLoading { get; set; } = true;
private EmailStatistics EmailStats { get; set; } = new();
/// <summary>
/// Refreshes the data displayed on the card.
/// </summary>
public async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
var now = DateTime.UtcNow;
var last24Hours = now.AddHours(-24);
var last7Days = now.AddDays(-7);
var last14Days = now.AddDays(-14);
// Get email statistics
var emailQuery = DbContext.Emails.AsQueryable();
EmailStats = new EmailStatistics
{
Last24Hours = await emailQuery.CountAsync(e => e.DateSystem >= last24Hours),
Last7Days = await emailQuery.CountAsync(e => e.DateSystem >= last7Days),
Last14Days = await emailQuery.CountAsync(e => e.DateSystem >= last14Days)
};
IsLoading = false;
StateHasChanged();
}
private sealed class EmailStatistics
{
public int Last24Hours { get; set; }
public int Last7Days { get; set; }
public int Last14Days { get; set; }
}
}

View File

@@ -0,0 +1,64 @@
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">User registrations</h3>
</div>
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 24 hours</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Last24Hours</h4>
</div>
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 7 days</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Last7Days</h4>
</div>
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-lg">
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Last14Days</h4>
</div>
</div>
}
</div>
@code {
private bool IsLoading { get; set; } = true;
private RegistrationStatistics RegistrationStats { get; set; } = new();
/// <summary>
/// Refreshes the data displayed on the card.
/// </summary>
public async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
var now = DateTime.UtcNow;
var last24Hours = now.AddHours(-24);
var last7Days = now.AddDays(-7);
var last14Days = now.AddDays(-14);
// Get registration statistics
var registrationQuery = DbContext.AliasVaultUsers.AsQueryable();
RegistrationStats = new RegistrationStatistics
{
Last24Hours = await registrationQuery.CountAsync(u => u.CreatedAt >= last24Hours),
Last7Days = await registrationQuery.CountAsync(u => u.CreatedAt >= last7Days),
Last14Days = await registrationQuery.CountAsync(u => u.CreatedAt >= last14Days)
};
IsLoading = false;
StateHasChanged();
}
private sealed class RegistrationStatistics
{
public int Last24Hours { get; set; }
public int Last7Days { get; set; }
public int Last14Days { get; set; }
}
}

View File

@@ -0,0 +1,54 @@
@page "/"
@using AliasVault.Admin.Main.Pages.Dashboard.Components
@inherits MainBase
<LayoutPageTitle>Home</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="AliasVault Admin"
Description="Welcome to the AliasVault admin portal. Below you can find statistics about recent email activity and active users.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
<div class="px-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<ActiveUsersCard @ref="_activeUsersCard" />
<RegistrationStatisticsCard @ref="_registrationStatisticsCard" />
<EmailStatisticsCard @ref="_emailStatisticsCard" />
</div>
</div>
@code {
private ActiveUsersCard? _activeUsersCard;
private RegistrationStatisticsCard? _registrationStatisticsCard;
private EmailStatisticsCard? _emailStatisticsCard;
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await RefreshData();
}
}
/// <summary>
/// Refreshes the data displayed on the cards.
/// </summary>
private async Task RefreshData()
{
if (_activeUsersCard != null &&
_registrationStatisticsCard != null &&
_emailStatisticsCard != null)
{
await Task.WhenAll(
_activeUsersCard.RefreshData(),
_registrationStatisticsCard.RefreshData(),
_emailStatisticsCard.RefreshData()
);
}
}
}

View File

@@ -1,21 +0,0 @@
@page "/"
@inherits MainBase
<LayoutPageTitle>Home</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="AliasVault Admin"
Description="Welcome to the AliasVault admin portal.">
</PageHeader>
@code {
/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
// Redirect to users page.
NavigationService.RedirectTo("users");
}
}

View File

@@ -23,7 +23,7 @@ using Microsoft.JSInterop;
/// Also, a default set of breadcrumbs is added in the parent OnInitialized method.
/// </summary>
[Authorize]
public class MainBase : OwningComponentBase
public abstract class MainBase : OwningComponentBase
{
/// <summary>
/// Gets or sets the NavigationService instance responsible for handling navigation, replaces the default NavigationManager.

View File

@@ -7,6 +7,7 @@
<SortableTableColumn IsPrimary="true">@entry.Id</SortableTableColumn>
<SortableTableColumn>@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@entry.Address</SortableTableColumn>
<SortableTableColumn>@entry.EmailCount</SortableTableColumn>
</SortableTableRow>
}
</SortableTable>
@@ -16,7 +17,7 @@
/// Gets or sets the list of email claims to display.
/// </summary>
[Parameter]
public List<UserEmailClaim> EmailClaimList { get; set; } = [];
public List<UserEmailClaimWithCount> EmailClaimList { get; set; } = [];
private string SortColumn { get; set; } = "CreatedAt";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
@@ -25,9 +26,10 @@
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Created", PropertyName = "CreatedAt" },
new TableColumn { Title = "Email", PropertyName = "Address" },
new TableColumn { Title = "Email Count", PropertyName = "EmailCount" },
];
private IEnumerable<UserEmailClaim> SortedEmailClaimList => SortList(EmailClaimList, SortColumn, SortDirection);
private IEnumerable<UserEmailClaimWithCount> SortedEmailClaimList => SortList(EmailClaimList, SortColumn, SortDirection);
private void HandleSortChanged((string column, SortDirection direction) sort)
{
@@ -36,13 +38,14 @@
StateHasChanged();
}
private static IEnumerable<UserEmailClaim> SortList(List<UserEmailClaim> emailClaims, string sortColumn, SortDirection sortDirection)
private static IEnumerable<UserEmailClaimWithCount> SortList(List<UserEmailClaimWithCount> emailClaims, string sortColumn, SortDirection sortDirection)
{
return sortColumn switch
{
"Id" => SortableTable.SortListByProperty(emailClaims, e => e.Id, sortDirection),
"CreatedAt" => SortableTable.SortListByProperty(emailClaims, e => e.CreatedAt, sortDirection),
"Address" => SortableTable.SortListByProperty(emailClaims, e => e.Address, sortDirection),
"EmailCount" => SortableTable.SortListByProperty(emailClaims, e => e.EmailCount, sortDirection),
_ => emailClaims
};
}

View File

@@ -97,7 +97,7 @@ else
private int TwoFactorKeysCount { get; set; }
private List<AliasVaultUserRefreshToken> RefreshTokenList { get; set; } = [];
private List<Vault> VaultList { get; set; } = [];
private List<UserEmailClaim> EmailClaimList { get; set; } = [];
private List<UserEmailClaimWithCount> EmailClaimList { get; set; } = [];
/// <inheritdoc />
protected override async Task OnInitializedAsync()
@@ -171,7 +171,18 @@ else
.ToListAsync();
// Load all email claims for this user.
EmailClaimList = await DbContext.UserEmailClaims.Where(x => x.UserId == User.Id)
EmailClaimList = await DbContext.UserEmailClaims
.Where(x => x.UserId == User.Id)
.Select(x => new UserEmailClaimWithCount
{
Id = x.Id,
Address = x.Address,
AddressLocal = x.AddressLocal,
AddressDomain = x.AddressDomain,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt,
EmailCount = DbContext.Emails.Count(e => e.To == x.Address)
})
.OrderBy(x => x.CreatedAt)
.ToListAsync();

View File

@@ -988,6 +988,14 @@ video {
justify-content: space-between;
}
.gap-4 {
gap: 1rem;
}
.gap-8 {
gap: 2rem;
}
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
@@ -1258,6 +1266,11 @@ video {
background-color: rgb(251 203 116 / var(--tw-bg-opacity));
}
.bg-primary-50 {
--tw-bg-opacity: 1;
background-color: rgb(255 224 150 / var(--tw-bg-opacity));
}
.bg-primary-500 {
--tw-bg-opacity: 1;
background-color: rgb(244 149 65 / var(--tw-bg-opacity));
@@ -1757,6 +1770,11 @@ video {
color: rgb(154 93 38 / var(--tw-text-opacity));
}
.hover\:text-gray-700:hover {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}
@@ -1952,6 +1970,30 @@ video {
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-blue-900\/30:is(.dark *) {
background-color: rgb(30 58 138 / 0.3);
}
.dark\:bg-gray-700\/50:is(.dark *) {
background-color: rgb(55 65 81 / 0.5);
}
.dark\:bg-green-900\/30:is(.dark *) {
background-color: rgb(20 83 45 / 0.3);
}
.dark\:bg-blue-950\/80:is(.dark *) {
background-color: rgb(23 37 84 / 0.8);
}
.dark\:bg-gray-800\/80:is(.dark *) {
background-color: rgb(31 41 55 / 0.8);
}
.dark\:bg-green-950\/80:is(.dark *) {
background-color: rgb(5 46 22 / 0.8);
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
@@ -2085,6 +2127,11 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:hover\:text-gray-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(209 213 219 / 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));
@@ -2227,6 +2274,10 @@ video {
width: 75%;
}
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@@ -2268,6 +2319,10 @@ video {
order: 2;
}
.lg\:mb-0 {
margin-bottom: 0px;
}
.lg\:mt-0 {
margin-top: 0px;
}

View File

@@ -19,7 +19,7 @@ using Microsoft.AspNetCore.Components.Authorization;
/// All pages that inherit from this class will receive default injected components that are used globally.
/// Also, a default set of breadcrumbs is added in the parent OnInitialized method.
/// </summary>
public class MainBase : OwningComponentBase
public abstract class MainBase : OwningComponentBase
{
private const string ReturnUrlKey = "returnUrl";
private bool _parametersInitialSet;

View File

@@ -245,10 +245,14 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
.Build();
// Add SQLite connection with enhanced settings
var connectionString = configuration.GetConnectionString("AliasServerDbContext") +
";Mode=ReadWriteCreate;Cache=Shared" +
";Journal Mode=WAL" +
";Synchronous=Normal" +
";Busy Timeout=30000";
optionsBuilder
.UseSqlite(
configuration.GetConnectionString("AliasServerDbContext") + ";Mode=ReadWriteCreate;Cache=Shared",
options => options.CommandTimeout(60))
.UseSqlite(connectionString, options => options.CommandTimeout(60))
.UseLazyLoadingProxies();
// Set additional PRAGMA settings
@@ -260,11 +264,7 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
using (var command = connection.CreateCommand())
{
// Increase busy timeout
command.CommandText = @"
PRAGMA busy_timeout = 30000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = FULL;
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 1073741824;";
command.ExecuteNonQuery();

View File

@@ -109,6 +109,6 @@ public class AdminPlaywrightTest : PlaywrightTest
await WaitForUrlAsync("**", "Users");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("This page gives an overview of all registered users and the associated vaults"), "No entry page content visible after logging in to admin app.");
Assert.That(pageContent, Does.Contain("Welcome to the AliasVault admin portal"), "No entry page content visible after logging in to admin app.");
}
}