mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 23:28:23 -04:00
Merge pull request #440 from lanedirt/349-add-statistics-to-admin
Add statistics to admin
This commit is contained in:
@@ -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">
|
||||
|
||||
49
src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs
Normal file
49
src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs
Normal 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; }
|
||||
}
|
||||
@@ -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/>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
54
src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor
Normal file
54
src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user