Files
aliasvault/apps/server/AliasVault.Admin/Main/Pages/Logging/Auth.razor
2025-04-30 19:03:18 +02:00

264 lines
9.4 KiB
Plaintext

@page "/logging/auth"
@using AliasVault.RazorComponents.Tables
@using AliasVault.Shared.Models.Enums
@inherits MainBase
<LayoutPageTitle>Auth logs</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@(TotalRecords > 0 ? $"Auth logs ({TotalRecords:N0})" : "Auth logs")"
Description="This page shows an overview of recent auth attempts.">
<CustomActions>
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<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 flex space-x-4">
<div class="flex w-full">
<div class="w-2/3 pr-2">
<div class="relative">
<SearchIcon />
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." 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 class="w-1/3 pl-2">
<select @bind="SelectedEventType" class="w-full px-4 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">
<option value="">All event types</option>
@foreach (var eventType in Enum.GetValues<AuthEventType>())
{
<option value="@eventType">@eventType</option>
}
</select>
</div>
</div>
</div>
</div>
}
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
<div class="px-4">
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var log in LogList)
{
<SortableTableRow>
<SortableTableColumn IsPrimary="true">@log.Id</SortableTableColumn>
<SortableTableColumn>@log.Timestamp.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@log.Username</SortableTableColumn>
<SortableTableColumn>@log.Client</SortableTableColumn>
<SortableTableColumn>@log.EventType</SortableTableColumn>
<SortableTableColumn><StatusPill Enabled="log.IsSuccess" TextTrue="Success" TextFalse="@log.FailureReason.ToString()" /></SortableTableColumn>
<SortableTableColumn>@log.IpAddress</SortableTableColumn>
</SortableTableRow>
}
</SortableTable>
</div>
}
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
new TableColumn { Title = "Username", PropertyName = "Username" },
new TableColumn { Title = "Client", PropertyName = "Client" },
new TableColumn { Title = "Event", PropertyName = "EventType" },
new TableColumn { Title = "Success", PropertyName = "IsSuccess" },
new TableColumn { Title = "IP", PropertyName = "IpAddress" },
];
private List<AuthLog> LogList { 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;
private string _lastSearchTerm = string.Empty;
private string SearchTerm
{
get => _searchTerm;
set
{
if (_searchTerm != value)
{
_searchTerm = value;
_ = RefreshData();
}
}
}
private string _selectedEventType = string.Empty;
private string SelectedEventType
{
get => _selectedEventType;
set
{
if (_selectedEventType != value)
{
_selectedEventType = value;
_ = RefreshData();
}
}
}
private string SortColumn { get; set; } = "Id";
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();
var query = dbContext.AuthLogs.AsQueryable();
if (!string.IsNullOrEmpty(SearchTerm))
{
// 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<AuthEventType>(SelectedEventType, out var eventType);
if (success)
{
query = query.Where(x => x.EventType == eventType);
}
}
query = ApplySort(query);
TotalRecords = await query.CountAsync();
LogList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
/// <summary>
/// Apply sort to the query.
/// </summary>
private IQueryable<AuthLog> ApplySort(IQueryable<AuthLog> query)
{
// Apply sort.
switch (SortColumn)
{
case "Timestamp":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Timestamp)
: query.OrderByDescending(x => x.Timestamp);
break;
case "Username":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Username)
: query.OrderByDescending(x => x.Username);
break;
case "Client":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Client)
: query.OrderByDescending(x => x.Client);
break;
case "EventType":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.EventType)
: query.OrderByDescending(x => x.EventType);
break;
case "IsSuccess":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.IsSuccess)
: query.OrderByDescending(x => x.IsSuccess);
break;
case "IpAddress":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.IpAddress)
: query.OrderByDescending(x => x.IpAddress);
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
}
return query;
}
private async Task DeleteLogsWithConfirmation()
{
if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone."))
{
await DeleteLogs();
}
}
private async Task DeleteLogs()
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.AuthLogs.RemoveRange(dbContext.AuthLogs);
await dbContext.SaveChangesAsync();
await RefreshData();
IsLoading = false;
StateHasChanged();
}
}