diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index 5734fe00fa..75953836af 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -92,7 +92,7 @@ namespace Bit.Api.Controllers } var userId = _userService.GetProperUserId(User); - var result = await _organizationService.InviteUserAsync(orgGuidId, userId.Value, model.Email, model.Type.Value, + var result = await _organizationService.InviteUserAsync(orgGuidId, userId.Value, model.Emails, model.Type.Value, model.AccessAll, null, model.Collections?.Select(c => c.ToSelectionReadOnly())); } diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs index e694a4fcce..01eeb44b7e 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUserRequestModels.cs @@ -1,18 +1,41 @@ using Bit.Core.Models.Table; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System; +using System.Linq; namespace Bit.Core.Models.Api { - public class OrganizationUserInviteRequestModel + public class OrganizationUserInviteRequestModel : IValidatableObject { [Required] - [EmailAddress] - public string Email { get; set; } + public IEnumerable Emails { get; set; } [Required] public Enums.OrganizationUserType? Type { get; set; } public bool AccessAll { get; set; } public IEnumerable Collections { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if(!Emails.Any()) + { + yield return new ValidationResult("An email is required."); + } + + if(Emails.Count() > 20) + { + yield return new ValidationResult("You can only invite up to 20 users at a time."); + } + + var attr = new EmailAddressAttribute(); + for(int i = 0; i < Emails.Count(); i++) + { + if(!attr.IsValid(Emails.ElementAt(i))) + { + yield return new ValidationResult($"Email #{i + 1} is not valid.", new string[] { nameof(Emails) }); + } + } + } } public class OrganizationUserAcceptRequestModel diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index f3beac650f..e26ece8e07 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -23,6 +23,8 @@ namespace Bit.Core.Services Task UpdateAsync(Organization organization, bool updateBilling = false); Task InviteUserAsync(Guid organizationId, Guid invitingUserId, string email, OrganizationUserType type, bool accessAll, string externalId, IEnumerable collections); + Task> InviteUserAsync(Guid organizationId, Guid invitingUserId, IEnumerable emails, + OrganizationUserType type, bool accessAll, string externalId, IEnumerable collections); Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId); Task AcceptUserAsync(Guid organizationUserId, User user, string token); Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index c6cc9ec06e..d7e460d8f6 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -685,6 +685,15 @@ namespace Bit.Core.Services public async Task InviteUserAsync(Guid organizationId, Guid invitingUserId, string email, OrganizationUserType type, bool accessAll, string externalId, IEnumerable collections) + { + var result = await InviteUserAsync(organizationId, invitingUserId, new List { email }, type, accessAll, + externalId, collections); + return result.FirstOrDefault(); + } + + public async Task> InviteUserAsync(Guid organizationId, Guid invitingUserId, + IEnumerable emails, OrganizationUserType type, bool accessAll, string externalId, + IEnumerable collections) { var organization = await _organizationRepository.GetByIdAsync(organizationId); if(organization == null) @@ -695,45 +704,52 @@ namespace Bit.Core.Services if(organization.Seats.HasValue) { var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId); - if(userCount >= organization.Seats.Value) + var availableSeats = organization.Seats.Value - userCount; + if(availableSeats >= emails.Count()) { throw new BadRequestException("You have reached the maximum number of users " + $"({organization.Seats.Value}) for this organization."); } } - // Make sure user is not already invited - var existingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, email); - if(existingOrgUser != null) + var orgUsers = new List(); + foreach(var email in emails) { - throw new BadRequestException("User already invited."); + // Make sure user is not already invited + var existingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, email); + if(existingOrgUser != null) + { + throw new BadRequestException("User already invited."); + } + + var orgUser = new OrganizationUser + { + OrganizationId = organizationId, + UserId = null, + Email = email.ToLowerInvariant(), + Key = null, + Type = type, + Status = OrganizationUserStatusType.Invited, + AccessAll = accessAll, + ExternalId = externalId, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow + }; + + if(!orgUser.AccessAll && collections.Any()) + { + await _organizationUserRepository.CreateAsync(orgUser, collections); + } + else + { + await _organizationUserRepository.CreateAsync(orgUser); + } + + await SendInviteAsync(orgUser); + orgUsers.Add(orgUser); } - var orgUser = new OrganizationUser - { - OrganizationId = organizationId, - UserId = null, - Email = email.ToLowerInvariant(), - Key = null, - Type = type, - Status = OrganizationUserStatusType.Invited, - AccessAll = accessAll, - ExternalId = externalId, - CreationDate = DateTime.UtcNow, - RevisionDate = DateTime.UtcNow - }; - - if(!orgUser.AccessAll && collections.Any()) - { - await _organizationUserRepository.CreateAsync(orgUser, collections); - } - else - { - await _organizationUserRepository.CreateAsync(orgUser); - } - - await SendInviteAsync(orgUser); - return orgUser; + return orgUsers; } public async Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId)