Added icons for iOS. Broke out data access into repositories. Added syncing service.

This commit is contained in:
Kyle Spearrin 2016-05-06 00:17:38 -04:00
parent 24a5a16723
commit decd3fc24e
46 changed files with 773 additions and 150 deletions

View File

@ -15,6 +15,7 @@ using Bit.Android.Services;
using Plugin.Settings;
using Plugin.Connectivity;
using Acr.UserDialogs;
using Bit.App.Repositories;
namespace Bit.Android
{
@ -40,15 +41,23 @@ namespace Bit.Android
var container = new UnityContainer();
container
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager())
// Services
.RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager())
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager())
.RegisterType<ISecureStorageService, KeyStoreStorageService>(new ContainerControlledLifetimeManager())
.RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager())
.RegisterType<IApiService, ApiService>(new ContainerControlledLifetimeManager())
.RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager())
.RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteService, SiteService>(new ContainerControlledLifetimeManager())
.RegisterType<ISyncService, SyncService>(new ContainerControlledLifetimeManager())
// Repositories
.RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteRepository, SiteRepository>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteApiRepository, SiteApiRepository>(new ContainerControlledLifetimeManager())
.RegisterType<IAuthApiRepository, AuthApiRepository>(new ContainerControlledLifetimeManager())
// Other
.RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager())
.RegisterInstance(CrossConnectivity.Current, new ContainerControlledLifetimeManager())
.RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager());

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models.Api;
namespace Bit.App.Abstractions
{
public interface IApiRepository<TRequest, TResponse, TId>
where TRequest : class
where TResponse : class
where TId : IEquatable<TId>
{
Task<ApiResult<TResponse>> GetByIdAsync(TId id);
Task<ApiResult<ListResponse<TResponse>>> GetAsync();
Task<ApiResult<TResponse>> PostAsync(TRequest requestObj);
Task<ApiResult<TResponse>> PutAsync(TId id, TRequest requestObj);
Task<ApiResult<object>> DeleteAsync(TId id);
}
}

