diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 12c94aa94..fe884130e 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -226,6 +226,8 @@ namespace Bit.Android container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); // Other container.RegisterSingleton(CrossSettings.Current); diff --git a/src/App/Abstractions/Repositories/ICipherCollectionRepository.cs b/src/App/Abstractions/Repositories/ICipherCollectionRepository.cs new file mode 100644 index 000000000..559e48d1c --- /dev/null +++ b/src/App/Abstractions/Repositories/ICipherCollectionRepository.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Bit.App.Models.Data; +using System.Collections.Generic; + +namespace Bit.App.Abstractions +{ + public interface ICipherCollectionRepository + { + Task> GetAllByUserIdAsync(string userId); + Task InsertAsync(CipherCollectionData obj); + Task DeleteAsync(CipherCollectionData obj); + Task DeleteByUserIdAsync(string userId); + } +} diff --git a/src/App/Abstractions/Repositories/ICollectionRepository.cs b/src/App/Abstractions/Repositories/ICollectionRepository.cs new file mode 100644 index 000000000..d05ed3fda --- /dev/null +++ b/src/App/Abstractions/Repositories/ICollectionRepository.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.App.Models.Data; + +namespace Bit.App.Abstractions +{ + public interface ICollectionRepository : IRepository + { + Task> GetAllByUserIdAsync(string userId); + } +} diff --git a/src/App/App.csproj b/src/App/App.csproj index 5572a215f..3e68389cc 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -36,6 +36,8 @@ + + @@ -107,6 +109,7 @@ + @@ -134,7 +137,10 @@ + + + @@ -186,6 +192,9 @@ + + + diff --git a/src/App/Models/Api/Response/CipherResponse.cs b/src/App/Models/Api/Response/CipherResponse.cs index 6b420403d..0dca96085 100644 --- a/src/App/Models/Api/Response/CipherResponse.cs +++ b/src/App/Models/Api/Response/CipherResponse.cs @@ -17,6 +17,7 @@ namespace Bit.App.Models.Api public bool OrganizationUseTotp { get; set; } public JObject Data { get; set; } public IEnumerable Attachments { get; set; } + public IEnumerable CollectionIds { get; set; } public DateTime RevisionDate { get; set; } } } diff --git a/src/App/Models/Api/Response/CollectionResponse.cs b/src/App/Models/Api/Response/CollectionResponse.cs new file mode 100644 index 000000000..b756f1b7b --- /dev/null +++ b/src/App/Models/Api/Response/CollectionResponse.cs @@ -0,0 +1,9 @@ +namespace Bit.App.Models.Api +{ + public class CollectionResponse + { + public string Id { get; set; } + public string Name { get; set; } + public string OrganizationId { get; set; } + } +} diff --git a/src/App/Models/Api/Response/SyncResponse.cs b/src/App/Models/Api/Response/SyncResponse.cs index dae0ad764..4f43d789f 100644 --- a/src/App/Models/Api/Response/SyncResponse.cs +++ b/src/App/Models/Api/Response/SyncResponse.cs @@ -6,6 +6,7 @@ namespace Bit.App.Models.Api { public ProfileResponse Profile { get; set; } public IEnumerable Folders { get; set; } + public IEnumerable Collections { get; set; } public IEnumerable Ciphers { get; set; } public DomainsResponse Domains { get; set; } } diff --git a/src/App/Models/Collection.cs b/src/App/Models/Collection.cs new file mode 100644 index 000000000..2e85b7b77 --- /dev/null +++ b/src/App/Models/Collection.cs @@ -0,0 +1,29 @@ +using Bit.App.Models.Data; +using Bit.App.Models.Api; + +namespace Bit.App.Models +{ + public class Collection + { + public Collection() + { } + + public Collection(CollectionData data) + { + Id = data.Id; + OrganizationId = data.OrganizationId; + Name = data.Name != null ? new CipherString(data.Name) : null; + } + + public Collection(CollectionResponse response) + { + Id = response.Id; + OrganizationId = response.OrganizationId; + Name = response.Name != null ? new CipherString(response.Name) : null; + } + + public string Id { get; set; } + public string OrganizationId { get; set; } + public CipherString Name { get; set; } + } +} diff --git a/src/App/Models/Data/CipherCollectionData.cs b/src/App/Models/Data/CipherCollectionData.cs new file mode 100644 index 000000000..58f213901 --- /dev/null +++ b/src/App/Models/Data/CipherCollectionData.cs @@ -0,0 +1,18 @@ +using SQLite; + +namespace Bit.App.Models.Data +{ + [Table("CipherCollection")] + public class CipherCollectionData + { + [PrimaryKey] + [AutoIncrement] + public int Id { get; set; } + [Indexed] + public string UserId { get; set; } + [Indexed] + public string CipherId { get; set; } + [Indexed] + public string CollectionId { get; set; } + } +} diff --git a/src/App/Models/Data/CollectionData.cs b/src/App/Models/Data/CollectionData.cs new file mode 100644 index 000000000..195c2fb77 --- /dev/null +++ b/src/App/Models/Data/CollectionData.cs @@ -0,0 +1,36 @@ +using SQLite; +using Bit.App.Abstractions; +using Bit.App.Models.Api; + +namespace Bit.App.Models.Data +{ + [Table("Collection")] + public class CollectionData : IDataObject + { + public CollectionData() + { } + + public CollectionData(Collection collection, string userId) + { + Id = collection.Id; + UserId = userId; + Name = collection.Name?.EncryptedString; + OrganizationId = collection.OrganizationId; + } + + public CollectionData(CollectionResponse collection, string userId) + { + Id = collection.Id; + UserId = userId; + Name = collection.Name; + OrganizationId = collection.OrganizationId; + } + + [PrimaryKey] + public string Id { get; set; } + [Indexed] + public string UserId { get; set; } + public string Name { get; set; } + public string OrganizationId { get; set; } + } +} diff --git a/src/App/Models/Data/FolderData.cs b/src/App/Models/Data/FolderData.cs index 5d806769d..c6c9a66c4 100644 --- a/src/App/Models/Data/FolderData.cs +++ b/src/App/Models/Data/FolderData.cs @@ -32,10 +32,5 @@ namespace Bit.App.Models.Data public string UserId { get; set; } public string Name { get; set; } public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow; - - public Folder ToFolder() - { - return new Folder(this); - } } } diff --git a/src/App/Repositories/BaseRepository.cs b/src/App/Repositories/BaseRepository.cs new file mode 100644 index 000000000..623bae693 --- /dev/null +++ b/src/App/Repositories/BaseRepository.cs @@ -0,0 +1,15 @@ +using Bit.App.Abstractions; +using SQLite; + +namespace Bit.App.Repositories +{ + public abstract class BaseRepository + { + public BaseRepository(ISqlService sqlService) + { + Connection = sqlService.GetConnection(); + } + + protected SQLiteConnection Connection { get; private set; } + } +} diff --git a/src/App/Repositories/CipherCollectionRepository.cs b/src/App/Repositories/CipherCollectionRepository.cs new file mode 100644 index 000000000..c844a6b99 --- /dev/null +++ b/src/App/Repositories/CipherCollectionRepository.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Data; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.App.Repositories +{ + public class CipherCollectionRepository : BaseRepository, ICipherCollectionRepository + { + public CipherCollectionRepository(ISqlService sqlService) + : base(sqlService) + { } + + public Task> GetAllByUserIdAsync(string userId) + { + var cipherCollections = Connection.Table().Where(f => f.UserId == userId) + .Cast(); + return Task.FromResult(cipherCollections); + } + + public virtual Task InsertAsync(CipherCollectionData obj) + { + Connection.Insert(obj); + return Task.FromResult(0); + } + + public virtual Task DeleteAsync(CipherCollectionData obj) + { + Connection.Delete(obj.Id); + return Task.FromResult(0); + } + + public virtual Task DeleteByUserIdAsync(string userId) + { + Connection.Execute("DELETE FROM CipherCollection WHERE UserId = ?", userId); + return Task.FromResult(0); + } + } +} diff --git a/src/App/Repositories/CollectionRepository.cs b/src/App/Repositories/CollectionRepository.cs new file mode 100644 index 000000000..9924e5bb4 --- /dev/null +++ b/src/App/Repositories/CollectionRepository.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Data; + +namespace Bit.App.Repositories +{ + public class CollectionRepository : Repository, ICollectionRepository + { + public CollectionRepository(ISqlService sqlService) + : base(sqlService) + { } + + public Task> GetAllByUserIdAsync(string userId) + { + var folders = Connection.Table().Where(f => f.UserId == userId).Cast(); + return Task.FromResult(folders); + } + } +} diff --git a/src/App/Repositories/Repository.cs b/src/App/Repositories/Repository.cs index 8be6118c8..92f3696a5 100644 --- a/src/App/Repositories/Repository.cs +++ b/src/App/Repositories/Repository.cs @@ -3,20 +3,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bit.App.Abstractions; -using SQLite; namespace Bit.App.Repositories { - public abstract class Repository : IRepository + public abstract class Repository : BaseRepository, IRepository where TId : IEquatable where T : class, IDataObject, new() { public Repository(ISqlService sqlService) - { - Connection = sqlService.GetConnection(); - } - - protected SQLiteConnection Connection { get; private set; } + : base(sqlService) + { } public virtual Task GetByIdAsync(TId id) { @@ -39,6 +35,7 @@ namespace Bit.App.Repositories Connection.Update(obj); return Task.FromResult(0); } + public virtual Task UpsertAsync(T obj) { Connection.InsertOrReplace(obj); diff --git a/src/App/Services/DatabaseService.cs b/src/App/Services/DatabaseService.cs index 578a610f4..3f177c6eb 100644 --- a/src/App/Services/DatabaseService.cs +++ b/src/App/Services/DatabaseService.cs @@ -17,7 +17,9 @@ namespace Bit.App.Services public void CreateTables() { _connection.CreateTable(); + _connection.CreateTable(); _connection.CreateTable(); + _connection.CreateTable(); _connection.CreateTable(); _connection.CreateTable(); } diff --git a/src/App/Services/SyncService.cs b/src/App/Services/SyncService.cs index 9130ad4e2..e2363d875 100644 --- a/src/App/Services/SyncService.cs +++ b/src/App/Services/SyncService.cs @@ -20,6 +20,8 @@ namespace Bit.App.Services private readonly ISettingsApiRepository _settingsApiRepository; private readonly ISyncApiRepository _syncApiRepository; private readonly IFolderRepository _folderRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly ICipherCollectionRepository _cipherCollectionRepository; private readonly ICipherService _cipherService; private readonly IAttachmentRepository _attachmentRepository; private readonly ISettingsRepository _settingsRepository; @@ -35,6 +37,8 @@ namespace Bit.App.Services ISettingsApiRepository settingsApiRepository, ISyncApiRepository syncApiRepository, IFolderRepository folderRepository, + ICollectionRepository collectionRepository, + ICipherCollectionRepository cipherCollectionRepository, ICipherService cipherService, IAttachmentRepository attachmentRepository, ISettingsRepository settingsRepository, @@ -49,6 +53,8 @@ namespace Bit.App.Services _settingsApiRepository = settingsApiRepository; _syncApiRepository = syncApiRepository; _folderRepository = folderRepository; + _collectionRepository = collectionRepository; + _cipherCollectionRepository = cipherCollectionRepository; _cipherService = cipherService; _attachmentRepository = attachmentRepository; _settingsRepository = settingsRepository; @@ -258,9 +264,9 @@ namespace Bit.App.Services var now = DateTime.UtcNow; var syncResponse = await _syncApiRepository.Get(); - if(!CheckSuccess(syncResponse, + if(!CheckSuccess(syncResponse, !string.IsNullOrWhiteSpace(_appSettingsService.SecurityStamp) && - syncResponse.Result?.Profile != null && + syncResponse.Result?.Profile != null && _appSettingsService.SecurityStamp != syncResponse.Result.Profile.SecurityStamp)) { return false; @@ -268,15 +274,17 @@ namespace Bit.App.Services var ciphersDict = syncResponse.Result.Ciphers.ToDictionary(s => s.Id); var foldersDict = syncResponse.Result.Folders.ToDictionary(f => f.Id); + var collectionsDict = syncResponse.Result.Collections?.ToDictionary(c => c.Id); var cipherTask = SyncCiphersAsync(ciphersDict); var folderTask = SyncFoldersAsync(foldersDict); + var collectionsTask = SyncCollectionsAsync(collectionsDict); var domainsTask = SyncDomainsAsync(syncResponse.Result.Domains); var profileTask = SyncProfileKeysAsync(syncResponse.Result.Profile); - await Task.WhenAll(cipherTask, folderTask, domainsTask, profileTask).ConfigureAwait(false); + await Task.WhenAll(cipherTask, folderTask, collectionsTask, domainsTask, profileTask).ConfigureAwait(false); - if(folderTask.Exception != null || cipherTask.Exception != null || domainsTask.Exception != null || - profileTask.Exception != null) + if(folderTask.Exception != null || cipherTask.Exception != null || collectionsTask.Exception != null || + domainsTask.Exception != null || profileTask.Exception != null) { SyncCompleted(false); return false; @@ -349,7 +357,48 @@ namespace Bit.App.Services } } - private async Task SyncCiphersAsync(IDictionary serviceCiphers) + private async Task SyncCollectionsAsync(IDictionary serverCollections) + { + if(!_authService.IsAuthenticated) + { + return; + } + + var localCollections = (await _collectionRepository.GetAllByUserIdAsync(_authService.UserId) + .ConfigureAwait(false)) + .GroupBy(f => f.Id) + .Select(f => f.First()) + .ToDictionary(f => f.Id); + + if(serverCollections != null) + { + foreach(var serverCollection in serverCollections) + { + if(!_authService.IsAuthenticated) + { + return; + } + + try + { + var data = new CollectionData(serverCollection.Value, _authService.UserId); + await _collectionRepository.UpsertAsync(data).ConfigureAwait(false); + } + catch(SQLite.SQLiteException) { } + } + } + + foreach(var collection in localCollections.Where(lc => !serverCollections.ContainsKey(lc.Key))) + { + try + { + await _collectionRepository.DeleteAsync(collection.Value.Id).ConfigureAwait(false); + } + catch(SQLite.SQLiteException) { } + } + } + + private async Task SyncCiphersAsync(IDictionary serverCiphers) { if(!_authService.IsAuthenticated) { @@ -367,16 +416,29 @@ namespace Bit.App.Services .GroupBy(a => a.LoginId) .ToDictionary(g => g.Key); - foreach(var serverCipher in serviceCiphers) + var cipherCollections = new List(); + foreach(var serverCipher in serverCiphers) { if(!_authService.IsAuthenticated) { return; } + var collectionForThisCipher = serverCipher.Value.CollectionIds?.Select(cid => new CipherCollectionData + { + CipherId = serverCipher.Value.Id, + CollectionId = cid, + UserId = _authService.UserId + }).ToList(); + + if(collectionForThisCipher != null && collectionForThisCipher.Any()) + { + cipherCollections.AddRange(collectionForThisCipher); + } + try { - var localCipher = localCiphers.ContainsKey(serverCipher.Value.Id) ? + var localCipher = localCiphers.ContainsKey(serverCipher.Value.Id) ? localCiphers[serverCipher.Value.Id] : null; var data = new CipherData(serverCipher.Value, _authService.UserId); @@ -404,7 +466,7 @@ namespace Bit.App.Services catch(SQLite.SQLiteException) { } } - foreach(var cipher in localCiphers.Where(local => !serviceCiphers.ContainsKey(local.Key))) + foreach(var cipher in localCiphers.Where(local => !serverCiphers.ContainsKey(local.Key))) { try { @@ -412,6 +474,21 @@ namespace Bit.App.Services } catch(SQLite.SQLiteException) { } } + + try + { + await _cipherCollectionRepository.DeleteByUserIdAsync(_authService.UserId).ConfigureAwait(false); + } + catch(SQLite.SQLiteException) { } + + foreach(var cipherCollection in cipherCollections) + { + try + { + await _cipherCollectionRepository.InsertAsync(cipherCollection).ConfigureAwait(false); + } + catch(SQLite.SQLiteException) { } + } } private async Task SyncDomainsAsync(DomainsResponse serverDomains) diff --git a/src/iOS.Extension/LoadingViewController.cs b/src/iOS.Extension/LoadingViewController.cs index fe5058246..9887d6511 100644 --- a/src/iOS.Extension/LoadingViewController.cs +++ b/src/iOS.Extension/LoadingViewController.cs @@ -301,6 +301,8 @@ namespace Bit.iOS.Extension container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); // Other container.RegisterSingleton(CrossConnectivity.Current); diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 53b96584a..4ba1a64db 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -285,6 +285,8 @@ namespace Bit.iOS container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); // Other container.RegisterSingleton(CrossConnectivity.Current);