From 0e277adb3ef05359112f1a3e93808d0a871c6361 Mon Sep 17 00:00:00 2001 From: Nick Krantz Date: Wed, 28 May 2025 15:33:30 -0500 Subject: [PATCH] mark all notifications associated with a security task as deleted when the task is completed --- .../Repositories/INotificationRepository.cs | 9 +++ ...arkNotificationsForTaskAsDeletedCommand.cs | 11 ++++ ...arkNotificationsForTaskAsDeletedCommand.cs | 61 +++++++++++++++++++ .../Commands/MarkTaskAsCompletedCommand.cs | 9 ++- .../Vault/VaultServiceCollectionExtensions.cs | 1 + .../Repositories/NotificationRepository.cs | 17 ++++++ .../Repositories/NotificationRepository.cs | 16 +++++ .../Notification_ReadActiveByTaskId.sql | 12 ++++ ...-28_00_Notification_ReadActiveByTaskId.sql | 12 ++++ 9 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs create mode 100644 src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs create mode 100644 src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadActiveByTaskId.sql create mode 100644 util/Migrator/DbScripts/2025-05-28_00_Notification_ReadActiveByTaskId.sql diff --git a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs index 21604ed169..f710f09c04 100644 --- a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs +++ b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs @@ -32,4 +32,13 @@ public interface INotificationRepository : IRepository /// Task> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions); + + /// + /// Get non-deleted notifications by a task Id. + /// + /// The unique identifier of the task. + /// + /// A collection of notifications associated with the task that are not marked as deleted. + /// + Task> GetActiveByTaskIdAsync(Guid taskId); } diff --git a/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs b/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs new file mode 100644 index 0000000000..90566b4d83 --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface IMarkNotificationsForTaskAsDeletedCommand +{ + /// + /// Marks notifications associated with a given taskId as deleted. + /// + /// The unique identifier of the task to complete + /// A task representing the async operation + Task MarkAsDeletedAsync(Guid taskId); +} diff --git a/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs new file mode 100644 index 0000000000..ffb993a824 --- /dev/null +++ b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs @@ -0,0 +1,61 @@ +using Bit.Core.Context; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; +using Bit.Core.Vault.Commands.Interfaces; + +namespace Bit.Core.Vault.Commands; + +public class MarkNotificationsForTaskAsDeletedCommand : IMarkNotificationsForTaskAsDeletedCommand +{ + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + private readonly ICurrentContext _currentContext; + private readonly IPushNotificationService _pushNotificationService; + + public MarkNotificationsForTaskAsDeletedCommand( + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository, + ICurrentContext currentContext, + IPushNotificationService pushNotificationService) + { + _notificationRepository = notificationRepository; + _notificationStatusRepository = notificationStatusRepository; + _currentContext = currentContext; + _pushNotificationService = pushNotificationService; + + } + + public async Task MarkAsDeletedAsync(Guid taskId) + { + var notifications = await _notificationRepository.GetActiveByTaskIdAsync(taskId); + + foreach (var notification in notifications) + { + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notification.Id, _currentContext.UserId.Value); + + if (notificationStatus == null) + { + notificationStatus = new NotificationStatus + { + NotificationId = notification.Id, + UserId = _currentContext.UserId.Value, + DeletedDate = DateTime.UtcNow + }; + + var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus); + + await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus); + } + else + { + notificationStatus.DeletedDate = DateTime.UtcNow; + + await _notificationStatusRepository.UpdateAsync(notificationStatus); + + await _pushNotificationService.PushNotificationStatusAsync(notification, notificationStatus); + } + + } + } +} diff --git a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs index 77b8a8625c..8a12910bb8 100644 --- a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs +++ b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs @@ -14,15 +14,19 @@ public class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand private readonly ISecurityTaskRepository _securityTaskRepository; private readonly IAuthorizationService _authorizationService; private readonly ICurrentContext _currentContext; + private readonly IMarkNotificationsForTaskAsDeletedCommand _markNotificationsForTaskAsDeletedAsync; + public MarkTaskAsCompletedCommand( ISecurityTaskRepository securityTaskRepository, IAuthorizationService authorizationService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IMarkNotificationsForTaskAsDeletedCommand markNotificationsForTaskAsDeletedAsync) { _securityTaskRepository = securityTaskRepository; _authorizationService = authorizationService; _currentContext = currentContext; + _markNotificationsForTaskAsDeletedAsync = markNotificationsForTaskAsDeletedAsync; } /// @@ -46,5 +50,8 @@ public class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand task.RevisionDate = DateTime.UtcNow; await _securityTaskRepository.ReplaceAsync(task); + + // Mark all notifications related to this task as deleted + await _markNotificationsForTaskAsDeletedAsync.MarkAsDeletedAsync(taskId); } } diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 1f361cb613..9efa1ea379 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -24,5 +24,6 @@ public static class VaultServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs index b6843d9801..0846f7e224 100644 --- a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs @@ -56,4 +56,21 @@ public class NotificationRepository : Repository, INotificat ContinuationToken = data.Count < pageOptions.PageSize ? null : (pageNumber + 1).ToString() }; } + + public async Task> GetActiveByTaskIdAsync(Guid taskId) + { + await using var connection = new SqlConnection(ConnectionString); + + var results = await connection.QueryAsync( + "[dbo].[Notification_ReadActiveByTaskId]", + new + { + TaskId = taskId, + }, + commandType: CommandType.StoredProcedure); + + var data = results.ToList(); + + return data; + } } diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs index 5d1071f26c..463bb1c36d 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs @@ -74,4 +74,20 @@ public class NotificationRepository : Repository> GetActiveByTaskIdAsync(Guid taskId) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + + var dbContext = GetDatabaseContext(scope); + + var query = from n in dbContext.Notifications + join ns in dbContext.Set() + on n.Id equals ns.NotificationId + where n.TaskId == taskId + && ns.DeletedDate == null + select n; + + return await query.ToListAsync(); + } } diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadActiveByTaskId.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadActiveByTaskId.sql new file mode 100644 index 0000000000..a00334d7b4 --- /dev/null +++ b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadActiveByTaskId.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[Notification_ReadActiveByTaskId] + @TaskId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT n.* + FROM [dbo].[NotificationView] n + LEFT JOIN [dbo].[NotificationStatus] ns ON n.Id = ns.NotificationId + WHERE n.[TaskId] = @TaskId + AND ns.DeletedDate IS NULL +END diff --git a/util/Migrator/DbScripts/2025-05-28_00_Notification_ReadActiveByTaskId.sql b/util/Migrator/DbScripts/2025-05-28_00_Notification_ReadActiveByTaskId.sql new file mode 100644 index 0000000000..2a894bccef --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-28_00_Notification_ReadActiveByTaskId.sql @@ -0,0 +1,12 @@ +CREATE OR ALTER PROCEDURE [dbo].[Notification_ReadActiveByTaskId] + @TaskId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT n.* + FROM [dbo].[NotificationView] n + LEFT JOIN [dbo].[NotificationStatus] ns ON n.Id = ns.NotificationId + WHERE n.[TaskId] = @TaskId + AND ns.DeletedDate IS NULL +END