diff --git a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs index 177f2ad52e..0ae0542838 100644 --- a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs +++ b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs @@ -34,11 +34,12 @@ public interface INotificationRepository : IRepository NotificationStatusFilter? statusFilter, PageOptions pageOptions); /// - /// Get non-deleted notifications by a task Id. + /// Marks notifications as deleted by a task Id. /// /// The unique identifier of the task. + /// User Id /// - /// A collection of notifications associated with the task that are not marked as deleted. + /// A collection of notifications associated with the task that are now marked as deleted. /// - Task> GetNonDeletedByTaskIdAsync(Guid taskId); + Task> MarkNotificationsAsDeletedByTask(Guid taskId, Guid userId); } diff --git a/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs b/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs index 90566b4d83..97febb7202 100644 --- a/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs +++ b/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs @@ -6,6 +6,7 @@ public interface IMarkNotificationsForTaskAsDeletedCommand /// Marks notifications associated with a given taskId as deleted. /// /// The unique identifier of the task to complete + /// User Id /// A task representing the async operation - Task MarkAsDeletedAsync(Guid taskId); + Task MarkAsDeletedAsync(Guid taskId, Guid userId); } diff --git a/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs index 33bfaaa6f2..67bd7f2f12 100644 --- a/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs +++ b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs @@ -1,6 +1,4 @@ -using Bit.Core.Context; -using Bit.Core.NotificationCenter.Entities; -using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Platform.Push; using Bit.Core.Vault.Commands.Interfaces; @@ -9,56 +7,26 @@ 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) + public async Task MarkAsDeletedAsync(Guid taskId, Guid userId) { - var notifications = await _notificationRepository.GetNonDeletedByTaskIdAsync(taskId); + var notifications = await _notificationRepository.MarkNotificationsAsDeletedByTask(taskId, userId); - 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 - }; - - await _notificationStatusRepository.CreateAsync(notificationStatus); - - } - else - { - notificationStatus.DeletedDate = DateTime.UtcNow; - - await _notificationStatusRepository.UpdateAsync(notificationStatus); - } - } - - // For each user, send a push notification so they can update their local tasks + // For each user associated with the notifications, send a push notification so local tasks can be updated. var uniqueUserIds = notifications.Select(n => n.UserId).Where(u => u.HasValue).Distinct(); - foreach (var userId in uniqueUserIds) + foreach (var id in uniqueUserIds) { - await _pushNotificationService.PushPendingSecurityTasksAsync((Guid)userId); + await _pushNotificationService.PushPendingSecurityTasksAsync((Guid)id); } } } diff --git a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs index 8a12910bb8..93fe162ce9 100644 --- a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs +++ b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs @@ -52,6 +52,6 @@ public class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand await _securityTaskRepository.ReplaceAsync(task); // Mark all notifications related to this task as deleted - await _markNotificationsForTaskAsDeletedAsync.MarkAsDeletedAsync(taskId); + await _markNotificationsForTaskAsDeletedAsync.MarkAsDeletedAsync(taskId, _currentContext.UserId.Value); } } diff --git a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs index 9718c7377e..0c67ede40a 100644 --- a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs @@ -57,15 +57,16 @@ public class NotificationRepository : Repository, INotificat }; } - public async Task> GetNonDeletedByTaskIdAsync(Guid taskId) + public async Task> MarkNotificationsAsDeletedByTask(Guid taskId, Guid userId) { await using var connection = new SqlConnection(ConnectionString); var results = await connection.QueryAsync( - "[dbo].[Notification_ReadNonDeletedByTaskId]", + "[dbo].[Notification_MarkAsDeletedByTask]", new { TaskId = taskId, + UserId = userId, }, commandType: CommandType.StoredProcedure); diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs index 0140fde0a2..06d287382e 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs @@ -75,19 +75,47 @@ public class NotificationRepository : Repository> GetNonDeletedByTaskIdAsync(Guid taskId) + public async Task> MarkNotificationsAsDeletedByTask(Guid taskId, Guid userId) { 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; + var notifications = await dbContext.Notifications + .Where(n => n.TaskId == taskId) + .ToListAsync(); - return await query.ToListAsync(); + var notificationIds = notifications.Select(n => n.Id).ToList(); + + var statuses = await dbContext.Set() + .Where(ns => notificationIds.Contains(ns.NotificationId) && ns.UserId == userId) + .ToListAsync(); + + var now = DateTime.UtcNow; + + // Update existing statuses and add missing ones + foreach (var notification in notifications) + { + var status = statuses.FirstOrDefault(s => s.NotificationId == notification.Id); + if (status != null) + { + if (status.DeletedDate == null) + { + status.DeletedDate = now; + } + } + else + { + dbContext.Set().Add(new NotificationStatus + { + NotificationId = notification.Id, + UserId = userId, + DeletedDate = now + }); + } + } + + await dbContext.SaveChangesAsync(); + + return notifications; } } diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql new file mode 100644 index 0000000000..2216dcc666 --- /dev/null +++ b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql @@ -0,0 +1,38 @@ +CREATE PROCEDURE [dbo].[Notification_MarkAsDeletedByTask] + @TaskId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + -- Collect NotificationIds as they are altered + DECLARE @AlteredNotifications TABLE ( + NotificationId UNIQUEIDENTIFIER + ); + + -- Update existing NotificationStatus as deleted + UPDATE ns + SET ns.DeletedDate = GETUTCDATE() + OUTPUT inserted.NotificationId INTO @AlteredNotifications + FROM NotificationStatus ns + INNER JOIN Notification n ON ns.NotificationId = n.Id + WHERE n.TaskId = @TaskId + AND ns.UserId = @UserId + AND ns.DeletedDate IS NULL; + + -- Insert NotificationStatus records for notifications that don't have one yet + INSERT INTO NotificationStatus (NotificationId, UserId, DeletedDate) + OUTPUT inserted.NotificationId INTO @AlteredNotifications + SELECT n.Id, @UserId, GETUTCDATE() + FROM Notification n + LEFT JOIN NotificationStatus ns + ON n.Id = ns.NotificationId AND ns.UserId = @UserId + WHERE n.TaskId = @TaskId + AND ns.NotificationId IS NULL; + + -- Return all notifications that have been altered + SELECT n.* + FROM Notification n + INNER JOIN @AlteredNotifications a ON n.Id = a.NotificationId; +END +GO diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadNonDeletedByTaskId.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadNonDeletedByTaskId.sql deleted file mode 100644 index cc6b906b16..0000000000 --- a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadNonDeletedByTaskId.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE PROCEDURE [dbo].[Notification_ReadNonDeletedByTaskId] - @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-29_00_Notification_ReadNonDeletedByTaskId.sql b/util/Migrator/DbScripts/2025-05-29_00_Notification_ReadNonDeletedByTaskId.sql deleted file mode 100644 index 1f08ceb318..0000000000 --- a/util/Migrator/DbScripts/2025-05-29_00_Notification_ReadNonDeletedByTaskId.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE OR ALTER PROCEDURE [dbo].[Notification_ReadNonDeletedByTaskId] - @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-29_02_Notification_MarkAsDeletedByTask.sql b/util/Migrator/DbScripts/2025-05-29_02_Notification_MarkAsDeletedByTask.sql new file mode 100644 index 0000000000..27f332baa8 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-29_02_Notification_MarkAsDeletedByTask.sql @@ -0,0 +1,38 @@ +CREATE OR ALTER PROCEDURE [dbo].[Notification_MarkAsDeletedByTask] + @TaskId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + -- Collect NotificationIds as they are altered + DECLARE @AlteredNotifications TABLE ( + NotificationId UNIQUEIDENTIFIER + ); + + -- Update existing NotificationStatus as deleted + UPDATE ns + SET ns.DeletedDate = GETUTCDATE() + OUTPUT inserted.NotificationId INTO @AlteredNotifications + FROM NotificationStatus ns + INNER JOIN Notification n ON ns.NotificationId = n.Id + WHERE n.TaskId = @TaskId + AND ns.UserId = @UserId + AND ns.DeletedDate IS NULL; + + -- Insert NotificationStatus records for notifications that don't have one yet + INSERT INTO NotificationStatus (NotificationId, UserId, DeletedDate) + OUTPUT inserted.NotificationId INTO @AlteredNotifications + SELECT n.Id, @UserId, GETUTCDATE() + FROM Notification n + LEFT JOIN NotificationStatus ns + ON n.Id = ns.NotificationId AND ns.UserId = @UserId + WHERE n.TaskId = @TaskId + AND ns.NotificationId IS NULL; + + -- Return all notifications that have been altered + SELECT n.* + FROM Notification n + INNER JOIN @AlteredNotifications a ON n.Id = a.NotificationId; +END +GO