1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-10 05:13:31 +00:00
This commit is contained in:
Kyle Spearrin
2016-05-03 19:49:49 -04:00
parent 92e74274e0
commit 24a5a16723
15 changed files with 250 additions and 77 deletions

View File

@@ -9,5 +9,6 @@ namespace Bit.App.Abstractions
{ {
Task<IEnumerable<Site>> GetAllAsync(); Task<IEnumerable<Site>> GetAllAsync();
Task<ApiResult<SiteResponse>> SaveAsync(Site site); Task<ApiResult<SiteResponse>> SaveAsync(Site site);
Task<ApiResult<object>> DeleteAsync(string id);
} }
} }

View File

@@ -41,6 +41,7 @@
<Compile Include="Abstractions\Services\ISecureStorageService.cs" /> <Compile Include="Abstractions\Services\ISecureStorageService.cs" />
<Compile Include="Abstractions\Services\ISqlService.cs" /> <Compile Include="Abstractions\Services\ISqlService.cs" />
<Compile Include="Behaviors\EmailValidationBehavior.cs" /> <Compile Include="Behaviors\EmailValidationBehavior.cs" />
<Compile Include="Behaviors\ConnectivityBehavior.cs" />
<Compile Include="Behaviors\RequiredValidationBehavior.cs" /> <Compile Include="Behaviors\RequiredValidationBehavior.cs" />
<Compile Include="Models\Api\ApiError.cs" /> <Compile Include="Models\Api\ApiError.cs" />
<Compile Include="Models\Api\ApiResult.cs" /> <Compile Include="Models\Api\ApiResult.cs" />
@@ -84,6 +85,7 @@
<Compile Include="Pages\VaultListPage.cs" /> <Compile Include="Pages\VaultListPage.cs" />
<Compile Include="Services\ApiService.cs" /> <Compile Include="Services\ApiService.cs" />
<Compile Include="Utilities\Extentions.cs" /> <Compile Include="Utilities\Extentions.cs" />
<Compile Include="Utilities\TokenHttpRequestMessage.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -0,0 +1,43 @@
using System;
using Plugin.Connectivity.Abstractions;
using Xamarin.Forms;
using XLabs.Ioc;
namespace Bit.App.Behaviors
{
public class ConnectivityBehavior : Behavior<Element>
{
private readonly IConnectivity _connectivity;
public ConnectivityBehavior()
{
_connectivity = Resolver.Resolve<IConnectivity>();
}
private static readonly BindablePropertyKey IsValidPropertyKey = BindableProperty.CreateReadOnly("Connected", typeof(bool), typeof(ConnectivityBehavior), false);
public static readonly BindableProperty IsValidProperty = IsValidPropertyKey.BindableProperty;
public bool Connected
{
get { return (bool)GetValue(IsValidProperty); }
private set { SetValue(IsValidPropertyKey, value); }
}
protected override void OnAttachedTo(Element el)
{
_connectivity.ConnectivityChanged += ConnectivityChanged;
base.OnAttachedTo(el);
}
private void ConnectivityChanged(object sender, ConnectivityChangedEventArgs e)
{
Connected = e.IsConnected;
}
protected override void OnDetachingFrom(Element el)
{
_connectivity.ConnectivityChanged -= ConnectivityChanged;
base.OnDetachingFrom(el);
}
}
}

View File

@@ -18,9 +18,10 @@ namespace Bit.App.Behaviors
private set { SetValue(IsValidPropertyKey, value); } private set { SetValue(IsValidPropertyKey, value); }
} }
protected override void OnAttachedTo(Entry bindable) protected override void OnAttachedTo(Entry entry)
{ {
bindable.TextChanged += HandleTextChanged; entry.TextChanged += HandleTextChanged;
base.OnAttachedTo(entry);
} }
void HandleTextChanged(object sender, TextChangedEventArgs e) void HandleTextChanged(object sender, TextChangedEventArgs e)
@@ -29,9 +30,10 @@ namespace Bit.App.Behaviors
((Entry)sender).BackgroundColor = IsValid ? Color.Default : Color.Red; ((Entry)sender).BackgroundColor = IsValid ? Color.Default : Color.Red;
} }
protected override void OnDetachingFrom(Entry bindable) protected override void OnDetachingFrom(Entry entry)
{ {
bindable.TextChanged -= HandleTextChanged; entry.TextChanged -= HandleTextChanged;
base.OnDetachingFrom(entry);
} }
} }
} }

View File

@@ -14,9 +14,10 @@ namespace Bit.App.Behaviors
private set { SetValue(IsValidPropertyKey, value); } private set { SetValue(IsValidPropertyKey, value); }
} }
protected override void OnAttachedTo(Entry bindable) protected override void OnAttachedTo(Entry entry)
{ {
bindable.TextChanged += HandleTextChanged; entry.TextChanged += HandleTextChanged;
base.OnAttachedTo(entry);
} }
void HandleTextChanged(object sender, TextChangedEventArgs e) void HandleTextChanged(object sender, TextChangedEventArgs e)
@@ -25,9 +26,10 @@ namespace Bit.App.Behaviors
((Entry)sender).BackgroundColor = IsValid ? Color.Default : Color.Red; ((Entry)sender).BackgroundColor = IsValid ? Color.Default : Color.Red;
} }
protected override void OnDetachingFrom(Entry bindable) protected override void OnDetachingFrom(Entry entry)
{ {
bindable.TextChanged -= HandleTextChanged; entry.TextChanged -= HandleTextChanged;
base.OnDetachingFrom(entry);
} }
} }
} }

View File

@@ -1,8 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using XLabs.Ioc; using XLabs.Ioc;
@@ -10,6 +6,8 @@ namespace Bit.App.Models
{ {
public class CipherString public class CipherString
{ {
private string _decryptedValue;
public CipherString(string encryptedString) public CipherString(string encryptedString)
{ {
if(string.IsNullOrWhiteSpace(encryptedString) || !encryptedString.Contains("|")) if(string.IsNullOrWhiteSpace(encryptedString) || !encryptedString.Contains("|"))
@@ -43,8 +41,13 @@ namespace Bit.App.Models
public string Decrypt() public string Decrypt()
{ {
var cryptoService = Resolver.Resolve<ICryptoService>(); if(_decryptedValue == null)
return cryptoService.Decrypt(this); {
var cryptoService = Resolver.Resolve<ICryptoService>();
_decryptedValue = cryptoService.Decrypt(this);
}
return _decryptedValue;
} }
} }
} }

View File

@@ -27,6 +27,7 @@ namespace Bit.App.Models.Data
[PrimaryKey] [PrimaryKey]
public string Id { get; set; } public string Id { get; set; }
[Indexed]
public string UserId { get; set; } public string UserId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow; public DateTime RevisionDateTime { get; set; } = DateTime.UtcNow;

View File

@@ -38,6 +38,7 @@ namespace Bit.App.Models.Data
[PrimaryKey] [PrimaryKey]
public string Id { get; set; } public string Id { get; set; }
public string FolderId { get; set; } public string FolderId { get; set; }
[Indexed]
public string UserId { get; set; } public string UserId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Uri { get; set; } public string Uri { get; set; }

View File

@@ -8,40 +8,39 @@ namespace Bit.App.Models.View
{ {
public class Site public class Site
{ {
public Site(Models.Site site) public Site(Models.Site site, string folderId)
{ {
Id = site.Id; Id = site.Id;
FolderId = folderId;
Name = site.Name?.Decrypt(); Name = site.Name?.Decrypt();
Username = site.Username?.Decrypt(); Username = site.Username?.Decrypt();
} }
public string Id { get; set; } public string Id { get; set; }
public string FolderId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Username { get; set; } public string Username { get; set; }
} }
public class Folder : ObservableCollection<Site> public class Folder : ObservableCollection<Site>
{ {
public Folder(string name) { Name = name; } public Folder(IEnumerable<Models.Site> sites, string folderId = null)
public Folder(IEnumerable<Models.Site> sites)
{ {
Name = "(none)";
foreach(var site in sites) foreach(var site in sites)
{ {
Items.Add(new Site(site)); Items.Add(new Site(site, folderId));
} }
} }
public Folder(Models.Folder folder, IEnumerable<Models.Site> sites) public Folder(Models.Folder folder, IEnumerable<Models.Site> sites)
: this(sites) : this(sites, folder.Id)
{ {
Id = folder.Id; Id = folder.Id;
Name = folder.Name?.Decrypt(); Name = folder.Name?.Decrypt();
} }
public string Id { get; set; } public string Id { get; set; }
public string Name { get; set; } public string Name { get; set; } = "(none)";
public string FirstLetter { get { return Name.Substring(0, 1); } } public string FirstLetter { get { return Name.Substring(0, 1); } }
} }
} }

View File

@@ -20,7 +20,7 @@ namespace Bit.App.Pages
var folderService = Resolver.Resolve<IFolderService>(); var folderService = Resolver.Resolve<IFolderService>();
var userDialogs = Resolver.Resolve<IUserDialogs>(); var userDialogs = Resolver.Resolve<IUserDialogs>();
var folders = folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name); var folders = folderService.GetAllAsync().GetAwaiter().GetResult().OrderBy(f => f.Name?.Decrypt());
var uriEntry = new Entry { Keyboard = Keyboard.Url }; var uriEntry = new Entry { Keyboard = Keyboard.Url };
var nameEntry = new Entry(); var nameEntry = new Entry();

View File

@@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Acr.UserDialogs;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models.View; using Bit.App.Models.View;
using Xamarin.Forms; using Xamarin.Forms;
@@ -13,67 +14,143 @@ namespace Bit.App.Pages
{ {
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
private readonly ISiteService _siteService; private readonly ISiteService _siteService;
private ListView _listView = new ListView(); private readonly IUserDialogs _userDialogs;
public VaultListPage() public VaultListPage()
{ {
_folderService = Resolver.Resolve<IFolderService>(); _folderService = Resolver.Resolve<IFolderService>();
_siteService = Resolver.Resolve<ISiteService>(); _siteService = Resolver.Resolve<ISiteService>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
var addSiteToolBarItem = new ToolbarItem("+", null, async () => Init();
{ }
var selection = await DisplayActionSheet("Add", "Cancel", null, "Add New Folder", "Add New Site");
if(selection == "Add New Folder")
{
var addFolderPage = new VaultAddFolderPage();
await Navigation.PushAsync(addFolderPage);
}
else
{
var addSitePage = new VaultAddSitePage();
await Navigation.PushAsync(addSitePage);
} public ObservableCollection<VaultView.Folder> Folders { get; private set; } = new ObservableCollection<VaultView.Folder>();
}, ToolbarItemOrder.Default, 0);
ToolbarItems.Add(addSiteToolBarItem); private void Init()
{
ToolbarItems.Add(new AddSiteToolBarItem(this));
_listView.IsGroupingEnabled = true; var moreAction = new MenuItem { Text = "More" };
_listView.GroupDisplayBinding = new Binding("Name"); moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
_listView.ItemSelected += FolderSelected; moreAction.Clicked += MoreClickedAsync;
_listView.ItemTemplate = new DataTemplate(() =>
{ var deleteAction = new MenuItem { Text = "Delete", IsDestructive = true };
var cell = new TextCell(); deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
cell.SetBinding<VaultView.Site>(TextCell.TextProperty, s => s.Name); deleteAction.Clicked += DeleteClickedAsync;
cell.SetBinding<VaultView.Site>(TextCell.DetailProperty, s => s.Username);
return cell; var listView = new ListView { IsGroupingEnabled = true, ItemsSource = Folders };
}); listView.GroupDisplayBinding = new Binding("Name");
listView.ItemSelected += SiteSelected;
listView.ItemTemplate = new DataTemplate(() => new VaultListViewCell(moreAction, deleteAction));
Title = "My Vault"; Title = "My Vault";
Content = _listView; Content = listView;
} }
protected override void OnAppearing() protected override void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
LoadFoldersAsync().Wait();
}
var folders = _folderService.GetAllAsync().GetAwaiter().GetResult(); private async Task LoadFoldersAsync()
var sites = _siteService.GetAllAsync().GetAwaiter().GetResult(); {
Folders.Clear();
var folders = await _folderService.GetAllAsync();
var sites = await _siteService.GetAllAsync();
foreach(var folder in folders)
{
var f = new VaultView.Folder(folder, sites.Where(s => s.FolderId == folder.Id));
Folders.Add(f);
}
var folderItems = folders.Select(f => new VaultView.Folder(f, sites.Where(s => s.FolderId == f.Id))).ToList();
// add the sites with no folder // add the sites with no folder
folderItems.Add(new VaultView.Folder(sites.Where(s => s.FolderId != null))); var noneFolder = new VaultView.Folder(sites.Where(s => s.FolderId == null));
_listView.ItemsSource = folderItems; Folders.Add(noneFolder);
} }
void FolderSelected(object sender, SelectedItemChangedEventArgs e) private void SiteSelected(object sender, SelectedItemChangedEventArgs e)
{ {
} }
void SiteSelected(object sender, SelectedItemChangedEventArgs e) private async void MoreClickedAsync(object sender, EventArgs e)
{ {
var mi = sender as MenuItem;
var site = mi.CommandParameter as VaultView.Site;
var selection = await DisplayActionSheet("More Options", "Cancel", null, "View", "Edit", "Copy Password", "Copy Username", "Go To Website");
switch(selection)
{
case "View":
case "Edit":
case "Copy Password":
case "Copy Username":
case "Go To Website":
default:
break;
}
}
private async void DeleteClickedAsync(object sender, EventArgs e)
{
var mi = sender as MenuItem;
var site = mi.CommandParameter as VaultView.Site;
var deleteCall = await _siteService.DeleteAsync(site.Id);
if(deleteCall.Succeeded)
{
var folder = Folders.Single(f => f.Id == site.FolderId);
var siteIndex = folder.Select((s, i) => new { s, i }).First(s => s.s.Id == site.Id).i;
folder.RemoveAt(siteIndex);
_userDialogs.SuccessToast("Site deleted.");
}
else if(deleteCall.Errors.Count() > 0)
{
await DisplayAlert("An error has occurred", deleteCall.Errors.First().Message, "Ok");
}
}
private class AddSiteToolBarItem : ToolbarItem
{
private readonly VaultListPage _page;
public AddSiteToolBarItem(VaultListPage page)
{
_page = page;
Text = "Add";
Icon = "";
Clicked += ClickedItem;
}
private async void ClickedItem(object sender, EventArgs e)
{
var selection = await _page.DisplayActionSheet("Add", "Cancel", null, "Add New Folder", "Add New Site");
if(selection == "Add New Folder")
{
var addFolderPage = new VaultAddFolderPage();
await _page.Navigation.PushAsync(addFolderPage);
}
else if(selection == "Add New Site")
{
var addSitePage = new VaultAddSitePage();
await _page.Navigation.PushAsync(addSitePage);
}
}
}
private class VaultListViewCell : TextCell
{
public VaultListViewCell(MenuItem moreMenuItem, MenuItem deleteMenuItem)
{
this.SetBinding<VaultView.Site>(TextProperty, s => s.Name);
this.SetBinding<VaultView.Site>(DetailProperty, s => s.Username);
ContextActions.Add(moreMenuItem);
ContextActions.Add(deleteMenuItem);
}
} }
} }
} }

