diff --git a/src/Pages/AuditTrail.razor b/src/Pages/AuditTrail.razor new file mode 100644 index 00000000..91ff5461 --- /dev/null +++ b/src/Pages/AuditTrail.razor @@ -0,0 +1,433 @@ +@page "/audittrail" +@using System.Security.Claims +@using Humanizer +@using NodeGuard.Data.Models +@using NodeGuard.Data.Repositories.Interfaces +@attribute [Authorize] + +Audit Trail +

Audit Trail

+ + + + Audit logs are stored in the database with a retention period of @Constants.AUDIT_LOG_RETENTION_DAYS days. + Logs older than this will be automatically cleaned up. + + + +@if (!_hasFullAccess) +{ + + + You are viewing your own audit log entries only. FinanceManager and Superadmin roles have access to all audit logs. + + +} + + + + + Action Type + + + + + + Event Type + + + + + + Object Type + + + + @if (_hasFullAccess) + { + + + User + + + + } + + + From Date + + + + + + To Date + + + + + + + + + + + + + + + + + @context.Timestamp.Humanize() + + + + + + + @context.ActionType.Humanize() + + + + + + + @context.EventType.Humanize() + + + + + + @if (!string.IsNullOrEmpty(context.Username)) + { + + @context.Username + + } + else + { + System + } + + + + + @if (!string.IsNullOrEmpty(context.IpAddress)) + { + @context.IpAddress + } + else + { + N/A + } + + + + + @context.ObjectAffected.Humanize() + + + + + @if (!string.IsNullOrEmpty(context.ObjectId)) + { + + @StringHelper.TruncateTail(context.ObjectId, 15) + + } + else + { + N/A + } + + + + + @if (!string.IsNullOrEmpty(context.Details)) + { + + } + else + { + - + } + + + + +
+ No audit log entries found. +
+
+ +
+ +
+
+
+
+
+ + + + + Audit Log Details + + + + @if (_selectedAuditLog != null) + { + + + + Timestamp + @_selectedAuditLog.Timestamp.ToString("yyyy-MM-dd HH:mm:ss zzz") + + + Action + @_selectedAuditLog.ActionType.Humanize() + + + Result + @_selectedAuditLog.EventType.Humanize() + + + User + @(_selectedAuditLog.Username ?? "System") + + + User ID + @(_selectedAuditLog.UserId ?? "N/A") + + + IP Address + @(_selectedAuditLog.IpAddress ?? "N/A") + + + Object Type + @_selectedAuditLog.ObjectAffected.Humanize() + + + Object ID + @(_selectedAuditLog.ObjectId ?? "N/A") + + +
+ @if (!string.IsNullOrEmpty(_selectedAuditLog.Details)) + { +
Additional Details (JSON)
+
@FormatJson(_selectedAuditLog.Details)
+ } + } +
+ + + +
+
+ +@inject IAuditLogRepository AuditLogRepository +@inject IApplicationUserRepository ApplicationUserRepository +@inject IToastService ToastService + +@code { + [CascadingParameter] + private ApplicationUser? LoggedUser { get; set; } + + [CascadingParameter] + private ClaimsPrincipal? ClaimsPrincipal { get; set; } + + private DataGrid? _auditLogDataGrid; + private List _auditLogs = new(); + private int _totalItems; + private bool _hasFullAccess = false; + private List _users = new(); + + // Filters + private AuditActionType? _actionTypeFilter; + private AuditEventType? _eventTypeFilter; + private AuditObjectType? _objectTypeFilter; + private string? _userIdFilter; + private DateTime? _fromDate; + private DateTime? _toDate; + + // Details modal + private Modal? _detailsModal; + private AuditLog? _selectedAuditLog; + + protected override async Task OnInitializedAsync() + { + if (LoggedUser == null) return; + + // Check if user has full access (FinanceManager or Superadmin) + _hasFullAccess = ClaimsPrincipal != null && + (ClaimsPrincipal.IsInRole(ApplicationUserRole.FinanceManager.ToString()) || + ClaimsPrincipal.IsInRole(ApplicationUserRole.Superadmin.ToString())); + + // Load users list for filter if user has full access + if (_hasFullAccess) + { + _users = await ApplicationUserRepository.GetAll(); + } + } + + private async Task OnReadData(DataGridReadDataEventArgs e) + { + if (!e.CancellationToken.IsCancellationRequested) + { + var fromDateOffset = _fromDate.HasValue ? new DateTimeOffset(_fromDate.Value, TimeSpan.Zero) : (DateTimeOffset?)null; + var toDateOffset = _toDate.HasValue ? new DateTimeOffset(_toDate.Value.AddDays(1).AddSeconds(-1), TimeSpan.Zero) : (DateTimeOffset?)null; + + // If user doesn't have full access, filter by their user ID + // If user has full access, use the selected user filter (if any) + var userIdFilter = _hasFullAccess ? _userIdFilter : LoggedUser?.Id; + + var (logs, totalCount) = await AuditLogRepository.GetPaginatedAsync( + e.Page, + e.PageSize, + _actionTypeFilter, + _eventTypeFilter, + _objectTypeFilter, + userIdFilter, + fromDateOffset, + toDateOffset); + + _auditLogs = logs; + _totalItems = totalCount; + } + } + + private async Task ApplyFilters() + { + if (_auditLogDataGrid != null) + { + await _auditLogDataGrid.Reload(); + } + } + + private async Task ClearFilters() + { + _actionTypeFilter = null; + _eventTypeFilter = null; + _objectTypeFilter = null; + _userIdFilter = null; + _fromDate = null; + _toDate = null; + + if (_auditLogDataGrid != null) + { + await _auditLogDataGrid.Reload(); + } + } + + private async Task ShowDetailsModal(AuditLog auditLog) + { + _selectedAuditLog = auditLog; + if (_detailsModal != null) + { + await _detailsModal.Show(); + } + } + + private async Task CloseDetailsModal() + { + _selectedAuditLog = null; + if (_detailsModal != null) + { + await _detailsModal.Hide(); + } + } + + private static Color GetActionBadgeColor(AuditActionType actionType) + { + return actionType switch + { + AuditActionType.Create => Color.Success, + AuditActionType.Update => Color.Info, + AuditActionType.Delete => Color.Danger, + AuditActionType.Approve => Color.Success, + AuditActionType.Reject => Color.Warning, + AuditActionType.Cancel => Color.Secondary, + AuditActionType.Login => Color.Primary, + AuditActionType.Logout => Color.Secondary, + AuditActionType.TwoFactorLogin => Color.Primary, + AuditActionType.LoginWithRecoveryCode => Color.Warning, + AuditActionType.TwoFactorEnabled => Color.Success, + AuditActionType.TwoFactorDisabled => Color.Warning, + AuditActionType.TwoFactorReset => Color.Warning, + AuditActionType.LockUser => Color.Danger, + AuditActionType.UnlockUser => Color.Success, + AuditActionType.Transfer => Color.Info, + AuditActionType.Close => Color.Warning, + AuditActionType.ForceClose => Color.Danger, + AuditActionType.Block => Color.Danger, + AuditActionType.Unblock => Color.Success, + _ => Color.Light + }; + } + + private static Color GetEventBadgeColor(AuditEventType eventType) + { + return eventType switch + { + AuditEventType.Success => Color.Success, + AuditEventType.Failure => Color.Danger, + AuditEventType.Attempt => Color.Warning, + _ => Color.Light + }; + } + + private static string FormatJson(string json) + { + try + { + var element = System.Text.Json.JsonSerializer.Deserialize(json); + return System.Text.Json.JsonSerializer.Serialize(element, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); + } + catch + { + return json; + } + } +}