diff --git a/apps/server/AliasVault.Admin/Main/Pages/Emails.razor b/apps/server/AliasVault.Admin/Main/Pages/Emails.razor
index 0c2a10602..b0f592b42 100644
--- a/apps/server/AliasVault.Admin/Main/Pages/Emails.razor
+++ b/apps/server/AliasVault.Admin/Main/Pages/Emails.razor
@@ -15,7 +15,7 @@
Email Storage Stats
-
+
@@ -88,6 +88,7 @@ else
private int PageSize { get; set; } = 50;
private int TotalRecords { get; set; }
private string _searchTerm = string.Empty;
+ private CancellationTokenSource? _searchCancellationTokenSource;
///
/// The last search term.
@@ -102,7 +103,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
- _ = RefreshData();
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource = new CancellationTokenSource();
+ _ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -113,7 +116,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
///
@@ -134,53 +137,65 @@ else
_searchTerm = SearchTermFromQuery;
}
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
- _ = RefreshData();
+ _ = RefreshData(CancellationToken.None);
}
- private async Task RefreshData()
+ private async Task RefreshData(CancellationToken cancellationToken = default)
{
- IsLoading = true;
- StateHasChanged();
-
- await using var dbContext = await DbContextFactory.CreateDbContextAsync();
-
- IQueryable query = dbContext.Emails;
-
- query = ApplySearchFilter(query);
- query = ApplySort(query);
-
- TotalRecords = await query.CountAsync();
- var emailList = await query
- .Skip((CurrentPage - 1) * PageSize)
- .Take(PageSize)
- .ToListAsync();
-
- // Get all usernames for the emails in the current list
- var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
- var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
- .Where(x => encryptionKeyIds.Contains(x.Id))
- .Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
- .ToListAsync();
-
- // Create new list of viewmodels
- EmailViewModelList = new List();
-
- foreach (var email in emailList)
+ try
{
- var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
- EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
- }
+ IsLoading = true;
+ StateHasChanged();
- IsLoading = false;
- IsInitialized = true;
- StateHasChanged();
+ await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
+
+ IQueryable query = dbContext.Emails;
+
+ query = ApplySearchFilter(query);
+ query = ApplySort(query);
+
+ TotalRecords = await query.CountAsync(cancellationToken);
+ var emailList = await query
+ .Skip((CurrentPage - 1) * PageSize)
+ .Take(PageSize)
+ .ToListAsync(cancellationToken);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ // Get all usernames for the emails in the current list
+ var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
+ var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
+ .Where(x => encryptionKeyIds.Contains(x.Id))
+ .Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
+ .ToListAsync(cancellationToken);
+
+ // Create new list of viewmodels
+ EmailViewModelList = new List();
+
+ foreach (var email in emailList)
+ {
+ var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
+ EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
+ }
+
+ IsLoading = false;
+ IsInitialized = true;
+ StateHasChanged();
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when cancellation is requested, do nothing
+ }
}
///
@@ -254,4 +269,14 @@ else
public string UserId { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
}
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
}
diff --git a/apps/server/AliasVault.Admin/Main/Pages/Logging/Auth.razor b/apps/server/AliasVault.Admin/Main/Pages/Logging/Auth.razor
index eec295568..c61193e04 100644
--- a/apps/server/AliasVault.Admin/Main/Pages/Logging/Auth.razor
+++ b/apps/server/AliasVault.Admin/Main/Pages/Logging/Auth.razor
@@ -14,7 +14,7 @@
Description="This page shows an overview of recent auth attempts.">
-
+
@@ -107,6 +107,8 @@ else
private string _searchTerm = string.Empty;
private string _lastSearchTerm = string.Empty;
+ private CancellationTokenSource? _searchCancellationTokenSource;
+
private string SearchTerm
{
get => _searchTerm;
@@ -115,7 +117,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
- _ = RefreshData();
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource = new CancellationTokenSource();
+ _ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -129,7 +133,9 @@ else
if (_selectedEventType != value)
{
_selectedEventType = value;
- _ = RefreshData();
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource = new CancellationTokenSource();
+ _ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -139,7 +145,7 @@ else
private void ToggleUniqueUsernames()
{
ShowUniqueUsernames = !ShowUniqueUsernames;
- _ = RefreshData();
+ _ = RefreshData(CancellationToken.None);
}
private string SortColumn { get; set; } = "Id";
@@ -149,7 +155,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
///
@@ -161,7 +167,7 @@ else
Navigation.LocationChanged += OnLocationChanged;
ParseQueryAndRefresh();
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
@@ -186,7 +192,7 @@ else
if (_searchTerm != _lastSearchTerm)
{
_lastSearchTerm = _searchTerm;
- _ = RefreshData(); // Fire and forget
+ _ = RefreshData(CancellationToken.None); // Fire and forget
}
}
@@ -194,96 +200,115 @@ else
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource?.Dispose();
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
- _ = RefreshData();
+ _ = RefreshData(CancellationToken.None);
}
- private async Task RefreshData()
+ private async Task RefreshData(CancellationToken cancellationToken = default)
{
- IsLoading = true;
- StateHasChanged();
-
- await using var dbContext = await DbContextFactory.CreateDbContextAsync();
- var query = dbContext.AuthLogs.AsQueryable();
-
- if (!string.IsNullOrEmpty(SearchTerm))
+ try
{
- // Reset page number back to 1 if the search term has changed.
- if (SearchTerm != _lastSearchTerm)
- {
- CurrentPage = 1;
- }
- _lastSearchTerm = SearchTerm;
+ IsLoading = true;
+ StateHasChanged();
- // If the search term starts with "client:", we search for the client header.
- if (SearchTerm.StartsWith("client:", StringComparison.OrdinalIgnoreCase))
+ await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
+ var query = dbContext.AuthLogs.AsQueryable();
+
+ if (!string.IsNullOrEmpty(SearchTerm))
{
- var clientSearchTerm = SearchTerm.Substring(7).ToLower();
- query = query.Where(x => EF.Functions.Like((x.Client ?? string.Empty).ToLower(), "%" + clientSearchTerm + "%"));
+ // Reset page number back to 1 if the search term has changed.
+ if (SearchTerm != _lastSearchTerm)
+ {
+ CurrentPage = 1;
+ }
+ _lastSearchTerm = SearchTerm;
+
+ // If the search term starts with "client:", we search for the client header.
+ if (SearchTerm.StartsWith("client:", StringComparison.OrdinalIgnoreCase))
+ {
+ var clientSearchTerm = SearchTerm.Substring(7).ToLower();
+ query = query.Where(x => EF.Functions.Like((x.Client ?? string.Empty).ToLower(), "%" + clientSearchTerm + "%"));
+ }
+ else
+ {
+ var searchTerm = SearchTerm.Trim().ToLower();
+ query = query.Where(x => EF.Functions.Like((x.Username ?? string.Empty).ToLower(), "%" + searchTerm + "%") ||
+ EF.Functions.Like((x.IpAddress ?? string.Empty).ToLower(), "%" + searchTerm + "%"));
+ }
+ }
+
+ if (!string.IsNullOrEmpty(SelectedEventType))
+ {
+ var success = Enum.TryParse(SelectedEventType, out var eventType);
+ if (success)
+ {
+ query = query.Where(x => x.EventType == eventType);
+ }
+ }
+
+ query = ApplySort(query);
+
+ // Handle unique usernames filtering after getting all results (since GroupBy with OrderBy in Select is complex for EF)
+ if (ShowUniqueUsernames)
+ {
+ // Get all matching records first
+ var allLogs = await query.ToListAsync(cancellationToken);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ // Group by username and take the latest entry for each username
+ var uniqueLogs = allLogs
+ .GroupBy(x => x.Username)
+ .Select(g => g.OrderByDescending(x => x.Timestamp).First())
+ .ToList();
+
+ // Apply pagination to the unique results
+ TotalRecords = uniqueLogs.Count;
+ LogList = uniqueLogs
+ .Skip((CurrentPage - 1) * PageSize)
+ .Take(PageSize)
+ .ToList();
}
else
{
- var searchTerm = SearchTerm.Trim().ToLower();
- query = query.Where(x => EF.Functions.Like((x.Username ?? string.Empty).ToLower(), "%" + searchTerm + "%") ||
- EF.Functions.Like((x.IpAddress ?? string.Empty).ToLower(), "%" + searchTerm + "%"));
+ TotalRecords = await query.CountAsync(cancellationToken);
+ LogList = await query
+ .Skip((CurrentPage - 1) * PageSize)
+ .Take(PageSize)
+ .ToListAsync(cancellationToken);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
}
- }
- if (!string.IsNullOrEmpty(SelectedEventType))
+ // Create user lookup dictionary for the current page
+ var usernames = LogList.Select(x => x.Username).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
+ var users = await dbContext.AliasVaultUsers
+ .Where(u => u.UserName != null && usernames.Contains(u.UserName))
+ .Select(u => new { u.UserName, u.Id })
+ .ToListAsync(cancellationToken);
+
+ UserLookup = users.Where(u => u.UserName != null).ToDictionary(u => u.UserName!, u => u.Id);
+
+ IsLoading = false;
+ IsInitialized = true;
+ StateHasChanged();
+ }
+ catch (OperationCanceledException)
{
- var success = Enum.TryParse(SelectedEventType, out var eventType);
- if (success)
- {
- query = query.Where(x => x.EventType == eventType);
- }
+ // Expected when cancellation is requested, do nothing
}
-
- query = ApplySort(query);
-
- // Handle unique usernames filtering after getting all results (since GroupBy with OrderBy in Select is complex for EF)
- if (ShowUniqueUsernames)
- {
- // Get all matching records first
- var allLogs = await query.ToListAsync();
-
- // Group by username and take the latest entry for each username
- var uniqueLogs = allLogs
- .GroupBy(x => x.Username)
- .Select(g => g.OrderByDescending(x => x.Timestamp).First())
- .ToList();
-
- // Apply pagination to the unique results
- TotalRecords = uniqueLogs.Count;
- LogList = uniqueLogs
- .Skip((CurrentPage - 1) * PageSize)
- .Take(PageSize)
- .ToList();
- }
- else
- {
- TotalRecords = await query.CountAsync();
- LogList = await query
- .Skip((CurrentPage - 1) * PageSize)
- .Take(PageSize)
- .ToListAsync();
- }
-
- // Create user lookup dictionary for the current page
- var usernames = LogList.Select(x => x.Username).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
- var users = await dbContext.AliasVaultUsers
- .Where(u => u.UserName != null && usernames.Contains(u.UserName))
- .Select(u => new { u.UserName, u.Id })
- .ToListAsync();
-
- UserLookup = users.Where(u => u.UserName != null).ToDictionary(u => u.UserName!, u => u.Id);
-
- IsLoading = false;
- IsInitialized = true;
- StateHasChanged();
}
///
@@ -356,7 +381,7 @@ else
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.AuthLogs.RemoveRange(dbContext.AuthLogs);
await dbContext.SaveChangesAsync();
- await RefreshData();
+ await RefreshData(CancellationToken.None);
IsLoading = false;
StateHasChanged();
diff --git a/apps/server/AliasVault.Admin/Main/Pages/Logging/General.razor b/apps/server/AliasVault.Admin/Main/Pages/Logging/General.razor
index 3d1114525..669754095 100644
--- a/apps/server/AliasVault.Admin/Main/Pages/Logging/General.razor
+++ b/apps/server/AliasVault.Admin/Main/Pages/Logging/General.razor
@@ -10,7 +10,7 @@
Description="This page shows an overview of recent system logs.">
-
+
@@ -102,6 +102,8 @@ else
private string _searchTerm = string.Empty;
private string _lastSearchTerm = string.Empty;
+ private CancellationTokenSource? _searchCancellationTokenSource;
+
private string SearchTerm
{
get => _searchTerm;
@@ -110,7 +112,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
- _ = RefreshData();
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource = new CancellationTokenSource();
+ _ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -124,7 +128,9 @@ else
if (_selectedServiceName != value)
{
_selectedServiceName = value;
- _ = RefreshData();
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource = new CancellationTokenSource();
+ _ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -138,7 +144,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
///
@@ -155,66 +161,78 @@ else
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
ServiceNames = await dbContext.Logs.Select(l => l.Application).Distinct().ToListAsync();
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
- _ = RefreshData();
+ _ = RefreshData(CancellationToken.None);
}
- private async Task RefreshData()
+ private async Task RefreshData(CancellationToken cancellationToken = default)
{
- IsLoading = true;
- StateHasChanged();
-
- await using var dbContext = await DbContextFactory.CreateDbContextAsync();
- var query = dbContext.Logs.AsQueryable();
-
- query = ApplySearchTermFilter(query);
- query = ApplyServiceNameFilter(query);
-
- // Apply sort.
- switch (SortColumn)
+ try
{
- case "Application":
- query = SortDirection == SortDirection.Ascending
- ? query.OrderBy(x => x.Application)
- : query.OrderByDescending(x => x.Application);
- break;
- case "Message":
- query = SortDirection == SortDirection.Ascending
- ? query.OrderBy(x => x.Message)
- : query.OrderByDescending(x => x.Message);
- break;
- case "Level":
- query = SortDirection == SortDirection.Ascending
- ? query.OrderBy(x => x.Level)
- : query.OrderByDescending(x => x.Level);
- break;
- case "Timestamp":
- query = SortDirection == SortDirection.Ascending
- ? query.OrderBy(x => x.TimeStamp)
- : query.OrderByDescending(x => x.TimeStamp);
- break;
- default:
- query = SortDirection == SortDirection.Ascending
- ? query.OrderBy(x => x.Id)
- : query.OrderByDescending(x => x.Id);
- break;
+ IsLoading = true;
+ StateHasChanged();
+
+ await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
+ var query = dbContext.Logs.AsQueryable();
+
+ query = ApplySearchTermFilter(query);
+ query = ApplyServiceNameFilter(query);
+
+ // Apply sort.
+ switch (SortColumn)
+ {
+ case "Application":
+ query = SortDirection == SortDirection.Ascending
+ ? query.OrderBy(x => x.Application)
+ : query.OrderByDescending(x => x.Application);
+ break;
+ case "Message":
+ query = SortDirection == SortDirection.Ascending
+ ? query.OrderBy(x => x.Message)
+ : query.OrderByDescending(x => x.Message);
+ break;
+ case "Level":
+ query = SortDirection == SortDirection.Ascending
+ ? query.OrderBy(x => x.Level)
+ : query.OrderByDescending(x => x.Level);
+ break;
+ case "Timestamp":
+ query = SortDirection == SortDirection.Ascending
+ ? query.OrderBy(x => x.TimeStamp)
+ : query.OrderByDescending(x => x.TimeStamp);
+ break;
+ default:
+ query = SortDirection == SortDirection.Ascending
+ ? query.OrderBy(x => x.Id)
+ : query.OrderByDescending(x => x.Id);
+ break;
+ }
+
+ TotalRecords = await query.CountAsync(cancellationToken);
+ LogList = await query
+ .Skip((CurrentPage - 1) * PageSize)
+ .Take(PageSize)
+ .ToListAsync(cancellationToken);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ IsLoading = false;
+ IsInitialized = true;
+ StateHasChanged();
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when cancellation is requested, do nothing
}
-
- TotalRecords = await query.CountAsync();
- LogList = await query
- .Skip((CurrentPage - 1) * PageSize)
- .Take(PageSize)
- .ToListAsync();
-
- IsLoading = false;
- IsInitialized = true;
- StateHasChanged();
}
///
@@ -272,9 +290,19 @@ else
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.Logs.RemoveRange(dbContext.Logs);
await dbContext.SaveChangesAsync();
- await RefreshData();
+ await RefreshData(CancellationToken.None);
IsLoading = false;
StateHasChanged();
}
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
}
diff --git a/apps/server/AliasVault.Admin/Main/Pages/Users/Users.razor b/apps/server/AliasVault.Admin/Main/Pages/Users/Users.razor
index e25ac817d..69f817f80 100644
--- a/apps/server/AliasVault.Admin/Main/Pages/Users/Users.razor
+++ b/apps/server/AliasVault.Admin/Main/Pages/Users/Users.razor
@@ -9,7 +9,7 @@
Title="@(TotalRecords > 0 ? $"Users ({TotalRecords:N0})" : "Users")"
Description="This page shows an overview of all registered users and the associated vaults.">
-
+
@@ -86,6 +86,7 @@ else
private int TotalRecords { get; set; }
private string _searchTerm = string.Empty;
+ private CancellationTokenSource? _searchCancellationTokenSource;
///
/// The last search term.
@@ -100,7 +101,9 @@ else
if (_searchTerm != value)
{
_searchTerm = value;
- _ = RefreshData();
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource = new CancellationTokenSource();
+ _ = RefreshData(_searchCancellationTokenSource.Token);
}
}
}
@@ -112,7 +115,7 @@ else
{
SortColumn = sort.column;
SortDirection = sort.direction;
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
///
@@ -127,72 +130,84 @@ else
{
if (firstRender)
{
- await RefreshData();
+ await RefreshData(CancellationToken.None);
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
- _ = RefreshData();
+ _ = RefreshData(CancellationToken.None);
}
- private async Task RefreshData()
+ private async Task RefreshData(CancellationToken cancellationToken = default)
{
- IsLoading = true;
- StateHasChanged();
-
- await using var dbContext = await DbContextFactory.CreateDbContextAsync();
- IQueryable query = dbContext.AliasVaultUsers;
-
- query = ApplySearchFilter(query);
- query = ApplySort(query, dbContext);
-
- TotalRecords = await query.CountAsync();
- var users = await query
- .Skip((CurrentPage - 1) * PageSize)
- .Take(PageSize)
- .Select(u => new
- {
- u.Id,
- u.UserName,
- u.CreatedAt,
- u.TwoFactorEnabled,
- u.Blocked,
- Vaults = u.Vaults.Select(v => new
- {
- v.FileSize,
- v.CreatedAt,
- v.RevisionNumber,
- CredentialCount = v.CredentialsCount,
- }),
- EmailClaims = u.EmailClaims.Select(ec => new
- {
- ec.CreatedAt,
- ec.Address
- }),
- ReceivedEmails = u.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count(),
- })
- .ToListAsync();
-
- UserList = users.Select(user => new UserViewModel
+ try
{
- Id = user.Id,
- UserName = user.UserName?.ToLower() ?? "N/A",
- TwoFactorEnabled = user.TwoFactorEnabled,
- Blocked = user.Blocked,
- CreatedAt = user.CreatedAt,
- VaultCount = user.Vaults.Count(),
- CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
- EmailClaimCount = user.EmailClaims.Count(),
- ReceivedEmailCount = user.ReceivedEmails,
- VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
- LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt,
- }).ToList();
+ IsLoading = true;
+ StateHasChanged();
- IsLoading = false;
- IsInitialized = true;
- StateHasChanged();
+ await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
+ IQueryable query = dbContext.AliasVaultUsers;
+
+ query = ApplySearchFilter(query);
+ query = ApplySort(query, dbContext);
+
+ TotalRecords = await query.CountAsync(cancellationToken);
+ var users = await query
+ .Skip((CurrentPage - 1) * PageSize)
+ .Take(PageSize)
+ .Select(u => new
+ {
+ u.Id,
+ u.UserName,
+ u.CreatedAt,
+ u.TwoFactorEnabled,
+ u.Blocked,
+ Vaults = u.Vaults.Select(v => new
+ {
+ v.FileSize,
+ v.CreatedAt,
+ v.RevisionNumber,
+ CredentialCount = v.CredentialsCount,
+ }),
+ EmailClaims = u.EmailClaims.Select(ec => new
+ {
+ ec.CreatedAt,
+ ec.Address
+ }),
+ ReceivedEmails = u.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count(),
+ })
+ .ToListAsync(cancellationToken);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ UserList = users.Select(user => new UserViewModel
+ {
+ Id = user.Id,
+ UserName = user.UserName?.ToLower() ?? "N/A",
+ TwoFactorEnabled = user.TwoFactorEnabled,
+ Blocked = user.Blocked,
+ CreatedAt = user.CreatedAt,
+ VaultCount = user.Vaults.Count(),
+ CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
+ EmailClaimCount = user.EmailClaims.Count(),
+ ReceivedEmailCount = user.ReceivedEmails,
+ VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
+ LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt,
+ }).ToList();
+
+ IsLoading = false;
+ IsInitialized = true;
+ StateHasChanged();
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when cancellation is requested, do nothing
+ }
}
///
@@ -279,4 +294,14 @@ else
return query;
}
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _searchCancellationTokenSource?.Cancel();
+ _searchCancellationTokenSource?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
}