View File

@@ -8,7 +8,6 @@ using Bit.App.Models.Data;
using Bit.App.Models.Api; using Bit.App.Models.Api;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Net.Http; using System.Net.Http;
using System.Text;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@@ -30,26 +29,25 @@ namespace Bit.App.Services
public new Task<Folder> GetByIdAsync(string id) public new Task<Folder> GetByIdAsync(string id)
{ {
var data = Connection.Table<FolderData>().Where(f => f.UserId == _authService.UserId && f.Id == id).FirstOrDefault(); var data = Connection.Table<FolderData>().Where(f => f.UserId == _authService.UserId && f.Id == id).FirstOrDefault();
return Task.FromResult(new Folder(data)); var folder = new Folder(data);
return Task.FromResult(folder);
} }
public new Task<IEnumerable<Folder>> GetAllAsync() public new Task<IEnumerable<Folder>> GetAllAsync()
{ {
var data = Connection.Table<FolderData>().Where(f => f.UserId == _authService.UserId).Cast<FolderData>(); var data = Connection.Table<FolderData>().Where(f => f.UserId == _authService.UserId).Cast<FolderData>();
return Task.FromResult(data.Select(f => new Folder(f))); var folders = data.Select(f => new Folder(f));
return Task.FromResult(folders);
} }
public async Task<ApiResult<FolderResponse>> SaveAsync(Folder folder) public async Task<ApiResult<FolderResponse>> SaveAsync(Folder folder)
{ {
var request = new FolderRequest(folder); var request = new FolderRequest(folder);
var requestContent = JsonConvert.SerializeObject(request); var requestMessage = new TokenHttpRequestMessage(request)
var requestMessage = new HttpRequestMessage
{ {
Method = folder.Id == null ? HttpMethod.Post : HttpMethod.Put, Method = folder.Id == null ? HttpMethod.Post : HttpMethod.Put,
RequestUri = new Uri(_apiService.Client.BaseAddress, folder.Id == null ? "/folders" : string.Concat("/folders/", folder.Id)), RequestUri = new Uri(_apiService.Client.BaseAddress, folder.Id == null ? "/folders" : $"/folders/{folder.Id}"),
Content = new StringContent(requestContent, Encoding.UTF8, "application/json")
}; };
requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", _authService.Token));
var response = await _apiService.Client.SendAsync(requestMessage); var response = await _apiService.Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)

View File

@@ -40,9 +40,14 @@ namespace Bit.App.Services
return Task.FromResult(0); return Task.FromResult(0);
} }
protected virtual Task DeleteAsync(T obj) protected virtual async Task DeleteAsync(T obj)
{ {
Connection.Delete<T>(obj.Id); await DeleteAsync(obj.Id);
}
protected virtual Task DeleteAsync(TId id)
{
Connection.Delete<T>(id);
return Task.FromResult(0); return Task.FromResult(0);
} }
} }

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Models; using Bit.App.Models;
@@ -30,20 +29,18 @@ namespace Bit.App.Services
public new Task<IEnumerable<Site>> GetAllAsync() public new Task<IEnumerable<Site>> GetAllAsync()
{ {
var data = Connection.Table<SiteData>().Where(f => f.UserId == _authService.UserId).Cast<SiteData>(); var data = Connection.Table<SiteData>().Where(f => f.UserId == _authService.UserId).Cast<SiteData>();
return Task.FromResult(data.Select(s => new Site(s))); var sites = data.Select(s => new Site(s));
return Task.FromResult(sites);
} }
public async Task<ApiResult<SiteResponse>> SaveAsync(Site site) public async Task<ApiResult<SiteResponse>> SaveAsync(Site site)
{ {
var request = new SiteRequest(site); var request = new SiteRequest(site);
var requestContent = JsonConvert.SerializeObject(request); var requestMessage = new TokenHttpRequestMessage(request)
var requestMessage = new HttpRequestMessage
{ {
Method = site.Id == null ? HttpMethod.Post : HttpMethod.Put, Method = site.Id == null ? HttpMethod.Post : HttpMethod.Put,
RequestUri = new Uri(_apiService.Client.BaseAddress, site.Id == null ? "/sites" : string.Concat("/folders/", site.Id)), RequestUri = new Uri(_apiService.Client.BaseAddress, site.Id == null ? "/sites" : $"/folders/{site.Id}")
Content = new StringContent(requestContent, Encoding.UTF8, "application/json")
}; };
requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", _authService.Token));
var response = await _apiService.Client.SendAsync(requestMessage); var response = await _apiService.Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode) if(!response.IsSuccessStatusCode)
@@ -57,15 +54,33 @@ namespace Bit.App.Services
if(site.Id == null) if(site.Id == null)
{ {
await CreateAsync(data); await base.CreateAsync(data);
site.Id = responseObj.Id; site.Id = responseObj.Id;
} }
else else
{ {
await ReplaceAsync(data); await base.ReplaceAsync(data);
} }
return ApiResult<SiteResponse>.Success(responseObj, response.StatusCode); return ApiResult<SiteResponse>.Success(responseObj, response.StatusCode);
} }
public new async Task<ApiResult<object>> DeleteAsync(string id)
{
var requestMessage = new TokenHttpRequestMessage
{
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 base.DeleteAsync(id);
return ApiResult<object>.Success(null, response.StatusCode);
}
} }
} }

View File

@@ -0,0 +1,24 @@
using System.Net.Http;
using System.Text;
using Bit.App.Abstractions;
using Newtonsoft.Json;
using XLabs.Ioc;
namespace Bit.App
{
public class TokenHttpRequestMessage : HttpRequestMessage
{
public TokenHttpRequestMessage()
{
var authService = Resolver.Resolve<IAuthService>();
Headers.Add("Authorization", $"Bearer {authService.Token}");
}
public TokenHttpRequestMessage(object requestObject)
: this()
{
var stringContent = JsonConvert.SerializeObject(requestObject);
Content = new StringContent(stringContent, Encoding.UTF8, "application/json");
}
}
}