diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index bfa681d295..1bc8a60ab2 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -47,6 +47,18 @@ namespace Bit.Api.Controllers return new CollectionResponseModel(collection); } + [HttpGet("{id}/details")] + public async Task GetDetails(string orgId, string id) + { + var collectionDetails = await _collectionRepository.GetByIdWithGroupsAsync(new Guid(id)); + if(collectionDetails?.Item1 == null || !_currentContext.OrganizationAdmin(collectionDetails.Item1.OrganizationId)) + { + throw new NotFoundException(); + } + + return new CollectionDetailsResponseModel(collectionDetails.Item1, collectionDetails.Item2); + } + [HttpGet("")] public async Task> Get(string orgId) { @@ -80,7 +92,7 @@ namespace Bit.Api.Controllers } var collection = model.ToCollection(orgIdGuid); - await _collectionService.SaveAsync(collection); + await _collectionService.SaveAsync(collection, model?.GroupIds.Select(g => new Guid(g))); return new CollectionResponseModel(collection); } @@ -94,7 +106,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await _collectionService.SaveAsync(model.ToCollection(collection)); + await _collectionService.SaveAsync(model.ToCollection(collection), model?.GroupIds.Select(g => new Guid(g))); return new CollectionResponseModel(collection); } diff --git a/src/Core/Models/Api/Request/CollectionRequestModel.cs b/src/Core/Models/Api/Request/CollectionRequestModel.cs index 1065361afc..4710cb6523 100644 --- a/src/Core/Models/Api/Request/CollectionRequestModel.cs +++ b/src/Core/Models/Api/Request/CollectionRequestModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Bit.Core.Models.Table; using Newtonsoft.Json; +using System.Collections.Generic; namespace Bit.Core.Models.Api { @@ -12,6 +13,7 @@ namespace Bit.Core.Models.Api [EncryptedString] [StringLength(300)] public string Name { get; set; } + public IEnumerable GroupIds { get; set; } public Collection ToCollection(Guid orgId) { diff --git a/src/Core/Models/Api/Response/CollectionResponseModel.cs b/src/Core/Models/Api/Response/CollectionResponseModel.cs index ed72334941..d1685a09ea 100644 --- a/src/Core/Models/Api/Response/CollectionResponseModel.cs +++ b/src/Core/Models/Api/Response/CollectionResponseModel.cs @@ -1,6 +1,7 @@ using System; using Bit.Core.Models.Table; using Bit.Core.Models.Data; +using System.Collections.Generic; namespace Bit.Core.Models.Api { @@ -37,6 +38,17 @@ namespace Bit.Core.Models.Api public string Name { get; set; } } + public class CollectionDetailsResponseModel : CollectionResponseModel + { + public CollectionDetailsResponseModel(Collection collection, IEnumerable collectionIds) + : base(collection, "collectionDetails") + { + CollectionIds = collectionIds; + } + + public IEnumerable CollectionIds { get; set; } + } + public class CollectionUserDetailsResponseModel : CollectionResponseModel { public CollectionUserDetailsResponseModel(CollectionUserCollectionDetails collection) diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 2b110a6137..ddb134240f 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -8,8 +8,11 @@ namespace Bit.Core.Repositories public interface ICollectionRepository : IRepository { Task GetCountByOrganizationIdAsync(Guid organizationId); + Task>> GetByIdWithGroupsAsync(Guid id); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); + Task CreateAsync(Collection obj, IEnumerable groupIds); + Task ReplaceAsync(Collection obj, IEnumerable groupIds); } } diff --git a/src/Core/Repositories/SqlServer/CollectionRepository.cs b/src/Core/Repositories/SqlServer/CollectionRepository.cs index 12792b380c..6477bbc651 100644 --- a/src/Core/Repositories/SqlServer/CollectionRepository.cs +++ b/src/Core/Repositories/SqlServer/CollectionRepository.cs @@ -6,6 +6,8 @@ using System.Data.SqlClient; using System.Data; using Dapper; using System.Linq; +using Newtonsoft.Json; +using Bit.Core.Utilities; namespace Bit.Core.Repositories.SqlServer { @@ -32,6 +34,22 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task>> GetByIdWithGroupsAsync(Guid id) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryMultipleAsync( + $"[{Schema}].[Collection_ReadWithGroupsById]", + new { Id = id }, + commandType: CommandType.StoredProcedure); + + var collection = await results.ReadFirstOrDefaultAsync(); + var groupIds = (await results.ReadAsync()).ToList(); + + return new Tuple>(collection, groupIds); + } + } + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) { using(var connection = new SqlConnection(ConnectionString)) @@ -57,5 +75,39 @@ namespace Bit.Core.Repositories.SqlServer return results.ToList(); } } + + public async Task CreateAsync(Collection obj, IEnumerable groupIds) + { + obj.SetNewId(); + var objWithGroups = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj)); + objWithGroups.GroupIds = groupIds.ToGuidIdArrayTVP(); + + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Collection_CreateWithGroups]", + objWithGroups, + commandType: CommandType.StoredProcedure); + } + } + + public async Task ReplaceAsync(Collection obj, IEnumerable groupIds) + { + var objWithGroups = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj)); + objWithGroups.GroupIds = groupIds.ToGuidIdArrayTVP(); + + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[Collection_UpdateWithGroups]", + objWithGroups, + commandType: CommandType.StoredProcedure); + } + } + + public class CollectionWithGroups : Collection + { + public DataTable GroupIds { get; set; } + } } } diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs index 655bf7dd57..74f5cc99cb 100644 --- a/src/Core/Services/ICollectionService.cs +++ b/src/Core/Services/ICollectionService.cs @@ -1,10 +1,12 @@ using System.Threading.Tasks; using Bit.Core.Models.Table; +using System.Collections.Generic; +using System; namespace Bit.Core.Services { public interface ICollectionService { - Task SaveAsync(Collection collection); + Task SaveAsync(Collection collection, IEnumerable groupIds = null); } } diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index 1077d8d42e..a79529a528 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Bit.Core.Exceptions; using Bit.Core.Models.Table; using Bit.Core.Repositories; +using System.Collections.Generic; namespace Bit.Core.Services { @@ -31,7 +32,7 @@ namespace Bit.Core.Services _mailService = mailService; } - public async Task SaveAsync(Collection collection) + public async Task SaveAsync(Collection collection, IEnumerable groupIds = null) { if(collection.Id == default(Guid)) { @@ -51,11 +52,25 @@ namespace Bit.Core.Services } } - await _collectionRepository.CreateAsync(collection); + if(groupIds == null) + { + await _collectionRepository.CreateAsync(collection); + } + else + { + await _collectionRepository.CreateAsync(collection, groupIds); + } } else { - await _collectionRepository.ReplaceAsync(collection); + if(groupIds == null) + { + await _collectionRepository.ReplaceAsync(collection); + } + else + { + await _collectionRepository.ReplaceAsync(collection, groupIds); + } } } } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 7258756273..2e97ca8dc6 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -187,5 +187,8 @@ + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Collection_CreateWithGroups.sql b/src/Sql/dbo/Stored Procedures/Collection_CreateWithGroups.sql new file mode 100644 index 0000000000..f1c9dc294c --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_CreateWithGroups.sql @@ -0,0 +1,34 @@ +CREATE PROCEDURE [dbo].[Collection_CreateWithGroups] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @GroupIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @CreationDate, @RevisionDate + + ;WITH [AvailableGroupsCTE] AS( + SELECT + [Id] + FROM + [dbo].[Group] + WHERE + [OrganizationId] = @OrganizationId + ) + INSERT INTO [dbo].[CollectionGroup] + ( + [CollectionId], + [GroupId] + ) + SELECT + @Id, + [Id] + FROM + @GroupIds + WHERE + [Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsById.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsById.sql new file mode 100644 index 0000000000..cda84735c0 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsById.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[Collection_ReadWithGroupsById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_ReadById] @Id + + SELECT + [GroupId] + FROM + [dbo].[CollectionGroup] + WHERE + [CollectionId] = @Id +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql new file mode 100644 index 0000000000..e1cce487f0 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql @@ -0,0 +1,40 @@ +CREATE PROCEDURE [dbo].[Collection_UpdateWithGroups] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Name VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @GroupIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @CreationDate, @RevisionDate + + ;WITH [AvailableGroupsCTE] AS( + SELECT + Id + FROM + [dbo].[Group] + WHERE + OrganizationId = @OrganizationId + ) + MERGE + [dbo].[CollectionGroup] AS [Target] + USING + @GroupIds AS [Source] + ON + [Target].[CollectionId] = @Id + AND [Target].[GroupId] = [Source].[Id] + WHEN NOT MATCHED BY TARGET + AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN + INSERT VALUES + ( + @Id, + [Source].[Id] + ) + WHEN NOT MATCHED BY SOURCE + AND [Target].[CollectionId] = @Id THEN + DELETE + ; +END \ No newline at end of file