1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-25 12:05:59 +01: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<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\ISqlService.cs" />
<Compile Include="Behaviors\EmailValidationBehavior.cs" />
<Compile Include="Behaviors\ConnectivityBehavior.cs" />
<Compile Include="Behaviors\RequiredValidationBehavior.cs" />
<Compile Include="Models\Api\ApiError.cs" />
<Compile Include="Models\Api\ApiResult.cs" />
@ -84,6 +85,7 @@
<Compile Include="Pages\VaultListPage.cs" />
<Compile Include="Services\ApiService.cs" />
<Compile Include="Utilities\Extentions.cs" />
<Compile Include="Utilities\TokenHttpRequestMessage.cs" />
</ItemGroup>
<ItemGroup>
<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); }
}
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)
@ -29,9 +30,10 @@ namespace Bit.App.Behaviors
((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); }
}
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)
@ -25,9 +26,10 @@ namespace Bit.App.Behaviors
((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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using XLabs.Ioc;
@ -10,6 +6,8 @@ namespace Bit.App.Models
{
public class CipherString
{
private string _decryptedValue;
public CipherString(string encryptedString)
{
if(string.IsNullOrWhiteSpace(encryptedString) || !encryptedString.Contains("|"))
@ -42,9 +40,14 @@ namespace Bit.App.Models
public byte[] CipherTextBytes { get { return Convert.FromBase64String(CipherText); } }
public string Decrypt()
{
if(_decryptedValue == null)
{
var cryptoService = Resolver.Resolve<ICryptoService>();
return cryptoService.Decrypt(this);
_decryptedValue = cryptoService.Decrypt(this);
}
return _decryptedValue;
}
}
}

View File

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

View File

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

View File

@ -8,40 +8,39 @@ namespace Bit.App.Models.View
{
public class Site
{
public Site(Models.Site site)
public Site(Models.Site site, string folderId)
{
Id = site.Id;
FolderId = folderId;
Name = site.Name?.Decrypt();
Username = site.Username?.Decrypt();
}
public string Id { get; set; }
public string FolderId { get; set; }
public string Name { get; set; }
public string Username { get; set; }
}
public class Folder : ObservableCollection<Site>
{
public Folder(string name) { Name = name; }
public Folder(IEnumerable<Models.Site> sites)
public Folder(IEnumerable<Models.Site> sites, string folderId = null)
{
Name = "(none)";
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)
: this(sites)
: this(sites, folder.Id)
{
Id = folder.Id;
Name = folder.Name?.Decrypt();
}
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); } }
}
}

View File

@ -20,7 +20,7 @@ namespace Bit.App.Pages
var folderService = Resolver.Resolve<IFolderService>();
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 nameEntry = new Entry();

View File

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Acr.UserDialogs;
using Bit.App.Abstractions;
using Bit.App.Models.View;
using Xamarin.Forms;
@ -13,67 +14,143 @@ namespace Bit.App.Pages
{
private readonly IFolderService _folderService;
private readonly ISiteService _siteService;
private ListView _listView = new ListView();
private readonly IUserDialogs _userDialogs;
public VaultListPage()
{
_folderService = Resolver.Resolve<IFolderService>();
_siteService = Resolver.Resolve<ISiteService>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
var addSiteToolBarItem = new ToolbarItem("+", null, async () =>
{
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);
Init();
}
else
public ObservableCollection<VaultView.Folder> Folders { get; private set; } = new ObservableCollection<VaultView.Folder>();
private void Init()
{
var addSitePage = new VaultAddSitePage();
await Navigation.PushAsync(addSitePage);
ToolbarItems.Add(new AddSiteToolBarItem(this));
}
}, ToolbarItemOrder.Default, 0);
var moreAction = new MenuItem { Text = "More" };
moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
moreAction.Clicked += MoreClickedAsync;
ToolbarItems.Add(addSiteToolBarItem);
var deleteAction = new MenuItem { Text = "Delete", IsDestructive = true };
deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));
deleteAction.Clicked += DeleteClickedAsync;
_listView.IsGroupingEnabled = true;
_listView.GroupDisplayBinding = new Binding("Name");
_listView.ItemSelected += FolderSelected;
_listView.ItemTemplate = new DataTemplate(() =>
{
var cell = new TextCell();
cell.SetBinding<VaultView.Site>(TextCell.TextProperty, s => s.Name);
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";
Content = _listView;
Content = listView;
}
protected override void OnAppearing()
{
base.OnAppearing();
LoadFoldersAsync().Wait();
}
var folders = _folderService.GetAllAsync().GetAwaiter().GetResult();
var sites = _siteService.GetAllAsync().GetAwaiter().GetResult();
private async Task LoadFoldersAsync()
{
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
folderItems.Add(new VaultView.Folder(sites.Where(s => s.FolderId != null)));
_listView.ItemsSource = folderItems;
var noneFolder = new VaultView.Folder(sites.Where(s => s.FolderId == null));
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 Newtonsoft.Json;
using System.Net.Http;
using System.Text;
namespace Bit.App.Services
{
@ -30,26 +29,25 @@ namespace Bit.App.Services
public new Task<Folder> GetByIdAsync(string id)
{
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()
{
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)
{
var request = new FolderRequest(folder);
var requestContent = JsonConvert.SerializeObject(request);
var requestMessage = new HttpRequestMessage
var requestMessage = new TokenHttpRequestMessage(request)
{
Method = folder.Id == null ? HttpMethod.Post : HttpMethod.Put,
RequestUri = new Uri(_apiService.Client.BaseAddress, folder.Id == null ? "/folders" : string.Concat("/folders/", folder.Id)),
Content = new StringContent(requestContent, Encoding.UTF8, "application/json")
RequestUri = new Uri(_apiService.Client.BaseAddress, folder.Id == null ? "/folders" : $"/folders/{folder.Id}"),
};
requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", _authService.Token));
var response = await _apiService.Client.SendAsync(requestMessage);
if(!response.IsSuccessStatusCode)

View File

@ -40,9 +40,14 @@ namespace Bit.App.Services
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);
}
}

View File

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