diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index f3de59616..2a06e6e95 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -129,6 +129,19 @@ namespace Bit.Api.Controllers var userId = _userService.GetProperUserId(User); var result = await _organizationService.InviteUserAsync(orgGuidId, userId.Value, null, new OrganizationUserInvite(model)); } + + [HttpPost("reinvite")] + public async Task BulkReinvite(string orgId, [FromBody]OrganizationUserBulkReinviteRequestModel model) + { + var orgGuidId = new Guid(orgId); + if (!_currentContext.ManageUsers(orgGuidId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + await _organizationService.ResendInvitesAsync(orgGuidId, userId.Value, model.Ids); + } [HttpPost("{id}/reinvite")] public async Task Reinvite(string orgId, string id) diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs index 8be3867ee..428df040e 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs @@ -1,4 +1,5 @@ -using Bit.Core.Models.Data; +using System; +using Bit.Core.Models.Data; using Bit.Core.Models.Table; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -89,4 +90,10 @@ namespace Bit.Core.Models.Api { public string ResetPasswordKey { get; set; } } + + public class OrganizationUserBulkReinviteRequestModel + { + [Required] + public IEnumerable Ids { get; set; } + } } diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index 04a87fffb..c80ec6175 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -29,6 +29,7 @@ namespace Bit.Core.Repositories Task CreateAsync(OrganizationUser obj, IEnumerable collections); Task ReplaceAsync(OrganizationUser obj, IEnumerable collections); Task> GetManyByManyUsersAsync(IEnumerable userIds); + Task> GetManyAsync(IEnumerable Ids); Task GetByOrganizationEmailAsync(Guid organizationId, string email); } } diff --git a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs index 83d43991f..09b3a8247 100644 --- a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs @@ -260,6 +260,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task> GetManyAsync(IEnumerable Ids) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationUser_ReadByIds]", + new { Ids = Ids.ToGuidIdArrayTVP() }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task GetByOrganizationEmailAsync(Guid organizationId, string email) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 1381b0014..e9200efb5 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -33,6 +33,7 @@ namespace Bit.Core.Services Task InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email, OrganizationUserType type, bool accessAll, string externalId, IEnumerable collections); Task> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string externalId, OrganizationUserInvite orgUserInvite); + Task ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable organizationUsersId); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId); Task AcceptUserAsync(Guid organizationUserId, User user, string token, IUserService userService); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index d41c0357b..59d47d208 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1076,6 +1076,25 @@ namespace Bit.Core.Services return orgUsers; } + public async Task ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, + IEnumerable organizationUsersId) + { + var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); + var filteredUsers = orgUsers + .Where(u => u.Status == OrganizationUserStatusType.Invited && u.OrganizationId == organizationId); + + if (!filteredUsers.Any()) + { + throw new BadRequestException("Users invalid."); + } + + var org = await GetOrgById(organizationId); + foreach (var orgUser in filteredUsers) + { + await SendInviteAsync(orgUser, org); + } + } + public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId) { var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index c99ed203f..0fba467d2 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -112,6 +112,7 @@ + diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByIds.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByIds.sql new file mode 100644 index 000000000..fde8da5a9 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByIds.sql @@ -0,0 +1,18 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByIds] + @Ids AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + IF (SELECT COUNT(1) FROM @Ids) < 1 + BEGIN + RETURN(-1) + END + + SELECT + * + FROM + [dbo].[OrganizationUserView] + WHERE + [Id] IN (SELECT [Id] FROM @Ids) +END diff --git a/util/Migrator/DbScripts/2021-05-11_00_BulkReinvite.sql b/util/Migrator/DbScripts/2021-05-11_00_BulkReinvite.sql new file mode 100644 index 000000000..16e1ebbf9 --- /dev/null +++ b/util/Migrator/DbScripts/2021-05-11_00_BulkReinvite.sql @@ -0,0 +1,24 @@ +IF OBJECT_ID('[dbo].[OrganizationUser_ReadByIds]') IS NOT NULL +BEGIN + DROP FUNCTION [dbo].[OrganizationUser_ReadByIds] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByIds] + @Ids AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + IF (SELECT COUNT(1) FROM @Ids) < 1 + BEGIN + RETURN(-1) + END + + SELECT + * + FROM + [dbo].[OrganizationUserView] + WHERE + [Id] IN (SELECT [Id] FROM @Ids) +END