Files
aliasvault/apps/server/AliasVault.Admin/Main/Pages/Users/Users.razor

276 lines
11 KiB
Plaintext

@page "/users"
@using AliasVault.RazorComponents.Tables
@inherits MainBase
<LayoutPageTitle>Users</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@(TotalRecords > 0 ? $"Users ({TotalRecords:N0})" : "Users")"
Description="This page shows an overview of all registered users and the associated vaults.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsInitialized)
{
<div class="px-4">
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
<div class="mb-3">
<div class="relative">
<SearchIcon />
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search users..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
</div>
</div>
</div>
}
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
<div class="px-4">
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var user in UserList)
{
<SortableTableRow>
<SortableTableColumn IsPrimary="true">@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@user.UserName</SortableTableColumn>
<SortableTableColumn>@user.VaultCount</SortableTableColumn>
<SortableTableColumn>@user.CredentialCount</SortableTableColumn>
<SortableTableColumn>@user.EmailClaimCount</SortableTableColumn>
<SortableTableColumn>@user.ReceivedEmailCount</SortableTableColumn>
<SortableTableColumn>@Math.Round((double)user.VaultStorageInKb / 1024, 1) MB</SortableTableColumn>
<SortableTableColumn>@user.LastVaultUpdate.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>
@if (user.Blocked)
{
<StatusPill Enabled="false" TextFalse="Blocked" />
}
@if (user.TwoFactorEnabled)
{
<StatusPill Enabled="true" TextTrue="2FA enabled" />
}
</SortableTableColumn>
<SortableTableColumn>
<LinkButton Color="primary" Href="@($"users/{user.Id}")" Text="View" />
</SortableTableColumn>
</SortableTableRow>
}
</SortableTable>
</div>
}
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "Registered", PropertyName = "CreatedAt" },
new TableColumn { Title = "Username", PropertyName = "UserName" },
new TableColumn { Title = "# Vaults", PropertyName = "VaultCount" },
new TableColumn { Title = "# Credentials", PropertyName = "CredentialCount" },
new TableColumn { Title = "# Email claims", PropertyName = "EmailClaimCount" },
new TableColumn { Title = "# Received emails", PropertyName = "ReceivedEmailCount" },
new TableColumn { Title = "Storage", PropertyName = "VaultStorageInKb" },
new TableColumn { Title = "LastVaultUpdate", PropertyName = "LastVaultUpdate" },
new TableColumn { Title = "Status", Sortable = false },
new TableColumn { Title = "Actions", Sortable = false},
];
private List<UserViewModel> UserList { get; set; } = [];
private bool IsInitialized { get; set; } = false;
private bool IsLoading { get; set; } = true;
private int CurrentPage { get; set; } = 1;
private int PageSize { get; set; } = 50;
private int TotalRecords { get; set; }
private string _searchTerm = string.Empty;
/// <summary>
/// The last search term.
/// </summary>
private string _lastSearchTerm = string.Empty;
private string SearchTerm
{
get => _searchTerm;
set
{
if (_searchTerm != value)
{
_searchTerm = value;
_ = RefreshData();
}
}
}
private string SortColumn { get; set; } = "CreatedAt";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await RefreshData();
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
_ = RefreshData();
}
private async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
IQueryable<AliasVaultUser> 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
{
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();
}
/// <summary>
/// Apply search filter to the query.
/// </summary>
private IQueryable<AliasVaultUser> ApplySearchFilter(IQueryable<AliasVaultUser> query)
{
if (SearchTerm.Length > 0)
{
// Reset page number back to 1 if the search term has changed.
if (SearchTerm != _lastSearchTerm && CurrentPage != 1)
{
CurrentPage = 1;
}
_lastSearchTerm = SearchTerm;
var searchTerm = SearchTerm.Trim().ToLower();
query = query.Where(x => EF.Functions.Like(x.UserName!.ToLower(), "%" + searchTerm + "%"));
}
return query;
}
/// <summary>
/// Apply sort to the query.
/// </summary>
private IQueryable<AliasVaultUser> ApplySort(IQueryable<AliasVaultUser> query, AliasServerDbContext dbContext)
{
// Apply sort.
switch (SortColumn)
{
case "Id":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
case "CreatedAt":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.CreatedAt)
: query.OrderByDescending(x => x.CreatedAt);
break;
case "UserName":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.UserName)
: query.OrderByDescending(x => x.UserName);
break;
case "VaultCount":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Vaults.Count)
: query.OrderByDescending(x => x.Vaults.Count);
break;
case "CredentialCount":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialsCount)
: query.OrderByDescending(x => x.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialsCount);
break;
case "EmailClaimCount":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.EmailClaims.Count)
: query.OrderByDescending(x => x.EmailClaims.Count);
break;
case "ReceivedEmailCount":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count())
: query.OrderByDescending(x => x.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count());
break;
case "VaultStorageInKb":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Vaults.Sum(v => v.FileSize))
: query.OrderByDescending(x => x.Vaults.Sum(v => v.FileSize));
break;
case "LastVaultUpdate":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Vaults.Max(v => v.CreatedAt))
: query.OrderByDescending(x => x.Vaults.Max(v => v.CreatedAt));
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
}
return query;
}
}