mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-20 07:54:10 -05:00
426 lines
16 KiB
Plaintext
426 lines
16 KiB
Plaintext
@page "/email-storage-stats"
|
|
@inherits MainBase
|
|
@using AliasVault.Admin.Services
|
|
@using AliasVault.RazorComponents
|
|
@using AliasVault.RazorComponents.Tables
|
|
@using Microsoft.EntityFrameworkCore
|
|
@inject StatisticsService StatisticsService
|
|
@inject IAliasServerDbContextFactory ContextFactory
|
|
@implements IDisposable
|
|
|
|
<PageHeader Title="Email Storage Stats" BreadcrumbItems="BreadcrumbItems">
|
|
<CustomActions>
|
|
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
|
</CustomActions>
|
|
</PageHeader>
|
|
|
|
<div class="px-4 space-y-6">
|
|
<!-- Overview Statistics Cards -->
|
|
@if (LoadingStats)
|
|
{
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
@for (int i = 0; i < 3; i++)
|
|
{
|
|
<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">
|
|
<div class="ml-4">
|
|
<div class="w-32 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2"></div>
|
|
<div class="w-16 h-6 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<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">
|
|
<div class="ml-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Total Emails</h3>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">@TotalEmails.ToString("N0")</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<div class="ml-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Email Attachments</h3>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">@TotalAttachments.ToString("N0")</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<div class="ml-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Storage Used</h3>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">@TotalStorageMB.ToString("N2") MB</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Email Attachments Table -->
|
|
<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">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Email Attachments</h3>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">All email attachments stored in the system. The content of the attachments is encrypted with the user's public key and are unreadable by the server.</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if (LoadingAttachments)
|
|
{
|
|
<div class="p-4">
|
|
<div class="animate-pulse">
|
|
@for (int i = 0; i < 5; i++)
|
|
{
|
|
<div class="flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="w-8 h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
<div class="w-32 h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
<div class="w-16 h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
<div class="w-24 h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
</div>
|
|
<div class="w-16 h-8 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (Attachments?.Any() == true)
|
|
{
|
|
<div class="overflow-x-auto">
|
|
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
|
@foreach (var attachment in Attachments)
|
|
{
|
|
<SortableTableRow>
|
|
<SortableTableColumn IsPrimary="true">@attachment.Id</SortableTableColumn>
|
|
<SortableTableColumn>
|
|
<span class="font-medium text-gray-900 dark:text-white">@attachment.Filename</span>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">@attachment.MimeType</div>
|
|
</SortableTableColumn>
|
|
<SortableTableColumn>@FormatFileSize(attachment.Filesize)</SortableTableColumn>
|
|
<SortableTableColumn>
|
|
<span>@attachment.EmailId</span>
|
|
</SortableTableColumn>
|
|
<SortableTableColumn>
|
|
@if (!string.IsNullOrEmpty(attachment.UserEmail))
|
|
{
|
|
<a href="users/@attachment.UserId">
|
|
@attachment.UserEmail
|
|
</a>
|
|
}
|
|
else
|
|
{
|
|
<span>Unknown</span>
|
|
}
|
|
</SortableTableColumn>
|
|
<SortableTableColumn>
|
|
<button @onclick="() => DeleteAttachment(attachment.Id)"
|
|
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium">
|
|
Delete
|
|
</button>
|
|
</SortableTableColumn>
|
|
</SortableTableRow>
|
|
}
|
|
</SortableTable>
|
|
</div>
|
|
|
|
<div class="px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
|
<Paginator CurrentPage="CurrentPage"
|
|
PageSize="20"
|
|
TotalRecords="TotalRecords"
|
|
OnPageChanged="HandlePageChanged" />
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="p-8 text-center">
|
|
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-gray-100 dark:bg-gray-700">
|
|
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
|
</svg>
|
|
</div>
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No attachments found</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">No email attachments exist in the system.</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (ErrorMessage != null)
|
|
{
|
|
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Error Loading Data</h3>
|
|
<p class="text-sm text-red-700 dark:text-red-300 mt-1">@ErrorMessage</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
private bool LoadingStats = true;
|
|
private bool LoadingAttachments = true;
|
|
private string? ErrorMessage;
|
|
private bool _disposed = false;
|
|
|
|
private int TotalEmails = 0;
|
|
private int TotalAttachments = 0;
|
|
private double TotalStorageMB = 0;
|
|
|
|
private List<EmailAttachmentModel>? Attachments;
|
|
private int CurrentPage = 1;
|
|
private int TotalRecords = 0;
|
|
|
|
private string SortColumn { get; set; } = "Filesize";
|
|
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
|
|
|
private readonly List<TableColumn> _tableColumns = [
|
|
new TableColumn { Title = "ID", PropertyName = "Id" },
|
|
new TableColumn { Title = "Filename", PropertyName = "Filename" },
|
|
new TableColumn { Title = "File Size", PropertyName = "Filesize" },
|
|
new TableColumn { Title = "Email ID", PropertyName = "EmailId" },
|
|
new TableColumn { Title = "User", PropertyName = "UserEmail" },
|
|
new TableColumn { Title = "Actions", Sortable = false },
|
|
];
|
|
|
|
public class EmailAttachmentModel
|
|
{
|
|
public int Id { get; set; }
|
|
public string Filename { get; set; } = string.Empty;
|
|
public string MimeType { get; set; } = string.Empty;
|
|
public int Filesize { get; set; }
|
|
public int EmailId { get; set; }
|
|
public string? UserId { get; set; }
|
|
public string? UserEmail { get; set; }
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Emails", Url = "emails" });
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Email Storage Stats" });
|
|
|
|
await RefreshData();
|
|
}
|
|
|
|
private void SafeStateHasChanged()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
private async Task RefreshData()
|
|
{
|
|
if (_disposed) return;
|
|
|
|
LoadingStats = true;
|
|
LoadingAttachments = true;
|
|
ErrorMessage = null;
|
|
SafeStateHasChanged();
|
|
|
|
try
|
|
{
|
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
|
|
// Load statistics
|
|
TotalEmails = await context.Emails.CountAsync();
|
|
TotalAttachments = await context.EmailAttachments.CountAsync();
|
|
|
|
var totalBytes = TotalAttachments > 0
|
|
? await context.EmailAttachments.SumAsync(a => (long)a.Filesize)
|
|
: 0L;
|
|
TotalStorageMB = totalBytes / (1024.0 * 1024.0);
|
|
|
|
LoadingStats = false;
|
|
SafeStateHasChanged();
|
|
|
|
// Load attachments
|
|
await LoadAttachments();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
ErrorMessage = $"Error loading data: {ex.Message}";
|
|
LoadingStats = false;
|
|
LoadingAttachments = false;
|
|
SafeStateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task LoadAttachments()
|
|
{
|
|
if (_disposed) return;
|
|
|
|
LoadingAttachments = true;
|
|
SafeStateHasChanged();
|
|
|
|
try
|
|
{
|
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
|
|
TotalRecords = await context.EmailAttachments.CountAsync();
|
|
|
|
var skip = (CurrentPage - 1) * 20;
|
|
|
|
// Apply sorting and pagination
|
|
var sortedQuery = ApplySort(context.EmailAttachments);
|
|
|
|
var attachmentData = await sortedQuery
|
|
.Skip(skip)
|
|
.Take(20)
|
|
.ToListAsync();
|
|
|
|
// Then map to view models to avoid disposed context issues
|
|
if (!_disposed)
|
|
{
|
|
Attachments = attachmentData.Select(a => new EmailAttachmentModel
|
|
{
|
|
Id = a.Id,
|
|
Filename = a.Filename,
|
|
MimeType = a.MimeType,
|
|
Filesize = a.Filesize,
|
|
EmailId = a.EmailId,
|
|
UserId = a.Email?.EncryptionKey?.UserId,
|
|
UserEmail = a.Email?.EncryptionKey?.User?.UserName
|
|
}).ToList();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
ErrorMessage = $"Error loading attachments: {ex.Message}";
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
LoadingAttachments = false;
|
|
SafeStateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task HandlePageChanged(int newPage)
|
|
{
|
|
CurrentPage = newPage;
|
|
await LoadAttachments();
|
|
}
|
|
|
|
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
|
{
|
|
SortColumn = sort.column;
|
|
SortDirection = sort.direction;
|
|
CurrentPage = 1; // Reset to first page when sorting changes
|
|
await LoadAttachments();
|
|
}
|
|
|
|
private IQueryable<EmailAttachment> ApplySort(IQueryable<EmailAttachment> query)
|
|
{
|
|
// Always include navigation properties for all sorting scenarios
|
|
query = query.Include(a => a.Email)
|
|
.ThenInclude(e => e.EncryptionKey)
|
|
.ThenInclude(k => k.User);
|
|
|
|
switch (SortColumn)
|
|
{
|
|
case "Id":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Id)
|
|
: query.OrderByDescending(x => x.Id);
|
|
break;
|
|
case "Filename":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Filename)
|
|
: query.OrderByDescending(x => x.Filename);
|
|
break;
|
|
case "Filesize":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Filesize)
|
|
: query.OrderByDescending(x => x.Filesize);
|
|
break;
|
|
case "EmailId":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.EmailId)
|
|
: query.OrderByDescending(x => x.EmailId);
|
|
break;
|
|
case "UserEmail":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Email.EncryptionKey.User.Email)
|
|
: query.OrderByDescending(x => x.Email.EncryptionKey.User.Email);
|
|
break;
|
|
default:
|
|
query = query.OrderByDescending(x => x.Filesize);
|
|
break;
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
private async Task DeleteAttachment(int attachmentId)
|
|
{
|
|
var confirmed = await ConfirmModalService.ShowConfirmation(
|
|
"Delete Attachment",
|
|
"Are you sure you want to delete this attachment? This action cannot be undone.",
|
|
"Delete",
|
|
"Cancel");
|
|
|
|
if (!confirmed) return;
|
|
|
|
try
|
|
{
|
|
await using var context = await ContextFactory.CreateDbContextAsync();
|
|
|
|
var attachment = await context.EmailAttachments.FindAsync(attachmentId);
|
|
if (attachment != null)
|
|
{
|
|
context.EmailAttachments.Remove(attachment);
|
|
await context.SaveChangesAsync();
|
|
|
|
// Refresh data after deletion
|
|
await RefreshData();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
ErrorMessage = $"Error deleting attachment: {ex.Message}";
|
|
SafeStateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string FormatFileSize(int bytes)
|
|
{
|
|
if (bytes < 1024)
|
|
return $"{bytes} B";
|
|
else if (bytes < 1024 * 1024)
|
|
return $"{bytes / 1024.0:F1} KB";
|
|
else
|
|
return $"{bytes / (1024.0 * 1024.0):F1} MB";
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_disposed = true;
|
|
}
|
|
} |