diff --git a/src/Data/Repositories/ChannelOperationRequestRepository.cs b/src/Data/Repositories/ChannelOperationRequestRepository.cs index e6f7de72..a98bfb0d 100644 --- a/src/Data/Repositories/ChannelOperationRequestRepository.cs +++ b/src/Data/Repositories/ChannelOperationRequestRepository.cs @@ -69,6 +69,7 @@ public async Task> GetAll() .Include(request => request.SourceNode) .Include(request => request.DestNode) .Include(request => request.ChannelOperationRequestPsbts) + .Include(request => request.User) .Include(x => x.Utxos) .AsSplitQuery() .ToListAsync(); @@ -84,6 +85,7 @@ public async Task> GetUnsignedPendingRequestsByUse .Include(x => x.Wallet).ThenInclude(x => x.Keys) .Include(request => request.DestNode) .Include(request => request.ChannelOperationRequestPsbts) + .Include(request => request.User) .Include(x => x.Utxos) .Where(request => request.Wallet != null && request.Wallet.Keys.Count(key => userId == key.UserId) > request.ChannelOperationRequestPsbts.Count(req => req.UserSignerId == userId) diff --git a/src/Pages/Apis.razor b/src/Pages/Apis.razor index c3e9e166..c3584808 100644 --- a/src/Pages/Apis.razor +++ b/src/Pages/Apis.razor @@ -98,6 +98,7 @@ @inject IAPITokenRepository APITokenRepository @inject IToastService ToastService @inject ILocalStorageService LocalStorageService +@inject IAuditService AuditService @attribute [Authorize(Roles = "Superadmin")] @code { @@ -166,12 +167,24 @@ if (addResult.Item1) { await ShowCopyModalToken(arg.Item); + await AuditService.LogAsync( + AuditActionType.Create, + AuditEventType.Success, + AuditObjectType.APIToken, + arg.Item.Id.ToString(), + new { Name = arg.Item.Name }); } else { ToastService.ShowError("Something went wrong"); _apiTokens.Remove(arg.Item); + await AuditService.LogAsync( + AuditActionType.Create, + AuditEventType.Failure, + AuditObjectType.APIToken, + null, + new { Name = arg.Item.Name, Error = addResult.Item2 }); } } @@ -215,11 +228,22 @@ if (result) { ToastService.ShowSuccess(blockIt ? "Token blocked" : "Token unblocked"); + await AuditService.LogAsync( + blockIt ? AuditActionType.Block : AuditActionType.Unblock, + AuditEventType.Success, + AuditObjectType.APIToken, + contextItem.Id.ToString(), + new { Name = contextItem.Name }); } else { ToastService.ShowError("Something went wrong"); - + await AuditService.LogAsync( + blockIt ? AuditActionType.Block : AuditActionType.Unblock, + AuditEventType.Failure, + AuditObjectType.APIToken, + contextItem.Id.ToString(), + new { Name = contextItem.Name }); } await GetData(); diff --git a/src/Pages/ChannelRequests.razor b/src/Pages/ChannelRequests.razor index 624b7aeb..92e3dd82 100644 --- a/src/Pages/ChannelRequests.razor +++ b/src/Pages/ChannelRequests.razor @@ -226,6 +226,11 @@ } + + + @(context?.User?.UserName ?? "N/A") + + @context?.Status.Humanize() @@ -338,6 +343,11 @@ } + + + @(context?.User?.UserName ?? "N/A") + + @@ -443,6 +453,8 @@ @inject INBXplorerService NBXplorerService @inject ILocalStorageService LocalStorageService @inject IPriceConversionService PriceConversionService +@inject IAuditService AuditService +@inject IHttpContextAccessor HttpContextAccessor @code { private List? _channelRequests; @@ -514,6 +526,7 @@ public static readonly ColumnDefault Private = new("Private"); public static readonly ColumnDefault SignaturesCollected = new("Signatures Collected"); public static readonly ColumnDefault Status = new("Status"); + public static readonly ColumnDefault Requestor = new("Requestor"); } public abstract class AllChannelsColumnName @@ -526,6 +539,7 @@ public static readonly ColumnDefault Private = new("Private"); public static readonly ColumnDefault SignaturesCollected = new("Signatures Collected"); public static readonly ColumnDefault Status = new("Status"); + public static readonly ColumnDefault Requestor = new("Requestor"); public static readonly ColumnDefault CreationDate = new("Creation Date"); public static readonly ColumnDefault UpdateDate = new("Update Date"); public static readonly ColumnDefault Links = new("Links"); @@ -705,6 +719,15 @@ if (createChannelResult.Item1) { ToastService.ShowSuccess("Open channel request created!"); + await AuditService.LogAsync( + AuditActionType.Create, + AuditEventType.Success, + AuditObjectType.ChannelOperationRequest, + request.Id.ToString(), + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { Amount = amount, SourceNodeId = _selectedSourceNodeId, DestNodeId = _selectedDestNode?.Id, Description = $"Channel open request created. Amount: {amount} BTC, SourceNodeId: {_selectedSourceNodeId}, DestNodeId: {_selectedDestNode?.Id}" }); if (_selectedUTXOs.Count > 0) { @@ -714,6 +737,15 @@ else { ToastService.ShowError(createChannelResult.Item2); + await AuditService.LogAsync( + AuditActionType.Create, + AuditEventType.Failure, + AuditObjectType.ChannelOperationRequest, + null, + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { Error = createChannelResult.Item2, Description = $"Failed to create channel open request. Error: {createChannelResult.Item2}" }); } _utxoSelectorModalRef.ClearModal(); } @@ -765,10 +797,30 @@ if (!jobUpdateResult.Item1) { ToastService.ShowError("There has been an error when updating the request"); + var failActionType = _selectedStatus == ChannelOperationRequestStatus.Rejected ? AuditActionType.Reject : AuditActionType.Cancel; + await AuditService.LogAsync( + failActionType, + AuditEventType.Failure, + AuditObjectType.ChannelOperationRequest, + _selectedRequest.Id.ToString(), + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { RequestId = _selectedRequest.Id, Status = _selectedStatus, Description = $"Failed to {_selectedStatus.ToString().ToLower()} channel operation request" }); } else { ToastService.ShowSuccess("Request " + _selectedStatus); + var successActionType = _selectedStatus == ChannelOperationRequestStatus.Rejected ? AuditActionType.Reject : AuditActionType.Cancel; + await AuditService.LogAsync( + successActionType, + AuditEventType.Success, + AuditObjectType.ChannelOperationRequest, + _selectedRequest.Id.ToString(), + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { RequestId = _selectedRequest.Id, Status = _selectedStatus, Reason = _selectedRequest.ClosingReason, Description = $"Channel operation request {_selectedStatus.ToString().ToLower()}" }); await FetchRequests(); } } @@ -835,6 +887,15 @@ if (addResult.Item1) { ToastService.ShowSuccess("Signature collected"); + await AuditService.LogAsync( + AuditActionType.Sign, + AuditEventType.Success, + AuditObjectType.ChannelOperationRequest, + _selectedRequest.Id.ToString(), + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { RequestId = _selectedRequest.Id, Description = "PSBT signature collected for channel operation request" }); _selectedRequest = await ChannelOperationRequestRepository.GetById(_selectedRequest.Id); @@ -847,6 +908,15 @@ else { ToastService.ShowError("Error while saving the signature"); + await AuditService.LogAsync( + AuditActionType.Sign, + AuditEventType.Failure, + AuditObjectType.ChannelOperationRequest, + _selectedRequest.Id.ToString(), + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { RequestId = _selectedRequest.Id, Description = "Failed to save PSBT signature for channel operation request" }); } await FetchRequests(); @@ -880,10 +950,28 @@ if (createChannelResult.Item1) { ToastService.ShowSuccess("Open channel request created!"); + await AuditService.LogAsync( + AuditActionType.Create, + AuditEventType.Success, + AuditObjectType.ChannelOperationRequest, + _selectedRequest.Id.ToString(), + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { Amount = new Money(_selectedRequest.SatsAmount).ToUnit(MoneyUnit.BTC), SourceNodeId = _selectedRequest.SourceNodeId, DestNodeId = _selectedRequest.DestNodeId, IsHotWallet = true, Description = "Hot wallet channel open request created" }); } else { ToastService.ShowError(createChannelResult.Item2); + await AuditService.LogAsync( + AuditActionType.Create, + AuditEventType.Failure, + AuditObjectType.ChannelOperationRequest, + null, + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { Error = createChannelResult.Item2, IsHotWallet = true, Description = "Failed to create hot wallet channel open request" }); _utxoSelectorModalRef.ClearModal(); await _approveOperationConfirmationModal.CloseModal(); return; @@ -977,10 +1065,28 @@ var scheduler = await SchedulerFactory.GetScheduler(); await scheduler.DeleteJob(new JobKey($"{nameof(ChannelOpenJob)}-{request.Id}")); if (!ChannelOperationRequestRepository.Update(request).Item1) throw new Exception(); + await AuditService.LogAsync( + AuditActionType.Update, + AuditEventType.Success, + AuditObjectType.ChannelOperationRequest, + request.Id.ToString(), + LoggedUser.Id, + LoggedUser.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { RequestId = request.Id, NewStatus = ChannelOperationRequestStatus.Failed, Description = "Channel operation request marked as failed" }); } catch (Exception? e) { ToastService.ShowError("Error while marking request as failed"); + await AuditService.LogAsync( + AuditActionType.Update, + AuditEventType.Failure, + AuditObjectType.ChannelOperationRequest, + _selectedRequestForMarkingAsFailed?.Id.ToString(), + LoggedUser?.Id, + LoggedUser?.UserName, + HttpContextAccessor.HttpContext?.GetClientIpAddress(), + new { RequestId = _selectedRequestForMarkingAsFailed?.Id, Description = "Failed to mark channel operation request as failed" }); } finally { diff --git a/src/Pages/Channels.razor b/src/Pages/Channels.razor index c101038c..4764adbb 100644 --- a/src/Pages/Channels.razor +++ b/src/Pages/Channels.razor @@ -386,6 +386,7 @@ @inject INodeRepository NodeRepository @inject ILocalStorageService LocalStorageService @inject IPriceConversionService PriceConversionService +@inject IAuditService AuditService @code { private List? _channels = new List(); @@ -499,13 +500,20 @@ channel.SourceNode = await NodeRepository.GetById(channel.SourceNodeId) ?? throw new InvalidOperationException(); var result = await ChannelRepository.SafeRemove(channel, forceClose); + var actionType = forceClose ? AuditActionType.ForceClose : AuditActionType.Close; if (!result.Item1) { ToastService.ShowError("Something went wrong"); + await AuditService.LogAsync(actionType, AuditEventType.Failure, AuditObjectType.Channel, + channel.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Failed to {(forceClose ? "force " : "")}close channel. ChanId: {channel.ChanId}"); } else { ToastService.ShowSuccess("Channel closed successfully"); + await AuditService.LogAsync(actionType, AuditEventType.Success, AuditObjectType.Channel, + channel.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Channel {(forceClose ? "force " : "")}closed. ChanId: {channel.ChanId}"); await FetchData(); } } @@ -606,10 +614,16 @@ if (!updateResult.Item1) { ToastService.ShowError("Something went wrong"); + await AuditService.LogAsync(AuditActionType.Update, AuditEventType.Failure, AuditObjectType.Channel, + _selectedChannel.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Failed to update channel. ChanId: {_selectedChannel.ChanId}"); } else { ToastService.ShowSuccess("Channel updated successfully"); + await AuditService.LogAsync(AuditActionType.Update, AuditEventType.Success, AuditObjectType.Channel, + _selectedChannel.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Channel updated. ChanId: {_selectedChannel.ChanId}"); } } @@ -622,10 +636,16 @@ if (!liquidityRuleResult.Item1) { ToastService.ShowError("Something went wrong"); + await AuditService.LogAsync(AuditActionType.Create, AuditEventType.Failure, AuditObjectType.LiquidityRule, + _currentLiquidityRule.ChannelId.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Failed to add liquidity rule for channel. ChannelId: {_currentLiquidityRule.ChannelId}"); } else { ToastService.ShowSuccess("Liquidity rule added successfully"); + await AuditService.LogAsync(AuditActionType.Create, AuditEventType.Success, AuditObjectType.LiquidityRule, + _currentLiquidityRule.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Liquidity rule created. ChannelId: {_currentLiquidityRule.ChannelId}, MinLocal: {_currentLiquidityRule.MinimumLocalBalance}%, MinRemote: {_currentLiquidityRule.MinimumRemoteBalance}%, Target: {_currentLiquidityRule.RebalanceTarget}%"); } } else @@ -634,10 +654,16 @@ if (!liquidityRuleResult.Item1) { ToastService.ShowError("Something went wrong"); + await AuditService.LogAsync(AuditActionType.Update, AuditEventType.Failure, AuditObjectType.LiquidityRule, + _currentLiquidityRule.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Failed to update liquidity rule. RuleId: {_currentLiquidityRule.Id}"); } else { ToastService.ShowSuccess("Liquidity rule updated successfully"); + await AuditService.LogAsync(AuditActionType.Update, AuditEventType.Success, AuditObjectType.LiquidityRule, + _currentLiquidityRule.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Liquidity rule updated. ChannelId: {_currentLiquidityRule.ChannelId}, MinLocal: {_currentLiquidityRule.MinimumLocalBalance}%, MinRemote: {_currentLiquidityRule.MinimumRemoteBalance}%, Target: {_currentLiquidityRule.RebalanceTarget}%"); } } } @@ -703,14 +729,21 @@ } var updateResult = ChannelRepository.Update(_selectedChannel); + var actionType = enabledLiquidityMngmt ? AuditActionType.EnableLiquidityManagement : AuditActionType.DisableLiquidityManagement; if (!updateResult.Item1) { ToastService.ShowError("Something went wrong"); + await AuditService.LogAsync(actionType, AuditEventType.Failure, AuditObjectType.Channel, + _selectedChannel.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Failed to {(enabledLiquidityMngmt ? "enable" : "disable")} liquidity management. ChanId: {_selectedChannel.ChanId}"); } else { ToastService.ShowSuccess("Channel updated successfully"); + await AuditService.LogAsync(actionType, AuditEventType.Success, AuditObjectType.Channel, + _selectedChannel.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Liquidity management {(enabledLiquidityMngmt ? "enabled" : "disabled")}. ChanId: {_selectedChannel.ChanId}"); } } } @@ -874,10 +907,16 @@ if (result.Item1) { ToastService.ShowSuccess("Channel marked as closed"); + await AuditService.LogAsync(AuditActionType.MarkAsClosed, AuditEventType.Success, AuditObjectType.Channel, + contextItem.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Channel marked as closed. ChanId: {contextItem.ChanId}"); } else { ToastService.ShowError("Something went wrong: " + result.Item2); + await AuditService.LogAsync(AuditActionType.MarkAsClosed, AuditEventType.Failure, AuditObjectType.Channel, + contextItem.Id.ToString(), LoggedUser?.Id, LoggedUser?.UserName, null, + $"Failed to mark channel as closed. ChanId: {contextItem.ChanId}. Error: {result.Item2}"); } await FetchData(); diff --git a/src/Pages/Nodes.razor b/src/Pages/Nodes.razor index 083473f8..0ab51323 100644 --- a/src/Pages/Nodes.razor +++ b/src/Pages/Nodes.razor @@ -5,6 +5,7 @@ @using Humanizer @using Quartz +@inject IAuditService AuditService @attribute [Authorize(Roles = "FinanceManager, NodeManager, Superadmin")] Active Nodes

Nodes

@@ -154,17 +155,6 @@ }
- - - @{ - IEnumerable associatedChannels = context?.ChannelOperationRequestsAsDestination? - .Where(request => request.ChannelId != null) - .Select(channel => channel.ChannelId) - .Distinct(); - @($"{associatedChannels?.Count()}") - } - - @@ -620,11 +610,29 @@ ToastService.ShowSuccess($"Node {arg.Item.Name} Created"); _nodes = await NodeRepository.GetAll(); await CreateJobs(arg.Item); + await AuditService.LogAsync( + AuditActionType.AddNode, + AuditEventType.Success, + AuditObjectType.Node, + arg.Item.Id.ToString(), + LoggedUser?.Id, + LoggedUser?.UserName, + null, + new { Name = arg.Item.Name, PubKey = arg.Item.PubKey }); } else { ToastService.ShowError("Something went wrong"); _nodes?.Remove(arg.Item); + await AuditService.LogAsync( + AuditActionType.AddNode, + AuditEventType.Failure, + AuditObjectType.Node, + null, + LoggedUser?.Id, + LoggedUser?.UserName, + null, + new { Name = arg.Item.Name, PubKey = arg.Item.PubKey, Error = addResult.Item2 }); } } await GetData(); @@ -643,10 +651,28 @@ if (updateResult.Item1) { ToastService.ShowSuccess($"Node {arg.Item.Name} Updated"); + await AuditService.LogAsync( + AuditActionType.UpdateNode, + AuditEventType.Success, + AuditObjectType.Node, + arg.Item.Id.ToString(), + LoggedUser?.Id, + LoggedUser?.UserName, + null, + new { Name = arg.Item.Name, PubKey = arg.Item.PubKey }); } else { ToastService.ShowError("Something went wrong"); + await AuditService.LogAsync( + AuditActionType.UpdateNode, + AuditEventType.Failure, + AuditObjectType.Node, + arg.Item.Id.ToString(), + LoggedUser?.Id, + LoggedUser?.UserName, + null, + new { Name = arg.Item.Name, PubKey = arg.Item.PubKey, Error = updateResult.Item2 }); } } await GetData(); @@ -691,6 +717,15 @@ else { ToastService.ShowSuccess($"{node.Name} deleted successfully"); + await AuditService.LogAsync( + AuditActionType.DeleteNode, + AuditEventType.Success, + AuditObjectType.Node, + node.Id.ToString(), + LoggedUser?.Id, + LoggedUser?.UserName, + null, + new { NodeName = node.Name, PubKey = node.PubKey }); _nodes = await NodeRepository.GetAll(); }