mirror of
https://github.com/bitwarden/mobile.git
synced 2024-12-27 17:08:00 +01:00
add collection syncing
This commit is contained in:
parent
3b44ede67e
commit
c9ceb09906
@ -226,6 +226,8 @@ namespace Bit.Android
|
||||
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
|
||||
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
|
||||
container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>();
|
||||
container.RegisterSingleton<ICollectionRepository, CollectionRepository>();
|
||||
container.RegisterSingleton<ICipherCollectionRepository, CipherCollectionRepository>();
|
||||
|
||||
// Other
|
||||
container.RegisterSingleton(CrossSettings.Current);
|
||||
|
@ -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<IEnumerable<CipherCollectionData>> GetAllByUserIdAsync(string userId);
|
||||
Task InsertAsync(CipherCollectionData obj);
|
||||
Task DeleteAsync(CipherCollectionData obj);
|
||||
Task DeleteByUserIdAsync(string userId);
|
||||
}
|
||||
}
|
11
src/App/Abstractions/Repositories/ICollectionRepository.cs
Normal file
11
src/App/Abstractions/Repositories/ICollectionRepository.cs
Normal file
@ -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<CollectionData, string>
|
||||
{
|
||||
Task<IEnumerable<CollectionData>> GetAllByUserIdAsync(string userId);
|
||||
}
|
||||
}
|
@ -36,6 +36,8 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Abstractions\Repositories\IAttachmentRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ICipherCollectionRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ICollectionRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ISyncApiRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ITwoFactorApiRepository.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ISettingsApiRepository.cs" />
|
||||
@ -107,6 +109,7 @@
|
||||
<Compile Include="Models\Api\FieldDataModel.cs" />
|
||||
<Compile Include="Models\Api\CardDataModel.cs" />
|
||||
<Compile Include="Models\Api\IdentityDataModel.cs" />
|
||||
<Compile Include="Models\Api\Response\CollectionResponse.cs" />
|
||||
<Compile Include="Models\Api\SecureNoteDataModel.cs" />
|
||||
<Compile Include="Models\Api\Request\DeviceTokenRequest.cs" />
|
||||
<Compile Include="Models\Api\Request\FolderRequest.cs" />
|
||||
@ -134,7 +137,10 @@
|
||||
<Compile Include="Models\CipherString.cs" />
|
||||
<Compile Include="Models\Data\AttachmentData.cs" />
|
||||
<Compile Include="Models\Attachment.cs" />
|
||||
<Compile Include="Models\Data\CipherCollectionData.cs" />
|
||||
<Compile Include="Models\Data\CollectionData.cs" />
|
||||
<Compile Include="Models\Field.cs" />
|
||||
<Compile Include="Models\Collection.cs" />
|
||||
<Compile Include="Models\Identity.cs" />
|
||||
<Compile Include="Models\Login.cs" />
|
||||
<Compile Include="Models\Page\VaultAttachmentsPageModel.cs" />
|
||||
@ -186,6 +192,9 @@
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Abstractions\Repositories\ICipherRepository.cs" />
|
||||
<Compile Include="Repositories\AttachmentRepository.cs" />
|
||||
<Compile Include="Repositories\BaseRepository.cs" />
|
||||
<Compile Include="Repositories\CipherCollectionRepository.cs" />
|
||||
<Compile Include="Repositories\CollectionRepository.cs" />
|
||||
<Compile Include="Repositories\SyncApiRepository.cs" />
|
||||
<Compile Include="Repositories\TwoFactorApiRepository.cs" />
|
||||
<Compile Include="Repositories\SettingsApiRepository.cs" />
|
||||
|
@ -17,6 +17,7 @@ namespace Bit.App.Models.Api
|
||||
public bool OrganizationUseTotp { get; set; }
|
||||
public JObject Data { get; set; }
|
||||
public IEnumerable<AttachmentResponse> Attachments { get; set; }
|
||||
public IEnumerable<string> CollectionIds { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
9
src/App/Models/Api/Response/CollectionResponse.cs
Normal file
9
src/App/Models/Api/Response/CollectionResponse.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ namespace Bit.App.Models.Api
|
||||
{
|
||||
public ProfileResponse Profile { get; set; }
|
||||
public IEnumerable<FolderResponse> Folders { get; set; }
|
||||
public IEnumerable<CollectionResponse> Collections { get; set; }
|
||||
public IEnumerable<CipherResponse> Ciphers { get; set; }
|
||||
public DomainsResponse Domains { get; set; }
|
||||
}
|
||||
|
29
src/App/Models/Collection.cs
Normal file
29
src/App/Models/Collection.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
18
src/App/Models/Data/CipherCollectionData.cs
Normal file
18
src/App/Models/Data/CipherCollectionData.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
36
src/App/Models/Data/CollectionData.cs
Normal file
36
src/App/Models/Data/CollectionData.cs
Normal file
@ -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<string>
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
src/App/Repositories/BaseRepository.cs
Normal file
15
src/App/Repositories/BaseRepository.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
40
src/App/Repositories/CipherCollectionRepository.cs
Normal file
40
src/App/Repositories/CipherCollectionRepository.cs
Normal file
@ -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<IEnumerable<CipherCollectionData>> GetAllByUserIdAsync(string userId)
|
||||
{
|
||||
var cipherCollections = Connection.Table<CipherCollectionData>().Where(f => f.UserId == userId)
|
||||
.Cast<CipherCollectionData>();
|
||||
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<CipherCollectionData>(obj.Id);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public virtual Task DeleteByUserIdAsync(string userId)
|
||||
{
|
||||
Connection.Execute("DELETE FROM CipherCollection WHERE UserId = ?", userId);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
21
src/App/Repositories/CollectionRepository.cs
Normal file
21
src/App/Repositories/CollectionRepository.cs
Normal file
@ -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<CollectionData, string>, ICollectionRepository
|
||||
{
|
||||
public CollectionRepository(ISqlService sqlService)
|
||||
: base(sqlService)
|
||||
{ }
|
||||
|
||||
public Task<IEnumerable<CollectionData>> GetAllByUserIdAsync(string userId)
|
||||
{
|
||||
var folders = Connection.Table<CollectionData>().Where(f => f.UserId == userId).Cast<CollectionData>();
|
||||
return Task.FromResult(folders);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<T, TId> : IRepository<T, TId>
|
||||
public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>
|
||||
where TId : IEquatable<TId>
|
||||
where T : class, IDataObject<TId>, new()
|
||||
{
|
||||
public Repository(ISqlService sqlService)
|
||||
{
|
||||
Connection = sqlService.GetConnection();
|
||||
}
|
||||
|
||||
protected SQLiteConnection Connection { get; private set; }
|
||||
: base(sqlService)
|
||||
{ }
|
||||
|
||||
public virtual Task<T> 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);
|
||||
|
@ -17,7 +17,9 @@ namespace Bit.App.Services
|
||||
public void CreateTables()
|
||||
{
|
||||
_connection.CreateTable<FolderData>();
|
||||
_connection.CreateTable<CollectionData>();
|
||||
_connection.CreateTable<CipherData>();
|
||||
_connection.CreateTable<CipherCollectionData>();
|
||||
_connection.CreateTable<AttachmentData>();
|
||||
_connection.CreateTable<SettingsData>();
|
||||
}
|
||||
|
@ -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;
|
||||
@ -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<string, CipherResponse> serviceCiphers)
|
||||
private async Task SyncCollectionsAsync(IDictionary<string, CollectionResponse> 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<string, CipherResponse> serverCiphers)
|
||||
{
|
||||
if(!_authService.IsAuthenticated)
|
||||
{
|
||||
@ -367,13 +416,26 @@ namespace Bit.App.Services
|
||||
.GroupBy(a => a.LoginId)
|
||||
.ToDictionary(g => g.Key);
|
||||
|
||||
foreach(var serverCipher in serviceCiphers)
|
||||
var cipherCollections = new List<CipherCollectionData>();
|
||||
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) ?
|
||||
@ -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)
|
||||
|
@ -301,6 +301,8 @@ namespace Bit.iOS.Extension
|
||||
container.RegisterSingleton<IAccountsApiRepository, AccountsApiRepository>();
|
||||
container.RegisterSingleton<ICipherApiRepository, CipherApiRepository>();
|
||||
container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>();
|
||||
container.RegisterSingleton<ICollectionRepository, CollectionRepository>();
|
||||
container.RegisterSingleton<ICipherCollectionRepository, CipherCollectionRepository>();
|
||||
|
||||
// Other
|
||||
container.RegisterSingleton(CrossConnectivity.Current);
|
||||
|
@ -285,6 +285,8 @@ namespace Bit.iOS
|
||||
container.RegisterSingleton<ISettingsApiRepository, SettingsApiRepository>();
|
||||
container.RegisterSingleton<ITwoFactorApiRepository, TwoFactorApiRepository>();
|
||||
container.RegisterSingleton<ISyncApiRepository, SyncApiRepository>();
|
||||
container.RegisterSingleton<ICollectionRepository, CollectionRepository>();
|
||||
container.RegisterSingleton<ICipherCollectionRepository, CipherCollectionRepository>();
|
||||
|
||||
// Other
|
||||
container.RegisterSingleton(CrossConnectivity.Current);
|
||||
|
Loading…
Reference in New Issue
Block a user