diff --git a/src/App/Abstractions/Services/ISiteService.cs b/src/App/Abstractions/Services/ISiteService.cs index 8f377ed48..96b11482e 100644 --- a/src/App/Abstractions/Services/ISiteService.cs +++ b/src/App/Abstractions/Services/ISiteService.cs @@ -9,5 +9,6 @@ namespace Bit.App.Abstractions { Task> GetAllAsync(); Task> SaveAsync(Site site); + Task> DeleteAsync(string id); } } diff --git a/src/App/App.csproj b/src/App/App.csproj index ee588f2e8..050fcad7d 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -41,6 +41,7 @@ + @@ -84,6 +85,7 @@ + diff --git a/src/App/Behaviors/ConnectivityBehavior.cs b/src/App/Behaviors/ConnectivityBehavior.cs new file mode 100644 index 000000000..c11e9480d --- /dev/null +++ b/src/App/Behaviors/ConnectivityBehavior.cs @@ -0,0 +1,43 @@ +using System; +using Plugin.Connectivity.Abstractions; +using Xamarin.Forms; +using XLabs.Ioc; + +namespace Bit.App.Behaviors +{ + public class ConnectivityBehavior : Behavior + { + private readonly IConnectivity _connectivity; + + public ConnectivityBehavior() + { + _connectivity = Resolver.Resolve(); + } + + 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); + } + } +} diff --git a/src/App/Behaviors/EmailValidationBehavior.cs b/src/App/Behaviors/EmailValidationBehavior.cs index 471ef2548..55cf64d79 100644 --- a/src/App/Behaviors/EmailValidationBehavior.cs +++ b/src/App/Behaviors/EmailValidationBehavior.cs @@ -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); } } } diff --git a/src/App/Behaviors/RequiredValidationBehavior.cs b/src/App/Behaviors/RequiredValidationBehavior.cs index d26d1e986..b45b011ef 100644 --- a/src/App/Behaviors/RequiredValidationBehavior.cs +++ b/src/App/Behaviors/RequiredValidationBehavior.cs @@ -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); } } } diff --git a/src/App/Models/CipherString.cs b/src/App/Models/CipherString.cs index 70dff2dda..8cdc1d3ed 100644 --- a/src/App/Models/CipherString.cs +++ b/src/App/Models/CipherString.cs @@ -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("|")) @@ -43,8 +41,13 @@ namespace Bit.App.Models public string Decrypt() { - var cryptoService = Resolver.Resolve(); - return cryptoService.Decrypt(this); + if(_decryptedValue == null) + { + var cryptoService = Resolver.Resolve(); + _decryptedValue = cryptoService.Decrypt(this); + } + + return _decryptedValue; } } } diff --git a/src/App/Models/Data/FolderData.cs b/src/App/Models/Data/FolderData.cs index 91e22f635..fd38e36c2 100644 --- a/src/App/Models/Data/FolderData.cs +++ b/src/App/Models/Data/FolderData.cs @@ -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; diff --git a/src/App/Models/Data/SiteData.cs b/src/App/Models/Data/SiteData.cs index 28d978943..c4f7ad4f1 100644 --- a/src/App/Models/Data/SiteData.cs +++ b/src/App/Models/Data/SiteData.cs @@ -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; } diff --git a/src/App/Models/View/VaultView.cs b/src/App/Models/View/VaultView.cs index 960e53836..99fe4fd75 100644 --- a/src/App/Models/View/VaultView.cs +++ b/src/App/Models/View/VaultView.cs @@ -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 { - public Folder(string name) { Name = name; } - - public Folder(IEnumerable sites) + public Folder(IEnumerable 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 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); } } } } diff --git a/src/App/Pages/VaultAddSitePage.cs b/src/App/Pages/VaultAddSitePage.cs index 156907659..584321ec2 100644 --- a/src/App/Pages/VaultAddSitePage.cs +++ b/src/App/Pages/VaultAddSitePage.cs @@ -20,7 +20,7 @@ namespace Bit.App.Pages var folderService = Resolver.Resolve(); var userDialogs = Resolver.Resolve(); - 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(); diff --git a/src/App/Pages/VaultListPage.cs b/src/App/Pages/VaultListPage.cs index d4773f283..129750f9c 100644 --- a/src/App/Pages/VaultListPage.cs +++ b/src/App/Pages/VaultListPage.cs @@ -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(); _siteService = Resolver.Resolve(); + _userDialogs = Resolver.Resolve(); - 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); - } - else - { - var addSitePage = new VaultAddSitePage(); - await Navigation.PushAsync(addSitePage); + Init(); + } - } - }, ToolbarItemOrder.Default, 0); + public ObservableCollection Folders { get; private set; } = new ObservableCollection(); - ToolbarItems.Add(addSiteToolBarItem); + private void Init() + { + ToolbarItems.Add(new AddSiteToolBarItem(this)); - _listView.IsGroupingEnabled = true; - _listView.GroupDisplayBinding = new Binding("Name"); - _listView.ItemSelected += FolderSelected; - _listView.ItemTemplate = new DataTemplate(() => - { - var cell = new TextCell(); - cell.SetBinding(TextCell.TextProperty, s => s.Name); - cell.SetBinding(TextCell.DetailProperty, s => s.Username); - return cell; - }); + 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)); 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(TextProperty, s => s.Name); + this.SetBinding(DetailProperty, s => s.Username); + ContextActions.Add(moreMenuItem); + ContextActions.Add(deleteMenuItem); + } } } } diff --git a/src/App/Services/FolderService.cs b/src/App/Services/FolderService.cs index ebee5c280..9cfc77ead 100644 --- a/src/App/Services/FolderService.cs +++ b/src/App/Services/FolderService.cs @@ -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 GetByIdAsync(string id) { var data = Connection.Table().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> GetAllAsync() { var data = Connection.Table().Where(f => f.UserId == _authService.UserId).Cast(); - return Task.FromResult(data.Select(f => new Folder(f))); + var folders = data.Select(f => new Folder(f)); + return Task.FromResult(folders); } public async Task> 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) diff --git a/src/App/Services/Repository.cs b/src/App/Services/Repository.cs index 197c84cf6..c549a8ed4 100644 --- a/src/App/Services/Repository.cs +++ b/src/App/Services/Repository.cs @@ -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(obj.Id); + await DeleteAsync(obj.Id); + } + + protected virtual Task DeleteAsync(TId id) + { + Connection.Delete(id); return Task.FromResult(0); } } diff --git a/src/App/Services/SiteService.cs b/src/App/Services/SiteService.cs index 3bc7f6d90..83aa7fc71 100644 --- a/src/App/Services/SiteService.cs +++ b/src/App/Services/SiteService.cs @@ -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> GetAllAsync() { var data = Connection.Table().Where(f => f.UserId == _authService.UserId).Cast(); - return Task.FromResult(data.Select(s => new Site(s))); + var sites = data.Select(s => new Site(s)); + return Task.FromResult(sites); } public async Task> 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.Success(responseObj, response.StatusCode); } + + public new async Task> 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(response); + } + + await base.DeleteAsync(id); + return ApiResult.Success(null, response.StatusCode); + } } } diff --git a/src/App/Utilities/TokenHttpRequestMessage.cs b/src/App/Utilities/TokenHttpRequestMessage.cs new file mode 100644 index 000000000..f5e9501b0 --- /dev/null +++ b/src/App/Utilities/TokenHttpRequestMessage.cs @@ -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(); + Headers.Add("Authorization", $"Bearer {authService.Token}"); + } + + public TokenHttpRequestMessage(object requestObject) + : this() + { + var stringContent = JsonConvert.SerializeObject(requestObject); + Content = new StringContent(stringContent, Encoding.UTF8, "application/json"); + } + } +}