diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 3206aab7de..ce517bac7f 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -65,7 +65,7 @@ namespace Bit.Api.Controllers { var userId = _userService.GetProperUserId(User).Value; var folderCiphers = model.Folders.Select(f => f.ToFolder(userId)).ToList(); - var otherCiphers = model.Logins.Select(s => s.ToCipher(userId)).ToList(); + var otherCiphers = model.Logins.Select(s => s.ToCipherDetails(userId)).ToList(); await _cipherService.ImportCiphersAsync( folderCiphers, diff --git a/src/Api/Controllers/LoginsController.cs b/src/Api/Controllers/LoginsController.cs index fdc21a2497..cf517ed918 100644 --- a/src/Api/Controllers/LoginsController.cs +++ b/src/Api/Controllers/LoginsController.cs @@ -58,7 +58,7 @@ namespace Bit.Api.Controllers public async Task Post([FromBody]LoginRequestModel model) { var userId = _userService.GetProperUserId(User).Value; - var login = model.ToCipher(userId); + var login = model.ToCipherDetails(userId); await _cipherService.SaveAsync(login); var response = new LoginResponseModel(login); @@ -76,7 +76,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - await _cipherService.SaveAsync(model.ToCipher(login)); + await _cipherService.SaveAsync(model.ToCipherDetails(login)); var response = new LoginResponseModel(login); return response; diff --git a/src/Core/Models/Api/Request/LoginRequestModel.cs b/src/Core/Models/Api/Request/LoginRequestModel.cs index 8b44d05001..fd79f4fa60 100644 --- a/src/Core/Models/Api/Request/LoginRequestModel.cs +++ b/src/Core/Models/Api/Request/LoginRequestModel.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; -using Bit.Core.Models.Table; using Newtonsoft.Json; using Core.Models.Data; @@ -29,15 +28,15 @@ namespace Bit.Core.Models.Api [StringLength(10000)] public string Notes { get; set; } - public CipherDetails ToCipher(Guid userId) + public CipherDetails ToCipherDetails(Guid userId) { - return ToCipher(new CipherDetails + return ToCipherDetails(new CipherDetails { UserId = userId }); } - public CipherDetails ToCipher(CipherDetails existingLogin) + public CipherDetails ToCipherDetails(CipherDetails existingLogin) { existingLogin.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId); existingLogin.Favorite = Favorite; diff --git a/src/Core/Models/Api/Response/CipherHistoryResponseModel.cs b/src/Core/Models/Api/Response/CipherHistoryResponseModel.cs index 3ece67cc8a..63b48e9735 100644 --- a/src/Core/Models/Api/Response/CipherHistoryResponseModel.cs +++ b/src/Core/Models/Api/Response/CipherHistoryResponseModel.cs @@ -20,7 +20,7 @@ namespace Bit.Core.Models.Api throw new ArgumentNullException(nameof(deletedIds)); } - Revised = revisedCiphers.Select(c => new CipherResponseModel(c)); + //Revised = revisedCiphers.Select(c => new CipherResponseModel(c)); Deleted = deletedIds.Select(id => id.ToString()); } diff --git a/src/Core/Models/Api/Response/CipherResponseModel.cs b/src/Core/Models/Api/Response/CipherResponseModel.cs index 8ae52a6721..2bc0a626c5 100644 --- a/src/Core/Models/Api/Response/CipherResponseModel.cs +++ b/src/Core/Models/Api/Response/CipherResponseModel.cs @@ -1,32 +1,10 @@ using System; -using Bit.Core.Models.Table; using Core.Models.Data; namespace Bit.Core.Models.Api { public class CipherResponseModel : ResponseModel { - public CipherResponseModel(Cipher cipher, string obj = "cipher") - : base(obj) - { - if(cipher == null) - { - throw new ArgumentNullException(nameof(cipher)); - } - - Id = cipher.Id.ToString(); - Type = cipher.Type; - RevisionDate = cipher.RevisionDate; - - switch(cipher.Type) - { - case Enums.CipherType.Login: - Data = new LoginDataModel(cipher); - break; - default: - throw new ArgumentException("Unsupported " + nameof(Type) + "."); - } - } public CipherResponseModel(CipherDetails cipher, string obj = "cipher") : base(obj) { @@ -38,6 +16,8 @@ namespace Bit.Core.Models.Api Id = cipher.Id.ToString(); Type = cipher.Type; RevisionDate = cipher.RevisionDate; + FolderId = cipher.FolderId?.ToString(); + Favorite = cipher.Favorite; switch(cipher.Type) { diff --git a/src/Core/Models/Api/Response/LoginResponseModel.cs b/src/Core/Models/Api/Response/LoginResponseModel.cs index ffcf4410dd..d547b9b00c 100644 --- a/src/Core/Models/Api/Response/LoginResponseModel.cs +++ b/src/Core/Models/Api/Response/LoginResponseModel.cs @@ -1,6 +1,5 @@ using System; using Core.Models.Data; -using Bit.Core.Models.Table; namespace Bit.Core.Models.Api { @@ -41,8 +40,5 @@ namespace Bit.Core.Models.Api public string Password { get; set; } public string Notes { get; set; } public DateTime RevisionDate { get; set; } - - // Expandables - public FolderResponseModel Folder { get; set; } } } diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index ab10677edf..892e5f94cd 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -13,6 +13,9 @@ namespace Bit.Core.Repositories Task> GetManyByTypeAndUserIdAsync(Enums.CipherType type, Guid userId); Task, ICollection>> GetManySinceRevisionDateAndUserIdWithDeleteHistoryAsync( DateTime sinceRevisionDate, Guid userId); + Task CreateAsync(CipherDetails cipher); + Task ReplaceAsync(CipherDetails cipher); + Task UpsertAsync(CipherDetails cipher); Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers); Task CreateAsync(IEnumerable ciphers); } diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index d1aa517cae..eb9abde8f3 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -85,6 +85,41 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task CreateAsync(CipherDetails cipher) + { + cipher.SetNewId(); + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[CipherDetails_Create]", + cipher, + commandType: CommandType.StoredProcedure); + } + } + + public async Task ReplaceAsync(CipherDetails obj) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[{Schema}].[CipherDetails_Update]", + obj, + commandType: CommandType.StoredProcedure); + } + } + + public async Task UpsertAsync(CipherDetails cipher) + { + if(cipher.Id.Equals(default(Guid))) + { + await CreateAsync(cipher); + } + else + { + await ReplaceAsync(cipher); + } + } + public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers) { if(ciphers.Count() == 0) diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index 753cd263e8..0afe7e35ed 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -7,7 +7,7 @@ namespace Bit.Core.Services { public interface ICipherService { - Task SaveAsync(Cipher cipher); + Task SaveAsync(CipherDetails cipher); Task DeleteAsync(Cipher cipher); Task SaveFolderAsync(Folder folder); Task DeleteFolderAsync(Folder folder); diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index 72425c22d9..c910f00932 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -27,7 +27,7 @@ namespace Bit.Core.Services _pushService = pushService; } - public async Task SaveAsync(Cipher cipher) + public async Task SaveAsync(CipherDetails cipher) { if(cipher.Id == default(Guid)) { diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 1b990cf0e2..44f6e567ca 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -165,5 +165,8 @@ + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql new file mode 100644 index 0000000000..2f4ba02504 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_Create.sql @@ -0,0 +1,45 @@ +CREATE PROCEDURE [dbo].[CipherDetails_Create] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Cipher] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @UserId, + @OrganizationId, + @Type, + @Data, + @CreationDate, + @RevisionDate + ) + + IF @FolderId IS NOT NULL + BEGIN + EXEC [dbo].[FolderCipher_Create] @FolderId, @Id + END + + IF @Favorite = 1 + BEGIN + EXEC [dbo].[Favorite_Create] @UserId, @Id + END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql new file mode 100644 index 0000000000..ee62b5b9e9 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/CipherDetails_Update.sql @@ -0,0 +1,44 @@ +CREATE PROCEDURE [dbo].[CipherDetails_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Cipher] + SET + [UserId] = @UserId, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id + + IF @FolderId IS NULL + BEGIN + EXEC [dbo].[FolderCipher_DeleteByUserId] @UserId, @Id + END + ELSE IF (SELECT COUNT(1) FROM [dbo].[FolderCipher] WHERE [FolderId] = @FolderId AND [CipherId] = @Id) = 0 + BEGIN + EXEC [dbo].[FolderCipher_Create] @FolderId, @Id + END + + IF @Favorite = 0 + BEGIN + EXEC [dbo].[Favorite_Delete] @UserId, @Id + END + ELSE IF (SELECT COUNT(1) FROM [dbo].[Favorite] WHERE [UserId] = @UserId AND [CipherId] = @Id) = 0 + BEGIN + EXEC [dbo].[Favorite_Create] @UserId, @Id + END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_Create.sql b/src/Sql/dbo/Stored Procedures/Cipher_Create.sql index 03956191be..dc3fe7693b 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_Create.sql @@ -5,10 +5,7 @@ @Type TINYINT, @Data NVARCHAR(MAX), @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7), - -- Extra, unused props from cipher details model since dapper maps all child properties too. This is kind of a hack. - @FolderId UNIQUEIDENTIFIER, - @Favorite BIT + @RevisionDate DATETIME2(7) AS BEGIN SET NOCOUNT ON diff --git a/src/Sql/dbo/Stored Procedures/Cipher_Update.sql b/src/Sql/dbo/Stored Procedures/Cipher_Update.sql index ad3a3d0fba..8c05b46a3d 100644 --- a/src/Sql/dbo/Stored Procedures/Cipher_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Cipher_Update.sql @@ -5,10 +5,7 @@ @Type TINYINT, @Data NVARCHAR(MAX), @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7), - -- Extra, unused props from cipher details model since dapper maps all child properties too. This is kind of a hack. - @FolderId UNIQUEIDENTIFIER, - @Favorite BIT + @RevisionDate DATETIME2(7) AS BEGIN SET NOCOUNT ON diff --git a/src/Sql/dbo/Stored Procedures/FolderCipher_DeleteByUserId.sql b/src/Sql/dbo/Stored Procedures/FolderCipher_DeleteByUserId.sql new file mode 100644 index 0000000000..e78b3203d8 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/FolderCipher_DeleteByUserId.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[FolderCipher_DeleteByUserId] + @UserId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FC + FROM + [dbo].[FolderCipher] FC + INNER JOIN + [dbo].[Folder] F ON F.[Id] = FC.[FolderId] + WHERE + F.[UserId] = @UserId + AND [CipherId] = @CipherId +END \ No newline at end of file