diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index b4bf2f73a..cebe4849c 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -188,6 +188,19 @@ namespace Bit.Api.Controllers await _organizationService.ReinstateSubscriptionAsync(orgIdGuid); } + [HttpPost("{id}/leave")] + public async Task Leave(string id) + { + var orgGuidId = new Guid(id); + if(!_currentContext.OrganizationUser(orgGuidId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User); + await _organizationService.DeleteUserAsync(orgGuidId, userId.Value); + } + [HttpDelete("{id}")] [HttpPost("{id}/delete")] public async Task Delete(string id, [FromBody]OrganizationDeleteRequestModel model) diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index 13ea4c731..c6943a5b5 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -14,6 +14,7 @@ namespace Bit.Core.Repositories Task> GetManyByUserAsync(Guid userId); Task> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type); Task GetByOrganizationAsync(Guid organizationId, string email); + Task GetByOrganizationAsync(Guid organizationId, Guid userId); Task>> GetDetailsByIdAsync(Guid id); Task> GetManyDetailsByOrganizationAsync(Guid organizationId); Task> GetManyDetailsByUserAsync(Guid userId, diff --git a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs index 5592e9684..b7b95c773 100644 --- a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs @@ -60,6 +60,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task GetByOrganizationAsync(Guid organizationId, Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationUser_ReadByOrganizationIdUserId]", + new { OrganizationId = organizationId, UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + public async Task> GetManyByUserAsync(Guid userId) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index d0b4ad8da..768157457 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -25,5 +25,6 @@ namespace Bit.Core.Services Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId); Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable subvaults); Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId); + Task DeleteUserAsync(Guid organizationId, Guid userId); } } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index c2737ef75..5e2cd2cb9 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -808,6 +808,23 @@ namespace Bit.Core.Services await _organizationUserRepository.DeleteAsync(orgUser); } + public async Task DeleteUserAsync(Guid organizationId, Guid userId) + { + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + if(orgUser == null) + { + throw new NotFoundException(); + } + + var confirmedOwners = (await GetConfirmedOwnersAsync(organizationId)).ToList(); + if(confirmedOwners.Count == 1 && confirmedOwners[0].Id == orgUser.Id) + { + throw new BadRequestException("Organization must have at least one confirmed owner."); + } + + await _organizationUserRepository.DeleteAsync(orgUser); + } + private async Task> GetConfirmedOwnersAsync(Guid organizationId) { var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId, diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 5719e1dbc..a85a33835 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -183,5 +183,6 @@ + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdUserId.sql new file mode 100644 index 000000000..96838916c --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdUserId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdUserId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserView] + WHERE + [OrganizationId] = @OrganizationId + AND [UserId] = @UserId +END \ No newline at end of file