View File

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Bit.App.Models.Api;
namespace Bit.App.Abstractions
{
public interface IAuthApiRepository
{
Task<ApiResult<TokenResponse>> PostTokenAsync(TokenRequest requestObj);
Task<ApiResult<TokenResponse>> PostTokenTwoFactorAsync(TokenTwoFactorRequest requestObj);
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models.Api;
namespace Bit.App.Abstractions
{
public interface IFolderApiRepository : IApiRepository<FolderRequest, FolderResponse, string>
{
Task<ApiResult<ListResponse<FolderResponse>>> GetByRevisionDateAsync(DateTime since);
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models.Data;
namespace Bit.App.Abstractions
{
public interface IFolderRepository : IRepository<FolderData, string>
{
Task<IEnumerable<FolderData>> GetAllByUserIdAsync(string userId);
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Bit.App.Abstractions
{
public interface IRepository<T, TId>
where T : class, IDataObject<TId>, new()
where TId : IEquatable<TId>
{
Task<T> GetByIdAsync(TId id);
Task<IEnumerable<T>> GetAllAsync();
Task UpdateAsync(T obj);
Task InsertAsync(T obj);
Task DeleteAsync(TId id);
Task DeleteAsync(T obj);
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models.Api;
namespace Bit.App.Abstractions
{
public interface ISiteApiRepository : IApiRepository<SiteRequest, SiteResponse, string>
{
Task<ApiResult<ListResponse<SiteResponse>>> GetByRevisionDateAsync(DateTime since);
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Models.Data;
namespace Bit.App.Abstractions
{
public interface ISiteRepository : IRepository<SiteData, string>
{
Task<IEnumerable<SiteData>> GetAllByUserIdAsync(string userId);
}
}

View File

@ -1,13 +0,0 @@
using System.Net.Http;
using System.Threading.Tasks;
using Bit.App.Models.Api;
namespace Bit.App.Abstractions
{
public interface IApiService
{
HttpClient Client { get; set; }
Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response);
}
}

View File

@ -7,6 +7,7 @@ namespace Bit.App.Abstractions
{
public interface ISiteService
{
Task<Site> GetByIdAsync(string id);
Task<IEnumerable<Site>> GetAllAsync();
Task<ApiResult<SiteResponse>> SaveAsync(Site site);
Task<ApiResult<object>> DeleteAsync(string id);

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Bit.App.Abstractions
{
public interface ISyncService
{
Task<bool> SyncAsync();
}
}

View File

@ -48,8 +48,10 @@
<Compile Include="Models\Api\Request\FolderRequest.cs" />
<Compile Include="Models\Api\Request\SiteRequest.cs" />
<Compile Include="Models\Api\Request\TokenRequest.cs" />
<Compile Include="Models\Api\Request\TokenTwoFactorRequest.cs" />
<Compile Include="Models\Api\Response\ErrorResponse.cs" />
<Compile Include="Models\Api\Response\FolderResponse.cs" />
<Compile Include="Models\Api\Response\ListResponse.cs" />
<Compile Include="Models\Api\Response\SiteResponse.cs" />
<Compile Include="Models\Api\Response\TokenResponse.cs" />
<Compile Include="Models\Api\Response\ProfileResponse.cs" />
@ -65,13 +67,28 @@
<Compile Include="Pages\SyncPage.cs" />
<Compile Include="Pages\SettingsPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Abstractions\Repositories\ISiteRepository.cs" />
<Compile Include="Repositories\ApiRepository.cs" />
<Compile Include="Repositories\BaseApiRepository.cs" />
<Compile Include="Abstractions\Repositories\IApiRepository.cs" />
<Compile Include="Abstractions\Repositories\IFolderApiRepository.cs" />
<Compile Include="Abstractions\Repositories\ISiteApiRepository.cs" />
<Compile Include="Repositories\AuthApiRepository.cs" />
<Compile Include="Abstractions\Repositories\IAuthApiRepository.cs" />
<Compile Include="Repositories\SiteApiRepository.cs" />
<Compile Include="Repositories\FolderApiRepository.cs" />
<Compile Include="Repositories\SiteRepository.cs" />
<Compile Include="Repositories\FolderRepository.cs" />
<Compile Include="Abstractions\Repositories\IFolderRepository.cs" />
<Compile Include="Abstractions\Repositories\IRepository.cs" />
<Compile Include="Services\DatabaseService.cs" />
<Compile Include="Services\FolderService.cs" />
<Compile Include="Services\Repository.cs" />
<Compile Include="Abstractions\Services\IApiService.cs" />
<Compile Include="Repositories\Repository.cs" />
<Compile Include="Abstractions\Services\IAuthService.cs" />
<Compile Include="Abstractions\Services\ICryptoService.cs" />
<Compile Include="Abstractions\Services\IDatabaseService.cs" />
<Compile Include="Abstractions\Services\ISyncService.cs" />
<Compile Include="Services\SyncService.cs" />
<Compile Include="Services\SiteService.cs" />
<Compile Include="Services\AuthService.cs" />
<Compile Include="Services\CryptoService.cs" />
@ -83,7 +100,6 @@
<Compile Include="Pages\VaultViewSitePage.cs" />
<Compile Include="Pages\VaultEditSitePage.cs" />
<Compile Include="Pages\VaultListPage.cs" />
<Compile Include="Services\ApiService.cs" />
<Compile Include="Utilities\Extentions.cs" />
<Compile Include="Utilities\TokenHttpRequestMessage.cs" />
</ItemGroup>

View File

@ -0,0 +1,8 @@
namespace Bit.App.Models.Api
{
public class TokenTwoFactorRequest
{
public string Code { get; set; }
public string Provider { get; set; }
}
}

View File

@ -1,8 +1,11 @@
namespace Bit.App.Models.Api
using System;
namespace Bit.App.Models.Api
{
public class FolderResponse
{
public string Id { get; set; }
public string Name { get; set; }
public DateTime RevisionDate { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Bit.App.Models.Api
{
public class ListResponse<T>
{
public ListResponse(IEnumerable<T> data)
{
Data = data;
}
public IEnumerable<T> Data { get; set; }
}
}

View File

@ -1,4 +1,6 @@
namespace Bit.App.Models.Api
using System;
namespace Bit.App.Models.Api
{
public class SiteResponse
{
@ -9,6 +11,7 @@
public string Username { get; set; }
public string Password { get; set; }
public string Notes { get; set; }
public DateTime RevisionDate { get; set; }
// Expandables
public FolderResponse Folder { get; set; }

View File

@ -1,5 +1,4 @@
using System;
using Bit.App.Abstractions;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -8,18 +7,21 @@ namespace Bit.App.Pages
{
public MainPage()
{
var vaultNavigation = new NavigationPage(new VaultListPage());
vaultNavigation.BarBackgroundColor = Color.FromHex("3c8dbc");
vaultNavigation.BarTextColor = Color.FromHex("ffffff");
vaultNavigation.Title = "My Vault";
var settingsNavigation = new NavigationPage(new SettingsPage());
settingsNavigation.BarBackgroundColor = Color.FromHex("3c8dbc");
settingsNavigation.BarTextColor = Color.FromHex("ffffff");
var vaultNavigation = new NavigationPage(new VaultListPage());
var syncPage = new SyncPage();
vaultNavigation.BarBackgroundColor = settingsNavigation.BarBackgroundColor = Color.FromHex("3c8dbc");
vaultNavigation.BarTextColor = settingsNavigation.BarTextColor = Color.FromHex("ffffff");
vaultNavigation.Title = "My Vault";
vaultNavigation.Icon = "fa-lock";
settingsNavigation.Title = "Settings";
settingsNavigation.Icon = "fa-cogs";
Children.Add(vaultNavigation);
Children.Add(new SyncPage());
Children.Add(syncPage);
Children.Add(settingsNavigation);
}
}

View File

@ -1,14 +1,54 @@
using System;
using System.Threading.Tasks;
using Acr.UserDialogs;
using Bit.App.Abstractions;
using Xamarin.Forms;
using XLabs.Ioc;
namespace Bit.App.Pages
{
public class SyncPage : ContentPage
{
private readonly ISyncService _syncService;
private readonly IUserDialogs _userDialogs;
public SyncPage()
{
_syncService = Resolver.Resolve<ISyncService>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
Init();
}
public void Init()
{
var syncButton = new Button
{
Text = "Sync Vault",
Command = new Command(async () => await SyncAsync())
};
var stackLayout = new StackLayout { };
stackLayout.Children.Add(syncButton);
Title = "Sync";
Content = null;
Content = stackLayout;
Icon = "fa-refresh";
}
public async Task SyncAsync()
{
_userDialogs.ShowLoading("Syncing...", MaskType.Black);
var succeeded = await _syncService.SyncAsync();
_userDialogs.HideLoading();
if(succeeded)
{
_userDialogs.SuccessToast("Syncing complete.");
}
else
{
_userDialogs.ErrorToast("Syncing failed.");
}
}
}
}

View File

@ -31,21 +31,14 @@ namespace Bit.App.Pages
{
ToolbarItems.Add(new AddSiteToolBarItem(this));
var moreAction = new MenuItem { Text = "More" };
moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
moreAction.Clicked += MoreClickedAsync;
var deleteAction = new MenuItem { Text = "Delete", IsDestructive = true };
deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
deleteAction.Clicked += DeleteClickedAsync;
var listView = new ListView { IsGroupingEnabled = true, ItemsSource = Folders };
listView.GroupDisplayBinding = new Binding("Name");
listView.ItemSelected += SiteSelected;
listView.ItemTemplate = new DataTemplate(() => new VaultListViewCell(moreAction, deleteAction));
listView.ItemTemplate = new DataTemplate(() => new VaultListViewCell(this));
Title = "My Vault";
Content = listView;
NavigationPage.SetBackButtonTitle(this, string.Empty);
}
protected override void OnAppearing()
@ -122,7 +115,7 @@ namespace Bit.App.Pages
{
_page = page;
Text = "Add";
Icon = "";
Icon = "fa-plus";
Clicked += ClickedItem;
}
@ -144,12 +137,20 @@ namespace Bit.App.Pages
private class VaultListViewCell : TextCell
{
public VaultListViewCell(MenuItem moreMenuItem, MenuItem deleteMenuItem)
public VaultListViewCell(VaultListPage page)
{
var moreAction = new MenuItem { Text = "More" };
moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
moreAction.Clicked += page.MoreClickedAsync;
var deleteAction = new MenuItem { Text = "Delete", IsDestructive = true };
deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
deleteAction.Clicked += page.DeleteClickedAsync;
this.SetBinding<VaultView.Site>(TextProperty, s => s.Name);
this.SetBinding<VaultView.Site>(DetailProperty, s => s.Username);
ContextActions.Add(moreMenuItem);
ContextActions.Add(deleteMenuItem);
ContextActions.Add(moreAction);
ContextActions.Add(deleteAction);
}
}
}

View File

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
using Newtonsoft.Json;
namespace Bit.App.Repositories
{
public abstract class ApiRepository<TRequest, TResponse, TId> : BaseApiRepository, IApiRepository<TRequest, TResponse, TId>
where TId : IEquatable<TId>
where TRequest : class
where TResponse : class
{
public ApiRepository()
{ }
public virtual async Task<ApiResult<TResponse>> GetByIdAsync(TId id)
{
var requestMessage = new TokenHttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/", id)),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<TResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseContent);
return ApiResult<TResponse>.Success(responseObj, response.StatusCode);
}
public virtual async Task<ApiResult<ListResponse<TResponse>>> GetAsync()
{
var requestMessage = new TokenHttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(Client.BaseAddress, ApiRoute),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<ListResponse<TResponse>>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<ListResponse<TResponse>>(responseContent);
return ApiResult<ListResponse<TResponse>>.Success(responseObj, response.StatusCode);
}
public virtual async Task<ApiResult<TResponse>> PostAsync(TRequest requestObj)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
Method = HttpMethod.Post,
RequestUri = new Uri(Client.BaseAddress, ApiRoute),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<TResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseContent);
return ApiResult<TResponse>.Success(responseObj, response.StatusCode);
}
public virtual async Task<ApiResult<TResponse>> PutAsync(TId id, TRequest requestObj)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
Method = HttpMethod.Put,
RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/", id)),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<TResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseContent);
return ApiResult<TResponse>.Success(responseObj, response.StatusCode);
}
public virtual async Task<ApiResult<object>> DeleteAsync(TId id)
{
var requestMessage = new TokenHttpRequestMessage()
{
Method = HttpMethod.Delete,
RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/", id)),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<object>(response);
}
return ApiResult<object>.Success(null, response.StatusCode);
}
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
using Newtonsoft.Json;
namespace Bit.App.Repositories
{
public class AuthApiRepository : BaseApiRepository, IAuthApiRepository
{
protected override string ApiRoute => "auth";
public virtual async Task<ApiResult<TokenResponse>> PostTokenAsync(TokenRequest requestObj)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
Method = HttpMethod.Post,
RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/token")),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<TokenResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
return ApiResult<TokenResponse>.Success(responseObj, response.StatusCode);
}
public virtual async Task<ApiResult<TokenResponse>> PostTokenTwoFactorAsync(TokenTwoFactorRequest requestObj)
{
var requestMessage = new TokenHttpRequestMessage(requestObj)
{
Method = HttpMethod.Post,
RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "/token/two-factor")),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<TokenResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
return ApiResult<TokenResponse>.Success(responseObj, response.StatusCode);
}
}
}

View File

@ -1,23 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
using ModernHttpClient;
using Newtonsoft.Json;
namespace Bit.App.Services
namespace Bit.App.Repositories
{
public class ApiService : IApiService
public abstract class BaseApiRepository
{
public ApiService()
public BaseApiRepository()
{
Client = new HttpClient(new NativeMessageHandler());
Client.BaseAddress = new Uri("https://api.bitwarden.com");
Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
public HttpClient Client { get; set; }
protected virtual HttpClient Client { get; private set; }
protected abstract string ApiRoute { get; }
public async Task<ApiResult<T>> HandleErrorAsync<T>(HttpResponseMessage response)
{

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
using Newtonsoft.Json;
namespace Bit.App.Repositories
{
public class FolderApiRepository : ApiRepository<FolderRequest, FolderResponse, string>, IFolderApiRepository
{
protected override string ApiRoute => "folders";
public virtual async Task<ApiResult<ListResponse<FolderResponse>>> GetByRevisionDateAsync(DateTime since)
{
var requestMessage = new TokenHttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "?since=", since)),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<ListResponse<FolderResponse>>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<ListResponse<FolderResponse>>(responseContent);
return ApiResult<ListResponse<FolderResponse>>.Success(responseObj, response.StatusCode);
}
}
}

View File

@ -0,0 +1,22 @@
using System;
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 FolderRepository : Repository<FolderData, string>, IFolderRepository
{
public FolderRepository(ISqlService sqlService)
: base(sqlService)
{ }
public Task<IEnumerable<FolderData>> GetAllByUserIdAsync(string userId)
{
var folders = Connection.Table<FolderData>().Where(f => f.UserId == userId).Cast<FolderData>();
return Task.FromResult(folders);
}
}
}

View File

@ -5,9 +5,9 @@ using System.Threading.Tasks;
using Bit.App.Abstractions;
using SQLite;
namespace Bit.App.Services
namespace Bit.App.Repositories
{
public abstract class Repository<T, TId>
public abstract class Repository<T, TId> : IRepository<T, TId>
where TId : IEquatable<TId>
where T : class, IDataObject<TId>, new()
{
@ -18,34 +18,34 @@ namespace Bit.App.Services
protected SQLiteConnection Connection { get; private set; }
protected virtual Task<T> GetByIdAsync(TId id)
public virtual Task<T> GetByIdAsync(TId id)
{
return Task.FromResult(Connection.Get<T>(id));
}
protected virtual Task<IEnumerable<T>> GetAllAsync()
public virtual Task<IEnumerable<T>> GetAllAsync()
{
return Task.FromResult(Connection.Table<T>().Cast<T>());
}
protected virtual Task CreateAsync(T obj)
public virtual Task InsertAsync(T obj)
{
Connection.Insert(obj);
return Task.FromResult(0);
}
protected virtual Task ReplaceAsync(T obj)
public virtual Task UpdateAsync(T obj)
{
Connection.Update(obj);
return Task.FromResult(0);
}
protected virtual async Task DeleteAsync(T obj)
public virtual async Task DeleteAsync(T obj)
{
await DeleteAsync(obj.Id);
}
protected virtual Task DeleteAsync(TId id)
public virtual Task DeleteAsync(TId id)
{
Connection.Delete<T>(id);
return Task.FromResult(0);

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Api;
using Newtonsoft.Json;
namespace Bit.App.Repositories
{
public class SiteApiRepository : ApiRepository<SiteRequest, SiteResponse, string>, ISiteApiRepository
{
protected override string ApiRoute => "sites";
public virtual async Task<ApiResult<ListResponse<SiteResponse>>> GetByRevisionDateAsync(DateTime since)
{
var requestMessage = new TokenHttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(Client.BaseAddress, string.Concat(ApiRoute, "?since=", since)),
};
var response = await Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await HandleErrorAsync<ListResponse<SiteResponse>>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<ListResponse<SiteResponse>>(responseContent);
return ApiResult<ListResponse<SiteResponse>>.Success(responseObj, response.StatusCode);
}
}
}

View File

@ -0,0 +1,22 @@
using System;
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 SiteRepository : Repository<SiteData, string>, ISiteRepository
{
public SiteRepository(ISqlService sqlService)
: base(sqlService)
{ }
public Task<IEnumerable<SiteData>> GetAllByUserIdAsync(string userId)
{
var sites = Connection.Table<SiteData>().Where(f => f.UserId == userId).Cast<SiteData>();
return Task.FromResult(sites);
}
}
}

View File

@ -17,7 +17,7 @@ namespace Bit.App.Services
private readonly ISecureStorageService _secureStorage;
private readonly ISettings _settings;
private readonly ICryptoService _cryptoService;
private readonly IApiService _apiService;
private readonly IAuthApiRepository _authApiRepository;
private string _token;
private string _userId;
@ -26,12 +26,12 @@ namespace Bit.App.Services
ISecureStorageService secureStorage,
ISettings settings,
ICryptoService cryptoService,
IApiService apiService)
IAuthApiRepository authApiRepository)
{
_secureStorage = secureStorage;
_settings = settings;
_cryptoService = cryptoService;
_apiService = apiService;
_authApiRepository = authApiRepository;
}
public string Token
@ -110,16 +110,8 @@ namespace Bit.App.Services
public async Task<ApiResult<TokenResponse>> TokenPostAsync(TokenRequest request)
{
var requestContent = JsonConvert.SerializeObject(request);
var response = await _apiService.Client.PostAsync("/auth/token", new StringContent(requestContent, Encoding.UTF8, "application/json"));
if(!response.IsSuccessStatusCode)
{
return await _apiService.HandleErrorAsync<TokenResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
return ApiResult<TokenResponse>.Success(responseObj, response.StatusCode);
// TODO: move more logic in here
return await _authApiRepository.PostTokenAsync(request);
}
}
}

View File

@ -6,70 +6,73 @@ using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Models.Data;
using Bit.App.Models.Api;
using Newtonsoft.Json;
using System.Net.Http;
namespace Bit.App.Services
{
public class FolderService : Repository<FolderData, string>, IFolderService
public class FolderService : IFolderService
{
private readonly IFolderRepository _folderRepository;
private readonly IAuthService _authService;
private readonly IApiService _apiService;
private readonly IFolderApiRepository _folderApiRepository;
public FolderService(
ISqlService sqlService,
IFolderRepository folderRepository,
IAuthService authService,
IApiService apiService)
: base(sqlService)
IFolderApiRepository folderApiRepository)
{
_folderRepository = folderRepository;
_authService = authService;
_apiService = apiService;
_folderApiRepository = folderApiRepository;
}
public new Task<Folder> GetByIdAsync(string id)
public async Task<Folder> GetByIdAsync(string id)
{
var data = Connection.Table<FolderData>().Where(f => f.UserId == _authService.UserId && f.Id == id).FirstOrDefault();
var data = await _folderRepository.GetByIdAsync(id);
if(data == null || data.UserId != _authService.UserId)
{
return null;
}
var folder = new Folder(data);
return Task.FromResult(folder);
return folder;
}
public new Task<IEnumerable<Folder>> GetAllAsync()
public async Task<IEnumerable<Folder>> GetAllAsync()
{
var data = Connection.Table<FolderData>().Where(f => f.UserId == _authService.UserId).Cast<FolderData>();
var data = await _folderRepository.GetAllByUserIdAsync(_authService.UserId);
var folders = data.Select(f => new Folder(f));
return Task.FromResult(folders);
return folders;
}
public async Task<ApiResult<FolderResponse>> SaveAsync(Folder folder)
{
ApiResult<FolderResponse> response = null;
var request = new FolderRequest(folder);
var requestMessage = new TokenHttpRequestMessage(request)
{
Method = folder.Id == null ? HttpMethod.Post : HttpMethod.Put,
RequestUri = new Uri(_apiService.Client.BaseAddress, folder.Id == null ? "/folders" : $"/folders/{folder.Id}"),
};
var response = await _apiService.Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await _apiService.HandleErrorAsync<FolderResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<FolderResponse>(responseContent);
var data = new FolderData(responseObj, _authService.UserId);
if(folder.Id == null)
{
await CreateAsync(data);
folder.Id = responseObj.Id;
response = await _folderApiRepository.PostAsync(request);
}
else
{
await ReplaceAsync(data);
response = await _folderApiRepository.PutAsync(folder.Id, request);
}
return ApiResult<FolderResponse>.Success(responseObj, response.StatusCode);
if(response.Succeeded)
{
var data = new FolderData(response.Result, _authService.UserId);
if(folder.Id == null)
{
await _folderRepository.InsertAsync(data);
folder.Id = data.Id;
}
else
{
await _folderRepository.UpdateAsync(data);
}
}
return response;
}
}
}

View File

@ -1,86 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Models.Api;
using Bit.App.Models.Data;
using Newtonsoft.Json;
namespace Bit.App.Services
{
public class SiteService : Repository<SiteData, string>, ISiteService
public class SiteService : ISiteService
{
private readonly ISiteRepository _siteRepository;
private readonly IAuthService _authService;
private readonly IApiService _apiService;
private readonly ISiteApiRepository _siteApiRepository;
public SiteService(
ISqlService sqlService,
ISiteRepository siteRepository,
IAuthService authService,
IApiService apiService)
: base(sqlService)
ISiteApiRepository siteApiRepository)
{
_siteRepository = siteRepository;
_authService = authService;
_apiService = apiService;
_siteApiRepository = siteApiRepository;
}
public new Task<IEnumerable<Site>> GetAllAsync()
public async Task<Site> GetByIdAsync(string id)
{
var data = Connection.Table<SiteData>().Where(f => f.UserId == _authService.UserId).Cast<SiteData>();
var sites = data.Select(s => new Site(s));
return Task.FromResult(sites);
var data = await _siteRepository.GetByIdAsync(id);
if(data == null || data.UserId != _authService.UserId)
{
return null;
}
var site = new Site(data);
return site;
}
public async Task<IEnumerable<Site>> GetAllAsync()
{
var data = await _siteRepository.GetAllByUserIdAsync(_authService.UserId);
var sites = data.Select(f => new Site(f));
return sites;
}
public async Task<ApiResult<SiteResponse>> SaveAsync(Site site)
{
ApiResult<SiteResponse> response = null;
var request = new SiteRequest(site);
var requestMessage = new TokenHttpRequestMessage(request)
{
Method = site.Id == null ? HttpMethod.Post : HttpMethod.Put,
RequestUri = new Uri(_apiService.Client.BaseAddress, site.Id == null ? "/sites" : $"/folders/{site.Id}")
};
var response = await _apiService.Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await _apiService.HandleErrorAsync<SiteResponse>(response);
}
var responseContent = await response.Content.ReadAsStringAsync();
var responseObj = JsonConvert.DeserializeObject<SiteResponse>(responseContent);
var data = new SiteData(responseObj, _authService.UserId);
if(site.Id == null)
{
await base.CreateAsync(data);
site.Id = responseObj.Id;
response = await _siteApiRepository.PostAsync(request);
}
else
{
await base.ReplaceAsync(data);
response = await _siteApiRepository.PutAsync(site.Id, request);
}
return ApiResult<SiteResponse>.Success(responseObj, response.StatusCode);
if(response.Succeeded)
{
var data = new SiteData(response.Result, _authService.UserId);
if(site.Id == null)
{
await _siteRepository.InsertAsync(data);
site.Id = data.Id;
}
else
{
await _siteRepository.UpdateAsync(data);
}
}
return response;
}
public new async Task<ApiResult<object>> DeleteAsync(string id)
public async Task<ApiResult<object>> DeleteAsync(string id)
{
var requestMessage = new TokenHttpRequestMessage
ApiResult<object> response = await _siteApiRepository.DeleteAsync(id);
if(response.Succeeded)
{
Method = HttpMethod.Delete,
RequestUri = new Uri(_apiService.Client.BaseAddress, $"/sites/{id}")
};
var response = await _apiService.Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)
{
return await _apiService.HandleErrorAsync<object>(response);
await _siteRepository.DeleteAsync(id);
}
await base.DeleteAsync(id);
return ApiResult<object>.Success(null, response.StatusCode);
return response;
}
}
}

View File

@ -0,0 +1,108 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models.Data;
namespace Bit.App.Services
{
public class SyncService : ISyncService
{
private readonly IFolderApiRepository _folderApiRepository;
private readonly ISiteApiRepository _siteApiRepository;
private readonly IFolderRepository _folderRepository;
private readonly ISiteRepository _siteRepository;
private readonly IAuthService _authService;
public SyncService(
IFolderApiRepository folderApiRepository,
ISiteApiRepository siteApiRepository,
IFolderRepository folderRepository,
ISiteRepository siteRepository,
IAuthService authService)
{
_folderApiRepository = folderApiRepository;
_siteApiRepository = siteApiRepository;
_folderRepository = folderRepository;
_siteRepository = siteRepository;
_authService = authService;
}
public async Task<bool> SyncAsync()
{
// TODO: store now in settings and only fetch from last time stored
var now = DateTime.UtcNow.AddYears(-100);
var siteTask = await SyncSitesAsync(now);
var folderTask = await SyncFoldersAsync(now);
return siteTask && folderTask;
}
private async Task<bool> SyncFoldersAsync(DateTime now)
{
var folderResponse = await _folderApiRepository.GetAsync();
if(!folderResponse.Succeeded)
{
return false;
}
var serverFolders = folderResponse.Result.Data;
var folders = await _folderRepository.GetAllByUserIdAsync(_authService.UserId);
foreach(var serverFolder in serverFolders.Where(f => f.RevisionDate >= now))
{
var data = new FolderData(serverFolder, _authService.UserId);
var existingLocalFolder = folders.SingleOrDefault(f => f.Id == serverFolder.Id);
if(existingLocalFolder == null)
{
await _folderRepository.InsertAsync(data);
}
else
{
await _folderRepository.UpdateAsync(data);
}
}
foreach(var folder in folders.Where(localFolder => !serverFolders.Any(serverFolder => serverFolder.Id == localFolder.Id)))
{
await _folderRepository.DeleteAsync(folder.Id);
}
return true;
}
private async Task<bool> SyncSitesAsync(DateTime now)
{
var siteResponse = await _siteApiRepository.GetAsync();
if(!siteResponse.Succeeded)
{
return false;
}
var serverSites = siteResponse.Result.Data;
var sites = await _siteRepository.GetAllByUserIdAsync(_authService.UserId);
foreach(var serverSite in serverSites.Where(s => s.RevisionDate >= now))
{
var data = new SiteData(serverSite, _authService.UserId);
var existingLocalSite = sites.SingleOrDefault(s => s.Id == serverSite.Id);
if(existingLocalSite == null)
{
await _siteRepository.InsertAsync(data);
}
else
{
await _siteRepository.UpdateAsync(data);
}
}
foreach(var site in sites.Where(localSite => !serverSites.Any(serverSite => serverSite.Id == localSite.Id)))
{
await _siteRepository.DeleteAsync(site.Id);
}
return true;
}
}
}

View File

@ -13,6 +13,7 @@ using Bit.iOS.Services;
using Plugin.Settings;
using Plugin.Connectivity;
using Acr.UserDialogs;
using Bit.App.Repositories;
namespace Bit.iOS
{
@ -48,15 +49,23 @@ namespace Bit.iOS
var container = new UnityContainer();
container
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager())
// Services
.RegisterType<IDatabaseService, DatabaseService>(new ContainerControlledLifetimeManager())
.RegisterType<ISqlService, SqlService>(new ContainerControlledLifetimeManager())
.RegisterType<ISecureStorageService, KeyChainStorageService>(new ContainerControlledLifetimeManager())
.RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager())
.RegisterType<IApiService, ApiService>(new ContainerControlledLifetimeManager())
.RegisterType<ICryptoService, CryptoService>(new ContainerControlledLifetimeManager())
.RegisterType<IAuthService, AuthService>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderService, FolderService>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteService, SiteService>(new ContainerControlledLifetimeManager())
.RegisterType<ISyncService, SyncService>(new ContainerControlledLifetimeManager())
// Repositories
.RegisterType<IFolderRepository, FolderRepository>(new ContainerControlledLifetimeManager())
.RegisterType<IFolderApiRepository, FolderApiRepository>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteRepository, SiteRepository>(new ContainerControlledLifetimeManager())
.RegisterType<ISiteApiRepository, SiteApiRepository>(new ContainerControlledLifetimeManager())
.RegisterType<IAuthApiRepository, AuthApiRepository>(new ContainerControlledLifetimeManager())
// Other
.RegisterInstance(CrossSettings.Current, new ContainerControlledLifetimeManager())
.RegisterInstance(CrossConnectivity.Current, new ContainerControlledLifetimeManager())
.RegisterInstance(UserDialogs.Instance, new ContainerControlledLifetimeManager());

View File

@ -26,7 +26,7 @@
<key>CFBundleDisplayName</key>
<string>bitwarden</string>
<key>CFBundleIdentifier</key>
<string>com.bitwarden.bitwarden</string>
<string>com.bitwarden.vault</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>CFBundleIconFiles</key>

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -50,6 +50,9 @@
<CodesignKey>iPhone Developer</CodesignKey>
<MtouchDebug>true</MtouchDebug>
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
<CodesignProvision>2ae5608a-6142-4e1d-9344-326d1982b392</CodesignProvision>
<CodesignResourceRules />
<CodesignExtraArgs />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' ">
<DebugType>none</DebugType>
@ -205,6 +208,42 @@
<Name>App</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-refresh.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-refresh%403x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-refresh%402x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-cogs.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-cogs%402x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-cogs%403x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-lock.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-lock%402x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-lock%403x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-plus.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-plus%402x.png" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\fa-plus%403x.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>