diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs index ac6ebbe04c..3beb682721 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs @@ -147,16 +147,23 @@ public class EmergencyAccessRepository : Repository public async Task DeleteManyAsync(ICollection emergencyAccessIds) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var rangeToRemove = from ea in dbContext.EmergencyAccesses - where emergencyAccessIds.Contains(ea.Id) - select ea; - dbContext.EmergencyAccesses.RemoveRange(rangeToRemove); + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var rangeToRemove = from ea in dbContext.EmergencyAccesses + where emergencyAccessIds.Contains(ea.Id) + select ea; + dbContext.EmergencyAccesses.RemoveRange(rangeToRemove); - await dbContext.SaveChangesAsync(); - } + var granteeIds = rangeToRemove + .Where(ea => ea.Status == EmergencyAccessStatusType.Confirmed) + .Where(ea => ea.GranteeId.HasValue) + .Select(ea => ea.GranteeId!.Value) // .Value is safe here due to the Where above + .Distinct(); + + await dbContext.UserBumpManyAccountRevisionDatesAsync( + [.. granteeIds] + ); + + await dbContext.SaveChangesAsync(); } - } diff --git a/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteManyById.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteManyById.sql index b4c97ba6ea..ca536b30c1 100644 --- a/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteManyById.sql +++ b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteManyById.sql @@ -1,9 +1,22 @@ CREATE PROCEDURE [dbo].[EmergencyAccess_DeleteManyById] - @EmergencyAccessIds [dbo].[GuidIdArray] READONLY + @EmergencyAccessIds [dbo].[GuidIdArray] READONLY AS BEGIN SET NOCOUNT ON + -- track GranteeIds for bumping revision date prior to deletion + DECLARE @GranteeIds AS TABLE (UserId UNIQUEIDENTIFIER) + + -- this matches the logic in User_BumpAccountRevisionDateByEmergencyAccessGranteeId + INSERT INTO @GranteeIds + (UserId) + SELECT DISTINCT GranteeId + FROM + [dbo].[EmergencyAccess] EA + WHERE EA.Id IN (SELECT Id + FROM @EmergencyAccessIds + WHERE EA.[Status] = 2 ) + DECLARE @BatchSize INT = 100 -- Delete EmergencyAccess Records @@ -13,11 +26,17 @@ BEGIN DELETE TOP(@BatchSize) EA FROM [dbo].[EmergencyAccess] EA - INNER JOIN + INNER JOIN @EmergencyAccessIds EAI ON EAI.Id = EA.Id SET @BatchSize = @@ROWCOUNT - END + + -- Bump AccountRevisionDate for affected users after deletions + Exec [dbo].[User_BumpManyAccountRevisionDates] + ( + SELECT [UserId] + FROM @GranteeIds + ) END GO \ No newline at end of file diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/EmergencyAccessRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/EmergencyAccessRepositoryTests.cs index a91a983aa9..9877e8f84c 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/EmergencyAccessRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/EmergencyAccessRepositoryTests.cs @@ -48,7 +48,7 @@ public class EmergencyAccessRepositoriesTests /// All 3 records are then deleted in a single call to DeleteManyAsync. /// [DatabaseTheory, DatabaseData] - public async Task DeleteManyAsync_DeletesMultipleGranteeRecords( + public async Task DeleteManyAsync_DeletesMultipleGranteeRecords_UpdatesUserRevisionDates( IUserRepository userRepository, IEmergencyAccessRepository emergencyAccessRepository) { @@ -61,7 +61,7 @@ public class EmergencyAccessRepositoriesTests SecurityStamp = "stamp", }); - var granteeUser1 = await userRepository.CreateAsync(new User + var confirmedGranteeUser1 = await userRepository.CreateAsync(new User { Name = "Test Grantee User 1", Email = $"test+grantee{Guid.NewGuid()}@email.com", @@ -88,7 +88,7 @@ public class EmergencyAccessRepositoriesTests var confirmedEmergencyAccess = await emergencyAccessRepository.CreateAsync(new EmergencyAccess { GrantorId = grantorUser.Id, - GranteeId = granteeUser1.Id, + GranteeId = confirmedGranteeUser1.Id, Status = EmergencyAccessStatusType.Confirmed, }); @@ -110,7 +110,20 @@ public class EmergencyAccessRepositoriesTests await emergencyAccessRepository.DeleteManyAsync([confirmedEmergencyAccess.Id, invitedEmergencyAccess.Id, acceptedEmergencyAccess.Id]); // Assert - var emergencyAccess = await emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(grantorUser.Id); - Assert.Empty(emergencyAccess); + // ensure Grantor records deleted + var grantorEmergencyAccess = await emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(grantorUser.Id); + Assert.Empty(grantorEmergencyAccess); + + // ensure Grantee records deleted + foreach (User grantee in (List)[confirmedGranteeUser1, granteeUser2, granteeUser3]) + { + var granteeEmergencyAccess = await emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(grantee.Id); + Assert.Empty(granteeEmergencyAccess); + } + + // Only the Status.Confirmed grantee's AccountRevisionDate should be updated + var updatedGrantee = await userRepository.GetByIdAsync(confirmedGranteeUser1.Id); + Assert.NotNull(updatedGrantee); + Assert.NotEqual(updatedGrantee.AccountRevisionDate, confirmedGranteeUser1.AccountRevisionDate); } } diff --git a/util/Migrator/DbScripts/2026-01-23_00_AddDeleteManyEmergencyAccess.sql b/util/Migrator/DbScripts/2026-01-23_00_AddDeleteManyEmergencyAccess.sql index 40083f63a9..1c4a9c92b6 100644 --- a/util/Migrator/DbScripts/2026-01-23_00_AddDeleteManyEmergencyAccess.sql +++ b/util/Migrator/DbScripts/2026-01-23_00_AddDeleteManyEmergencyAccess.sql @@ -1,9 +1,22 @@ CREATE OR ALTER PROCEDURE [dbo].[EmergencyAccess_DeleteManyById] - @EmergencyAccessIds [dbo].[GuidIdArray] READONLY + @EmergencyAccessIds [dbo].[GuidIdArray] READONLY AS BEGIN SET NOCOUNT ON + -- track GranteeIds for bumping revision date prior to deletion + DECLARE @GranteeIds AS TABLE (UserId UNIQUEIDENTIFIER) + + -- this matches the logic in User_BumpAccountRevisionDateByEmergencyAccessGranteeId + INSERT INTO @GranteeIds + (UserId) + SELECT DISTINCT GranteeId + FROM + [dbo].[EmergencyAccess] EA + WHERE EA.Id IN (SELECT Id + FROM @EmergencyAccessIds + WHERE EA.[Status] = 2 ) + DECLARE @BatchSize INT = 100 -- Delete EmergencyAccess Records @@ -13,11 +26,17 @@ BEGIN DELETE TOP(@BatchSize) EA FROM [dbo].[EmergencyAccess] EA - INNER JOIN + INNER JOIN @EmergencyAccessIds EAI ON EAI.Id = EA.Id SET @BatchSize = @@ROWCOUNT - END + + -- Bump AccountRevisionDate for affected users after deletions + Exec [dbo].[User_BumpManyAccountRevisionDates] + ( + SELECT [UserId] + FROM @GranteeIds + ) END GO \ No newline at end of file