From e46357d603b98b41a12fcbc7e15c34543147dd4a Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 2 Dec 2024 16:54:07 +0100 Subject: [PATCH 1/6] Add statistics to admin home dashboard page (#349) --- src/AliasVault.Admin/Main/Pages/Home.razor | 229 +++++++++++++++++- src/AliasVault.Admin/wwwroot/css/tailwind.css | 55 +++++ 2 files changed, 277 insertions(+), 7 deletions(-) diff --git a/src/AliasVault.Admin/Main/Pages/Home.razor b/src/AliasVault.Admin/Main/Pages/Home.razor index c0be7fc3f..55d6a4b0d 100644 --- a/src/AliasVault.Admin/Main/Pages/Home.razor +++ b/src/AliasVault.Admin/Main/Pages/Home.razor @@ -6,16 +6,231 @@ + Description="Welcome to the AliasVault admin portal. Below you can find statistics about recent email activity and active users."> + + + -@code { - /// - protected override void OnInitialized() +
+ @if (IsLoading) { - base.OnInitialized(); + + } + else + { +
+ +
+
+

Active users

+ +
+
+
+

Last 24 hours

+

@UserStats.Last24Hours

+ @if (ShowUserNames) + { +
+
    + @foreach (var user in UserStats.Last24HourUsers) + { +
  • @user
  • + } +
+
+ } +
+
+

Last 7 days

+

@UserStats.Last7Days

+ @if (ShowUserNames) + { +
+
    + @foreach (var user in UserStats.Last7DayUsers) + { +
  • @user
  • + } +
+
+ } +
+
+

Last 14 days

+

@UserStats.Last14Days

+ @if (ShowUserNames) + { +
+
    + @foreach (var user in UserStats.Last14DayUsers) + { +
  • @user
  • + } +
+
+ } +
+
+
- // Redirect to users page. - NavigationService.RedirectTo("users"); + +
+
+

User registrations

+
+
+
+

Last 24 hours

+

@RegistrationStats.Last24Hours

+
+
+

Last 7 days

+

@RegistrationStats.Last7Days

+
+
+

Last 14 days

+

@RegistrationStats.Last14Days

+
+
+
+ + +
+
+

Recent emails received

+
+
+
+

Last 24 hours

+

@EmailStats.Last24Hours

+
+
+

Last 7 days

+

@EmailStats.Last7Days

+
+
+

Last 14 days

+

@EmailStats.Last14Days

+
+
+
+
+ } +
+ +@code { + private bool IsLoading { get; set; } = true; + private EmailStatistics EmailStats { get; set; } = new(); + private UserStatistics UserStats { get; set; } = new(); + private bool ShowUserNames { get; set; } = false; + private RegistrationStatistics RegistrationStats { get; set; } = new(); + + private class EmailStatistics + { + public int Last24Hours { get; set; } + public int Last7Days { get; set; } + public int Last14Days { get; set; } + } + + private class UserStatistics + { + public int Last24Hours { get; set; } + public int Last7Days { get; set; } + public int Last14Days { get; set; } + public List Last24HourUsers { get; set; } = new(); + public List Last7DayUsers { get; set; } = new(); + public List Last14DayUsers { get; set; } = new(); + } + + private class RegistrationStatistics + { + public int Last24Hours { get; set; } + public int Last7Days { get; set; } + public int Last14Days { get; set; } + } + + /// + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await RefreshData(); + } + + private 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) + }; + + // 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 + }; + + // 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 async Task<(int count, List 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(); } } diff --git a/src/AliasVault.Admin/wwwroot/css/tailwind.css b/src/AliasVault.Admin/wwwroot/css/tailwind.css index d6a08f5c6..af3cea8fb 100644 --- a/src/AliasVault.Admin/wwwroot/css/tailwind.css +++ b/src/AliasVault.Admin/wwwroot/css/tailwind.css @@ -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; } From b845245728f6811b4f3c865c2e597be28db67331 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 2 Dec 2024 16:59:21 +0100 Subject: [PATCH 2/6] Show amount of emails received in user email claim page (#349) --- .../Main/Models/UserEmailClaimWithCount.cs | 49 +++++++++++++++++++ .../View/Components/EmailClaimTable.razor | 9 ++-- .../Main/Pages/Users/View/Index.razor | 15 +++++- 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs diff --git a/src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs b/src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs new file mode 100644 index 000000000..8149256cb --- /dev/null +++ b/src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// +// 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; + +/// +/// User email claim view model with count. +/// +public class UserEmailClaimWithCount +{ + /// + /// Gets or sets the id. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the address. + /// + public string Address { get; set; } = string.Empty; + + /// + /// Gets or sets the address local. + /// + public string AddressLocal { get; set; } = string.Empty; + + /// + /// Gets or sets the address domain. + /// + public string AddressDomain { get; set; } = string.Empty; + + /// + /// Gets or sets the created at timestamp. + /// + public DateTime CreatedAt { get; set; } + + /// + /// Gets or sets the updated at timestamp. + /// + public DateTime UpdatedAt { get; set; } + + /// + /// Gets or sets the email count. + /// + public int EmailCount { get; set; } +} diff --git a/src/AliasVault.Admin/Main/Pages/Users/View/Components/EmailClaimTable.razor b/src/AliasVault.Admin/Main/Pages/Users/View/Components/EmailClaimTable.razor index 14b79d16a..4061b2941 100644 --- a/src/AliasVault.Admin/Main/Pages/Users/View/Components/EmailClaimTable.razor +++ b/src/AliasVault.Admin/Main/Pages/Users/View/Components/EmailClaimTable.razor @@ -7,6 +7,7 @@ @entry.Id @entry.CreatedAt.ToString("yyyy-MM-dd HH:mm") @entry.Address + @entry.EmailCount } @@ -16,7 +17,7 @@ /// Gets or sets the list of email claims to display. /// [Parameter] - public List EmailClaimList { get; set; } = []; + public List 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 SortedEmailClaimList => SortList(EmailClaimList, SortColumn, SortDirection); + private IEnumerable SortedEmailClaimList => SortList(EmailClaimList, SortColumn, SortDirection); private void HandleSortChanged((string column, SortDirection direction) sort) { @@ -36,13 +38,14 @@ StateHasChanged(); } - private static IEnumerable SortList(List emailClaims, string sortColumn, SortDirection sortDirection) + private static IEnumerable SortList(List 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 }; } diff --git a/src/AliasVault.Admin/Main/Pages/Users/View/Index.razor b/src/AliasVault.Admin/Main/Pages/Users/View/Index.razor index e5a08113f..ff85a41db 100644 --- a/src/AliasVault.Admin/Main/Pages/Users/View/Index.razor +++ b/src/AliasVault.Admin/Main/Pages/Users/View/Index.razor @@ -97,7 +97,7 @@ else private int TwoFactorKeysCount { get; set; } private List RefreshTokenList { get; set; } = []; private List VaultList { get; set; } = []; - private List EmailClaimList { get; set; } = []; + private List EmailClaimList { get; set; } = []; /// 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(); From fccf10dc82b376591b07738b83d30518db0b86b5 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 2 Dec 2024 17:08:09 +0100 Subject: [PATCH 3/6] Tweak UI of admin account settings (#349) --- .../Main/Layout/TopMenu.razor | 2 +- .../Pages/Account/Manage/ChangePassword.razor | 2 +- .../Main/Pages/Account/Manage/Index.razor | 67 ------------------- .../Manage/TwoFactorAuthentication.razor | 6 +- .../Main/Pages/Account/ManageLayout.razor | 5 +- .../Main/Pages/Account/ManageNavMenu.razor | 7 +- 6 files changed, 8 insertions(+), 81 deletions(-) delete mode 100644 src/AliasVault.Admin/Main/Pages/Account/Manage/Index.razor diff --git a/src/AliasVault.Admin/Main/Layout/TopMenu.razor b/src/AliasVault.Admin/Main/Layout/TopMenu.razor index 4e6260f83..9fda86b5f 100644 --- a/src/AliasVault.Admin/Main/Layout/TopMenu.razor +++ b/src/AliasVault.Admin/Main/Layout/TopMenu.razor @@ -52,7 +52,7 @@
    diff --git a/src/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor b/src/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor index 48daf0d81..8aa519f6b 100644 --- a/src/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor +++ b/src/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor @@ -8,7 +8,7 @@ Change password -
    +

    Change password

    diff --git a/src/AliasVault.Admin/Main/Pages/Account/Manage/Index.razor b/src/AliasVault.Admin/Main/Pages/Account/Manage/Index.razor deleted file mode 100644 index f6340b935..000000000 --- a/src/AliasVault.Admin/Main/Pages/Account/Manage/Index.razor +++ /dev/null @@ -1,67 +0,0 @@ -@page "/account/manage" -@using System.ComponentModel.DataAnnotations -@using Microsoft.AspNetCore.Identity - -@inject UserManager UserManager - -Profile - -
    -

    Profile

    - - - - -
    - - -
    -
    - - - -
    -
    - Save -
    -
    -
    - -@code { - private string? username; - private string? phoneNumber; - - [SupplyParameterFromForm] private InputModel Input { get; set; } = new(); - - /// - 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; } - } - -} diff --git a/src/AliasVault.Admin/Main/Pages/Account/Manage/TwoFactorAuthentication.razor b/src/AliasVault.Admin/Main/Pages/Account/Manage/TwoFactorAuthentication.razor index 1bff518c1..de59bcb54 100644 --- a/src/AliasVault.Admin/Main/Pages/Account/Manage/TwoFactorAuthentication.razor +++ b/src/AliasVault.Admin/Main/Pages/Account/Manage/TwoFactorAuthentication.razor @@ -1,15 +1,13 @@ @page "/account/manage/2fa" @using Microsoft.AspNetCore.Identity - @inject UserManager UserManager -@inject SignInManager SignInManager Two-factor authentication (2FA) @if (is2FaEnabled) { -
    +

    Two-factor authentication (2FA)

    @if (recoveryCodesLeft == 0) @@ -41,7 +39,7 @@
    } -
    +

    Authenticator app

    @if (!hasAuthenticator) diff --git a/src/AliasVault.Admin/Main/Pages/Account/ManageLayout.razor b/src/AliasVault.Admin/Main/Pages/Account/ManageLayout.razor index 0a5f761ae..7dd2f7a29 100644 --- a/src/AliasVault.Admin/Main/Pages/Account/ManageLayout.razor +++ b/src/AliasVault.Admin/Main/Pages/Account/ManageLayout.razor @@ -5,11 +5,10 @@ + Description="Manage security settings for the admin account here."> -
    -
    +
    diff --git a/src/AliasVault.Admin/Main/Pages/Account/ManageNavMenu.razor b/src/AliasVault.Admin/Main/Pages/Account/ManageNavMenu.razor index c3c39e2bd..e444039ed 100644 --- a/src/AliasVault.Admin/Main/Pages/Account/ManageNavMenu.razor +++ b/src/AliasVault.Admin/Main/Pages/Account/ManageNavMenu.razor @@ -4,12 +4,9 @@
    • - Profile + Password
    • - Password -
    • -
    • - Two-factor authentication + Two-factor authentication
    From ca4dd89e890781c19cb33a726bb2e43cf4c89d33 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 2 Dec 2024 17:10:15 +0100 Subject: [PATCH 4/6] Update AdminPlaywrightTest.cs (#349) --- src/Tests/AliasVault.E2ETests/Common/AdminPlaywrightTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/AliasVault.E2ETests/Common/AdminPlaywrightTest.cs b/src/Tests/AliasVault.E2ETests/Common/AdminPlaywrightTest.cs index 0e60787a2..411b88af9 100644 --- a/src/Tests/AliasVault.E2ETests/Common/AdminPlaywrightTest.cs +++ b/src/Tests/AliasVault.E2ETests/Common/AdminPlaywrightTest.cs @@ -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."); } } From 65304b0f84e6c34e04f5aab260d1c7f2edc57e8f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 3 Dec 2024 22:56:22 +0100 Subject: [PATCH 5/6] Refactor admin dashboard into separate components (#349) --- .../Components/ActiveUsersCard.razor | 137 ++++++++++ .../Components/EmailStatisticsCard.razor | 64 +++++ .../RegistrationStatisticsCard.razor | 64 +++++ .../Main/Pages/Dashboard/Index.razor | 54 ++++ src/AliasVault.Admin/Main/Pages/Home.razor | 236 ------------------ src/AliasVault.Admin/Main/Pages/MainBase.cs | 2 +- src/AliasVault.Client/Main/Pages/MainBase.cs | 2 +- 7 files changed, 321 insertions(+), 238 deletions(-) create mode 100644 src/AliasVault.Admin/Main/Pages/Dashboard/Components/ActiveUsersCard.razor create mode 100644 src/AliasVault.Admin/Main/Pages/Dashboard/Components/EmailStatisticsCard.razor create mode 100644 src/AliasVault.Admin/Main/Pages/Dashboard/Components/RegistrationStatisticsCard.razor create mode 100644 src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor delete mode 100644 src/AliasVault.Admin/Main/Pages/Home.razor diff --git a/src/AliasVault.Admin/Main/Pages/Dashboard/Components/ActiveUsersCard.razor b/src/AliasVault.Admin/Main/Pages/Dashboard/Components/ActiveUsersCard.razor new file mode 100644 index 000000000..d051e0aec --- /dev/null +++ b/src/AliasVault.Admin/Main/Pages/Dashboard/Components/ActiveUsersCard.razor @@ -0,0 +1,137 @@ +
    +
    +

    Active users

    + +
    + @if (IsLoading) + { + + } + else + { +
    +
    +

    Last 24 hours

    +

    @UserStats.Last24Hours

    + @if (ShowUserNames) + { +
    +
      + @foreach (var user in UserStats.Last24HourUsers) + { +
    • @user
    • + } +
    +
    + } +
    +
    +

    Last 7 days

    +

    @UserStats.Last7Days

    + @if (ShowUserNames) + { +
    +
      + @foreach (var user in UserStats.Last7DayUsers) + { +
    • @user
    • + } +
    +
    + } +
    +
    +

    Last 14 days

    +

    @UserStats.Last14Days

    + @if (ShowUserNames) + { +
    +
      + @foreach (var user in UserStats.Last14DayUsers) + { +
    • @user
    • + } +
    +
    + } +
    +
    + } +
    + +@code { + private bool IsLoading { get; set; } = true; + private UserStatistics UserStats { get; set; } = new(); + private bool ShowUserNames { get; set; } + + /// + /// Refreshes the data displayed on the card. + /// + 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 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 Last24HourUsers { get; set; } = new(); + public List Last7DayUsers { get; set; } = new(); + public List Last14DayUsers { get; set; } = new(); + } +} diff --git a/src/AliasVault.Admin/Main/Pages/Dashboard/Components/EmailStatisticsCard.razor b/src/AliasVault.Admin/Main/Pages/Dashboard/Components/EmailStatisticsCard.razor new file mode 100644 index 000000000..51402fdd1 --- /dev/null +++ b/src/AliasVault.Admin/Main/Pages/Dashboard/Components/EmailStatisticsCard.razor @@ -0,0 +1,64 @@ +
    +
    +

    Recent emails received

    +
    + @if (IsLoading) + { + + } + else + { +
    +
    +

    Last 24 hours

    +

    @EmailStats.Last24Hours

    +
    +
    +

    Last 7 days

    +

    @EmailStats.Last7Days

    +
    +
    +

    Last 14 days

    +

    @EmailStats.Last14Days

    +
    +
    + } +
    + +@code { + private bool IsLoading { get; set; } = true; + private EmailStatistics EmailStats { get; set; } = new(); + + /// + /// Refreshes the data displayed on the card. + /// + 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; } + } +} diff --git a/src/AliasVault.Admin/Main/Pages/Dashboard/Components/RegistrationStatisticsCard.razor b/src/AliasVault.Admin/Main/Pages/Dashboard/Components/RegistrationStatisticsCard.razor new file mode 100644 index 000000000..85be0dd3b --- /dev/null +++ b/src/AliasVault.Admin/Main/Pages/Dashboard/Components/RegistrationStatisticsCard.razor @@ -0,0 +1,64 @@ +
    +
    +

    User registrations

    +
    + @if (IsLoading) + { + + } + else + { +
    +
    +

    Last 24 hours

    +

    @RegistrationStats.Last24Hours

    +
    +
    +

    Last 7 days

    +

    @RegistrationStats.Last7Days

    +
    +
    +

    Last 14 days

    +

    @RegistrationStats.Last14Days

    +
    +
    + } +
    + +@code { + private bool IsLoading { get; set; } = true; + private RegistrationStatistics RegistrationStats { get; set; } = new(); + + /// + /// Refreshes the data displayed on the card. + /// + 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; } + } +} diff --git a/src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor b/src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor new file mode 100644 index 000000000..9b2459abe --- /dev/null +++ b/src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor @@ -0,0 +1,54 @@ +@page "/" +@using AliasVault.Admin.Main.Pages.Dashboard.Components +@inherits MainBase + +Home + + + + + + + +
    +
    + + + +
    +
    + +@code { + private ActiveUsersCard? _activeUsersCard; + private RegistrationStatisticsCard? _registrationStatisticsCard; + private EmailStatisticsCard? _emailStatisticsCard; + + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await RefreshData(); + } + } + + /// + /// Refreshes the data displayed on the cards. + /// + private async Task RefreshData() + { + if (_activeUsersCard != null && + _registrationStatisticsCard != null && + _emailStatisticsCard != null) + { + await Task.WhenAll( + _activeUsersCard.RefreshData(), + _registrationStatisticsCard.RefreshData(), + _emailStatisticsCard.RefreshData() + ); + } + } +} diff --git a/src/AliasVault.Admin/Main/Pages/Home.razor b/src/AliasVault.Admin/Main/Pages/Home.razor deleted file mode 100644 index 55d6a4b0d..000000000 --- a/src/AliasVault.Admin/Main/Pages/Home.razor +++ /dev/null @@ -1,236 +0,0 @@ -@page "/" -@inherits MainBase - -Home - - - - - - - -
    - @if (IsLoading) - { - - } - else - { -
    - -
    -
    -

    Active users

    - -
    -
    -
    -

    Last 24 hours

    -

    @UserStats.Last24Hours

    - @if (ShowUserNames) - { -
    -
      - @foreach (var user in UserStats.Last24HourUsers) - { -
    • @user
    • - } -
    -
    - } -
    -
    -

    Last 7 days

    -

    @UserStats.Last7Days

    - @if (ShowUserNames) - { -
    -
      - @foreach (var user in UserStats.Last7DayUsers) - { -
    • @user
    • - } -
    -
    - } -
    -
    -

    Last 14 days

    -

    @UserStats.Last14Days

    - @if (ShowUserNames) - { -
    -
      - @foreach (var user in UserStats.Last14DayUsers) - { -
    • @user
    • - } -
    -
    - } -
    -
    -
    - - -
    -
    -

    User registrations

    -
    -
    -
    -

    Last 24 hours

    -

    @RegistrationStats.Last24Hours

    -
    -
    -

    Last 7 days

    -

    @RegistrationStats.Last7Days

    -
    -
    -

    Last 14 days

    -

    @RegistrationStats.Last14Days

    -
    -
    -
    - - -
    -
    -

    Recent emails received

    -
    -
    -
    -

    Last 24 hours

    -

    @EmailStats.Last24Hours

    -
    -
    -

    Last 7 days

    -

    @EmailStats.Last7Days

    -
    -
    -

    Last 14 days

    -

    @EmailStats.Last14Days

    -
    -
    -
    -
    - } -
    - -@code { - private bool IsLoading { get; set; } = true; - private EmailStatistics EmailStats { get; set; } = new(); - private UserStatistics UserStats { get; set; } = new(); - private bool ShowUserNames { get; set; } = false; - private RegistrationStatistics RegistrationStats { get; set; } = new(); - - private class EmailStatistics - { - public int Last24Hours { get; set; } - public int Last7Days { get; set; } - public int Last14Days { get; set; } - } - - private class UserStatistics - { - public int Last24Hours { get; set; } - public int Last7Days { get; set; } - public int Last14Days { get; set; } - public List Last24HourUsers { get; set; } = new(); - public List Last7DayUsers { get; set; } = new(); - public List Last14DayUsers { get; set; } = new(); - } - - private class RegistrationStatistics - { - public int Last24Hours { get; set; } - public int Last7Days { get; set; } - public int Last14Days { get; set; } - } - - /// - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - await RefreshData(); - } - - private 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) - }; - - // 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 - }; - - // 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 async Task<(int count, List 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(); - } -} diff --git a/src/AliasVault.Admin/Main/Pages/MainBase.cs b/src/AliasVault.Admin/Main/Pages/MainBase.cs index 9b3ff31f7..dd4ef5de9 100644 --- a/src/AliasVault.Admin/Main/Pages/MainBase.cs +++ b/src/AliasVault.Admin/Main/Pages/MainBase.cs @@ -23,7 +23,7 @@ using Microsoft.JSInterop; /// Also, a default set of breadcrumbs is added in the parent OnInitialized method. /// [Authorize] -public class MainBase : OwningComponentBase +public abstract class MainBase : OwningComponentBase { /// /// Gets or sets the NavigationService instance responsible for handling navigation, replaces the default NavigationManager. diff --git a/src/AliasVault.Client/Main/Pages/MainBase.cs b/src/AliasVault.Client/Main/Pages/MainBase.cs index b99dae4de..072ba5986 100644 --- a/src/AliasVault.Client/Main/Pages/MainBase.cs +++ b/src/AliasVault.Client/Main/Pages/MainBase.cs @@ -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. /// -public class MainBase : OwningComponentBase +public abstract class MainBase : OwningComponentBase { private const string ReturnUrlKey = "returnUrl"; private bool _parametersInitialSet; From 973abc8917f17bafd543fb4a8964095054b1ce43 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 4 Dec 2024 10:00:49 +0100 Subject: [PATCH 6/6] Update sqlite connection string to include WAL mode directly (#349) --- .../AliasServerDb/AliasServerDbContext.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Databases/AliasServerDb/AliasServerDbContext.cs b/src/Databases/AliasServerDb/AliasServerDbContext.cs index dc2db1723..cca5d0616 100644 --- a/src/Databases/AliasServerDb/AliasServerDbContext.cs +++ b/src/Databases/AliasServerDb/AliasServerDbContext.cs @@ -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();