From 9506595fdda0ace361d34abacb5ad5dabba8c4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Tue, 7 Nov 2023 12:15:32 +0000 Subject: [PATCH] [PM-2671] Update mobile client to use regions (#2798) * [PM-2671] Update mobile client to use regions * [PM-2671] Refactor * [PM-2671] Move migration of region to migration service. * [PM-2671] Move comment * [PM-2671] Change method name * [PM-2671] Change method name on usages --------- Co-authored-by: Federico Maccaroni --- .../Accounts/EnvironmentPageViewModel.cs | 25 +++-- src/App/Pages/Accounts/HomePage.xaml.cs | 2 +- src/App/Pages/Accounts/HomePageViewModel.cs | 34 +++---- src/App/Pages/Accounts/LockPageViewModel.cs | 9 +- src/App/Pages/Accounts/LoginPageViewModel.cs | 2 +- src/App/Utilities/AppHelpers.cs | 5 +- src/Core/Abstractions/IApiService.cs | 5 +- src/Core/Abstractions/IEnvironmentService.cs | 5 +- src/Core/Abstractions/IStateService.cs | 4 + src/Core/Constants.cs | 1 + src/Core/Enums/Region.cs | 10 ++ src/Core/Models/Data/EnvironmentUrlData.cs | 90 ++++++++++++++++- src/Core/Models/Domain/Account.cs | 2 + src/Core/Models/Domain/EnvironmentUrls.cs | 10 -- src/Core/Models/View/AccountView.cs | 16 +-- src/Core/Services/ApiService.cs | 4 +- src/Core/Services/EnvironmentService.cs | 97 ++++++++----------- src/Core/Services/StateMigrationService.cs | 42 +++++++- src/Core/Services/StateService.cs | 16 +++ src/Core/Utilities/RegionExtensions.cs | 49 ++++++++++ 20 files changed, 294 insertions(+), 134 deletions(-) create mode 100644 src/Core/Enums/Region.cs delete mode 100644 src/Core/Models/Domain/EnvironmentUrls.cs create mode 100644 src/Core/Utilities/RegionExtensions.cs diff --git a/src/App/Pages/Accounts/EnvironmentPageViewModel.cs b/src/App/Pages/Accounts/EnvironmentPageViewModel.cs index abac94084..baf54eaae 100644 --- a/src/App/Pages/Accounts/EnvironmentPageViewModel.cs +++ b/src/App/Pages/Accounts/EnvironmentPageViewModel.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Bit.App.Resources; using Bit.Core.Abstractions; -using Bit.Core.Models.Data; +using Bit.Core.Enums; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; @@ -19,14 +19,25 @@ namespace Bit.App.Pages _environmentService = ServiceContainer.Resolve("environmentService"); PageTitle = AppResources.Settings; - BaseUrl = _environmentService.BaseUrl == EnvironmentUrlData.DefaultEU.Base || EnvironmentUrlData.DefaultUS.Base == _environmentService.BaseUrl ? - string.Empty : _environmentService.BaseUrl; + SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false); + Init(); + } + + public void Init() + { + if (_environmentService.SelectedRegion != Region.SelfHosted || + _environmentService.BaseUrl == Region.US.BaseUrl() || + _environmentService.BaseUrl == Region.EU.BaseUrl()) + { + return; + } + + BaseUrl = _environmentService.BaseUrl; WebVaultUrl = _environmentService.WebVaultUrl; ApiUrl = _environmentService.ApiUrl; IdentityUrl = _environmentService.IdentityUrl; IconsUrl = _environmentService.IconsUrl; NotificationsUrls = _environmentService.NotificationsUrl; - SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false); } public ICommand SubmitCommand { get; } @@ -46,8 +57,7 @@ namespace Bit.App.Pages await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok); return; } - - var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData + var urls = new Core.Models.Data.EnvironmentUrlData { Base = BaseUrl, Api = ApiUrl, @@ -55,7 +65,8 @@ namespace Bit.App.Pages WebVault = WebVaultUrl, Icons = IconsUrl, Notifications = NotificationsUrls - }); + }; + var resUrls = await _environmentService.SetRegionAsync(urls.Region, urls); // re-set urls since service can change them, ex: prefixing https:// BaseUrl = resUrls.Base; diff --git a/src/App/Pages/Accounts/HomePage.xaml.cs b/src/App/Pages/Accounts/HomePage.xaml.cs index ae06b5a9d..ee31b0772 100644 --- a/src/App/Pages/Accounts/HomePage.xaml.cs +++ b/src/App/Pages/Accounts/HomePage.xaml.cs @@ -74,7 +74,7 @@ namespace Bit.App.Pages }); try { - await _vm.UpdateEnvironment(); + await _vm.UpdateEnvironmentAsync(); } catch (Exception ex) { diff --git a/src/App/Pages/Accounts/HomePageViewModel.cs b/src/App/Pages/Accounts/HomePageViewModel.cs index d42c5e47a..629c860b1 100644 --- a/src/App/Pages/Accounts/HomePageViewModel.cs +++ b/src/App/Pages/Accounts/HomePageViewModel.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Controls; using Bit.App.Resources; +using Bit.App.Styles; using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; @@ -10,13 +11,12 @@ using Bit.Core.Models.Data; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; +using BwRegion = Bit.Core.Enums.Region; namespace Bit.App.Pages { public class HomeViewModel : BaseViewModel { - private const string LOGGING_IN_ON_US = "bitwarden.com"; - private const string LOGGING_IN_ON_EU = "bitwarden.eu"; private readonly IStateService _stateService; private readonly IMessagingService _messagingService; @@ -165,8 +165,8 @@ namespace Bit.App.Pages { _displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag); var options = _displayEuEnvironment - ? new string[] { LOGGING_IN_ON_US, LOGGING_IN_ON_EU, AppResources.SelfHosted } - : new string[] { LOGGING_IN_ON_US, AppResources.SelfHosted }; + ? new string[] { BwRegion.US.Domain(), BwRegion.EU.Domain(), AppResources.SelfHosted } + : new string[] { BwRegion.US.Domain(), AppResources.SelfHosted }; await Device.InvokeOnMainThreadAsync(async () => { @@ -183,35 +183,23 @@ namespace Bit.App.Pages return; } - await _environmentService.SetUrlsAsync(result == LOGGING_IN_ON_EU ? EnvironmentUrlData.DefaultEU : EnvironmentUrlData.DefaultUS); + await _environmentService.SetRegionAsync(result == BwRegion.EU.Domain() ? BwRegion.EU : BwRegion.US); await _configService.GetAsync(true); SelectedEnvironmentName = result; }); } - public async Task UpdateEnvironment() + public async Task UpdateEnvironmentAsync() { - var environmentsSaved = await _stateService.GetPreAuthEnvironmentUrlsAsync(); - if (environmentsSaved == null || environmentsSaved.IsEmpty) + var region = _environmentService.SelectedRegion; + if (region == BwRegion.SelfHosted) { - await _environmentService.SetUrlsAsync(EnvironmentUrlData.DefaultUS); - environmentsSaved = EnvironmentUrlData.DefaultUS; - SelectedEnvironmentName = LOGGING_IN_ON_US; - return; - } - - if (environmentsSaved.Base == EnvironmentUrlData.DefaultUS.Base) - { - SelectedEnvironmentName = LOGGING_IN_ON_US; - } - else if (environmentsSaved.Base == EnvironmentUrlData.DefaultEU.Base) - { - SelectedEnvironmentName = LOGGING_IN_ON_EU; + SelectedEnvironmentName = AppResources.SelfHosted; + await _configService.GetAsync(true); } else { - await _configService.GetAsync(true); - SelectedEnvironmentName = AppResources.SelfHosted; + SelectedEnvironmentName = region.Domain(); } } } diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index c5aaf1ab7..e79f6eef2 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -206,13 +206,8 @@ namespace Bit.App.Pages _logger.Exception(new NullReferenceException("Email not found in storage")); return; } - var webVault = _environmentService.GetWebVaultUrl(true); - if (string.IsNullOrWhiteSpace(webVault)) - { - webVault = "https://bitwarden.com"; - } - var webVaultHostname = CoreHelpers.GetHostname(webVault); - LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname); + + LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, _environmentService.GetCurrentDomain()); if (PinEnabled) { PageTitle = AppResources.VerifyPIN; diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs index 2d8d3aff5..b9618c5de 100644 --- a/src/App/Pages/Accounts/LoginPageViewModel.cs +++ b/src/App/Pages/Accounts/LoginPageViewModel.cs @@ -162,7 +162,7 @@ namespace Bit.App.Pages Email = await _stateService.GetRememberedEmailAsync(); } CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email; - EnvironmentDomainName = CoreHelpers.GetDomain((await _stateService.GetPreAuthEnvironmentUrlsAsync())?.Base); + EnvironmentDomainName = _environmentService.GetCurrentDomain(); IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync()); } catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized) diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index 3e0a70bda..24b18b8a5 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -405,14 +405,15 @@ namespace Bit.App.Utilities var settingValue = string.IsNullOrWhiteSpace(setting.Value) ? null : setting.Value; if (environmentService.BaseUrl != settingValue) { - await environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData + var urls = new EnvironmentUrlData { Base = settingValue, Api = environmentService.ApiUrl, Identity = environmentService.IdentityUrl, WebVault = environmentService.WebVaultUrl, Icons = environmentService.IconsUrl - }); + }; + await environmentService.SetRegionAsync(urls.Region, urls); } return; default: diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 9fc041b9c..40d892e6f 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; -using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Bit.Core.Enums; -using Bit.Core.Models.Domain; +using Bit.Core.Models.Data; using Bit.Core.Models.Request; using Bit.Core.Models.Response; @@ -51,7 +50,7 @@ namespace Bit.Core.Abstractions Task SendAsync(HttpMethod method, string path, TRequest body, bool authed, bool hasResponse, Action alterRequest, bool logoutOnUnauthorized = true, bool sendToIdentity = false); Task SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default); - void SetUrls(EnvironmentUrls urls); + void SetUrls(EnvironmentUrlData urls); [Obsolete("Mar 25 2021: This method has been deprecated in favor of direct uploads. This method still exists for backward compatibility with old server versions.")] Task PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data); Task PostCipherAttachmentAsync(string id, AttachmentRequest request); diff --git a/src/Core/Abstractions/IEnvironmentService.cs b/src/Core/Abstractions/IEnvironmentService.cs index 7469452b9..df09b489d 100644 --- a/src/Core/Abstractions/IEnvironmentService.cs +++ b/src/Core/Abstractions/IEnvironmentService.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Data; namespace Bit.Core.Abstractions @@ -12,10 +13,12 @@ namespace Bit.Core.Abstractions string NotificationsUrl { get; set; } string WebVaultUrl { get; set; } string EventsUrl { get; set; } + Region SelectedRegion { get; set; } string GetWebVaultUrl(bool returnNullIfDefault = false); string GetWebSendUrl(); - Task SetUrlsAsync(EnvironmentUrlData urls); + string GetCurrentDomain(); Task SetUrlsFromStorageAsync(); + Task SetRegionAsync(Region region, EnvironmentUrlData selfHostedUrls = null); } } diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 07ce65461..62b460d84 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -185,6 +185,10 @@ namespace Bit.Core.Abstractions void SetConfigs(ConfigResponse value); Task GetShouldTrustDeviceAsync(); Task SetShouldTrustDeviceAsync(bool value); + Task GetActiveUserRegionAsync(); + Task GetPreAuthRegionAsync(); + Task SetPreAuthRegionAsync(Region value); + [Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")] Task GetPinProtectedAsync(string userId = null); [Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")] diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 04641a6b0..f51d21f23 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -46,6 +46,7 @@ namespace Bit.Core public const string PreLoginEmailKey = "preLoginEmailKey"; public const string ConfigsKey = "configsKey"; public const string DisplayEuEnvironmentFlag = "display-eu-environment"; + public const string RegionEnvironment = "regionEnvironment"; /// /// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in diff --git a/src/Core/Enums/Region.cs b/src/Core/Enums/Region.cs new file mode 100644 index 000000000..543ef2f47 --- /dev/null +++ b/src/Core/Enums/Region.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Enums +{ + public enum Region + { + US, + EU, + SelfHosted + } +} + diff --git a/src/Core/Models/Data/EnvironmentUrlData.cs b/src/Core/Models/Data/EnvironmentUrlData.cs index ae57f804b..74928942d 100644 --- a/src/Core/Models/Data/EnvironmentUrlData.cs +++ b/src/Core/Models/Data/EnvironmentUrlData.cs @@ -1,9 +1,34 @@ -namespace Bit.Core.Models.Data +using System.Text.RegularExpressions; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Data { public class EnvironmentUrlData { - public static EnvironmentUrlData DefaultUS = new EnvironmentUrlData { Base = "https://vault.bitwarden.com" }; - public static EnvironmentUrlData DefaultEU = new EnvironmentUrlData { Base = "https://vault.bitwarden.eu" }; + public static EnvironmentUrlData DefaultUS = new EnvironmentUrlData + { + Base = "https://vault.bitwarden.com", + Api = "https://api.bitwarden.com", + Identity = "https://identity.bitwarden.com", + Icons = "https://icons.bitwarden.net", + WebVault = "https://vault.bitwarden.com", + Notifications = "https://notifications.bitwarden.com", + Events = "https://events.bitwarden.com", + Domain = "bitwarden.com" + }; + + public static EnvironmentUrlData DefaultEU = new EnvironmentUrlData + { + Base = "https://vault.bitwarden.eu", + Api = "https://api.bitwarden.eu", + Identity = "https://identity.bitwarden.eu", + Icons = "https://icons.bitwarden.eu", + WebVault = "https://vault.bitwarden.eu", + Notifications = "https://notifications.bitwarden.eu", + Events = "https://events.bitwarden.eu", + Domain = "bitwarden.eu" + }; public string Base { get; set; } public string Api { get; set; } @@ -12,6 +37,7 @@ public string Notifications { get; set; } public string WebVault { get; set; } public string Events { get; set; } + public string Domain { get; set; } public bool IsEmpty => string.IsNullOrEmpty(Base) && string.IsNullOrEmpty(Api) @@ -20,5 +46,63 @@ && string.IsNullOrEmpty(Notifications) && string.IsNullOrEmpty(WebVault) && string.IsNullOrEmpty(Events); + + public Region Region + { + get + { + if (Base == Region.US.BaseUrl()) + { + return Region.US; + } + if (Base == Region.EU.BaseUrl()) + { + return Region.EU; + } + return Region.SelfHosted; + } + } + + public EnvironmentUrlData FormatUrls() + { + return new EnvironmentUrlData + { + Base = FormatUrl(Base), + Api = FormatUrl(Api), + Identity = FormatUrl(Identity), + Icons = FormatUrl(Icons), + Notifications = FormatUrl(Notifications), + WebVault = FormatUrl(WebVault), + Events = FormatUrl(Events) + }; + } + + private string FormatUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return null; + } + url = Regex.Replace(url, "\\/+$", string.Empty); + if (!url.StartsWith("http://") && !url.StartsWith("https://")) + { + url = string.Concat("https://", url); + } + return url.Trim(); + } + + public string GetDomainOrHostname() + { + var url = WebVault ?? Base ?? Api ?? Identity; + if (string.IsNullOrWhiteSpace(url)) + { + return string.Empty; + } + if (url.Contains(Region.US.Domain()) || url.Contains(Region.EU.Domain())) + { + return CoreHelpers.GetDomain(url); + } + return CoreHelpers.GetHostname(url); + } } } diff --git a/src/Core/Models/Domain/Account.cs b/src/Core/Models/Domain/Account.cs index 3377d60a7..2d0f49bfa 100644 --- a/src/Core/Models/Domain/Account.cs +++ b/src/Core/Models/Domain/Account.cs @@ -102,12 +102,14 @@ namespace Bit.Core.Models.Domain return; } + Region = copy.Region; EnvironmentUrls = copy.EnvironmentUrls; VaultTimeout = copy.VaultTimeout; VaultTimeoutAction = copy.VaultTimeoutAction; ScreenCaptureAllowed = copy.ScreenCaptureAllowed; } + public Region? Region; public EnvironmentUrlData EnvironmentUrls; [Obsolete("Feb 10 2023: VaultTimeout has been deprecated in favor of stored prefs to retain value after logout. It remains here to allow for migration during app upgrade.")] public int? VaultTimeout; diff --git a/src/Core/Models/Domain/EnvironmentUrls.cs b/src/Core/Models/Domain/EnvironmentUrls.cs deleted file mode 100644 index 52cfff8d3..000000000 --- a/src/Core/Models/Domain/EnvironmentUrls.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bit.Core.Models.Domain -{ - public class EnvironmentUrls - { - public string Base { get; set; } - public string Api { get; set; } - public string Identity { get; set; } - public string Events { get; set; } - } -} diff --git a/src/Core/Models/View/AccountView.cs b/src/Core/Models/View/AccountView.cs index 958f1435d..d522a5b49 100644 --- a/src/Core/Models/View/AccountView.cs +++ b/src/Core/Models/View/AccountView.cs @@ -22,21 +22,7 @@ namespace Bit.Core.Models.View Email = a.Profile?.Email; Name = a.Profile?.Name; AvatarColor = a.Profile?.AvatarColor; - Hostname = ParseEndpoint(a.Settings?.EnvironmentUrls); - } - - private string ParseEndpoint(EnvironmentUrlData urls) - { - var url = urls?.WebVault ?? urls?.Base; - if (!string.IsNullOrWhiteSpace(url)) - { - if (url.Contains("bitwarden.com") || url.Contains("bitwarden.eu")) - { - return CoreHelpers.GetDomain(url); - } - return CoreHelpers.GetHostname(url); - } - return string.Empty; + Hostname = a.Settings?.EnvironmentUrls?.GetDomainOrHostname(); } public bool IsAccount { get; set; } diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index ff2667cfb..ec00dc710 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Domain; +using Bit.Core.Models.Data; using Bit.Core.Models.Request; using Bit.Core.Models.Response; using Bit.Core.Utilities; @@ -54,7 +54,7 @@ namespace Bit.Core.Services public string IdentityBaseUrl { get; set; } public string EventsBaseUrl { get; set; } - public void SetUrls(EnvironmentUrls urls) + public void SetUrls(EnvironmentUrlData urls) { UrlsSet = true; if (!string.IsNullOrWhiteSpace(urls.Base)) diff --git a/src/Core/Services/EnvironmentService.cs b/src/Core/Services/EnvironmentService.cs index a7a9dd197..98d53c46b 100644 --- a/src/Core/Services/EnvironmentService.cs +++ b/src/Core/Services/EnvironmentService.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; using Bit.Core.Utilities; @@ -33,6 +34,7 @@ namespace Bit.Core.Services public string IconsUrl { get; set; } public string NotificationsUrl { get; set; } public string EventsUrl { get; set; } + public Region SelectedRegion { get; set; } public string GetWebVaultUrl(bool returnNullIfDefault = false) { @@ -54,38 +56,33 @@ namespace Bit.Core.Services return GetWebVaultUrl(true) is string webVaultUrl ? $"{webVaultUrl}/#/send/" : DEFAULT_WEB_SEND_URL; } + public string GetCurrentDomain() + { + return new EnvironmentUrlData + { + WebVault = WebVaultUrl, + Base = BaseUrl, + Api = ApiUrl, + Identity = IdentityUrl + }.GetDomainOrHostname(); + } + public async Task SetUrlsFromStorageAsync() { try { + var region = await _stateService.GetActiveUserRegionAsync(); var urls = await _stateService.GetEnvironmentUrlsAsync(); - if (urls == null) - { - urls = await _stateService.GetPreAuthEnvironmentUrlsAsync(); - } - if (urls == null) - { - urls = new EnvironmentUrlData(); - } - var envUrls = new EnvironmentUrls(); - if (!string.IsNullOrWhiteSpace(urls.Base)) - { - BaseUrl = envUrls.Base = urls.Base; - _apiService.SetUrls(envUrls); + urls ??= await _stateService.GetPreAuthEnvironmentUrlsAsync(); + if (urls == null || urls.IsEmpty) + { + await SetRegionAsync(Region.US); _conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited); return; } - BaseUrl = urls.Base; - WebVaultUrl = urls.WebVault; - ApiUrl = envUrls.Api = urls.Api; - IdentityUrl = envUrls.Identity = urls.Identity; - IconsUrl = urls.Icons; - NotificationsUrl = urls.Notifications; - EventsUrl = envUrls.Events = urls.Events; - _apiService.SetUrls(envUrls); - + await SetRegionAsync(region.Value, urls); _conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited); } catch (System.Exception ex) @@ -96,15 +93,26 @@ namespace Bit.Core.Services } - public async Task SetUrlsAsync(EnvironmentUrlData urls) + public async Task SetRegionAsync(Region region, EnvironmentUrlData selfHostedUrls = null) { - urls.Base = FormatUrl(urls.Base); - urls.WebVault = FormatUrl(urls.WebVault); - urls.Api = FormatUrl(urls.Api); - urls.Identity = FormatUrl(urls.Identity); - urls.Icons = FormatUrl(urls.Icons); - urls.Notifications = FormatUrl(urls.Notifications); - urls.Events = FormatUrl(urls.Events); + EnvironmentUrlData urls; + + if (region == Region.SelfHosted) + { + // If user saves a self-hosted region with empty fields, default to US + if (selfHostedUrls.IsEmpty) + { + return await SetRegionAsync(Region.US); + } + urls = selfHostedUrls.FormatUrls(); + } + else + { + urls = region.GetUrls(); + } + + SelectedRegion = region; + await _stateService.SetPreAuthRegionAsync(region); await _stateService.SetPreAuthEnvironmentUrlsAsync(urls); BaseUrl = urls.Base; WebVaultUrl = urls.WebVault; @@ -113,35 +121,8 @@ namespace Bit.Core.Services IconsUrl = urls.Icons; NotificationsUrl = urls.Notifications; EventsUrl = urls.Events; - - var envUrls = new EnvironmentUrls(); - if (!string.IsNullOrWhiteSpace(BaseUrl)) - { - envUrls.Base = BaseUrl; - } - else - { - envUrls.Api = ApiUrl; - envUrls.Identity = IdentityUrl; - envUrls.Events = EventsUrl; - } - - _apiService.SetUrls(envUrls); + _apiService.SetUrls(urls); return urls; } - - private string FormatUrl(string url) - { - if (string.IsNullOrWhiteSpace(url)) - { - return null; - } - url = Regex.Replace(url, "\\/+$", string.Empty); - if (!url.StartsWith("http://") && !url.StartsWith("https://")) - { - url = string.Concat("https://", url); - } - return url.Trim(); - } } } diff --git a/src/Core/Services/StateMigrationService.cs b/src/Core/Services/StateMigrationService.cs index 068299455..bb015f3c2 100644 --- a/src/Core/Services/StateMigrationService.cs +++ b/src/Core/Services/StateMigrationService.cs @@ -8,12 +8,13 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; using Bit.Core.Utilities; +using Newtonsoft.Json; namespace Bit.Core.Services { public class StateMigrationService : IStateMigrationService { - private const int StateVersion = 6; + private const int StateVersion = 7; private readonly DeviceType _deviceType; private readonly IStorageService _preferencesStorageService; @@ -86,6 +87,9 @@ namespace Bit.Core.Services goto case 5; case 5: await MigrateFrom5To6Async(); + goto case 6; + case 6: + await MigrateFrom6To7Async(); break; } } @@ -837,6 +841,42 @@ namespace Bit.Core.Services #endregion + #region v6 to v7 Migration + + private class V7Keys + { + // global keys + internal const string StateKey = "state"; + internal const string RegionEnvironmentKey = "regionEnvironment"; + internal const string PreAuthEnvironmentUrlsKey = "preAuthEnvironmentUrls"; + } + + private async Task MigrateFrom6To7Async() + { + // account data + var state = await GetValueAsync(Storage.Prefs, V7Keys.StateKey); + + // Migrate environment data to use Regions + foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null && a.Value?.Settings != null)) + { + var urls = account.Value.Settings.EnvironmentUrls ?? Region.US.GetUrls(); + account.Value.Settings.Region = urls.Region; + account.Value.Settings.EnvironmentUrls = urls.Region.GetUrls() ?? urls; + } + + await SetValueAsync(Storage.Prefs, Constants.StateKey, state); + + // Update pre auth urls and region + var preAuthUrls = await GetValueAsync(Storage.Prefs, V7Keys.PreAuthEnvironmentUrlsKey) ?? Region.US.GetUrls(); + await SetValueAsync(Storage.Prefs, V7Keys.RegionEnvironmentKey, preAuthUrls.Region); + await SetValueAsync(Storage.Prefs, V7Keys.PreAuthEnvironmentUrlsKey, preAuthUrls.Region.GetUrls() ?? preAuthUrls); + + + // Update stored version + await SetLastStateVersionAsync(7); + } + #endregion + // Helpers private async Task GetLastStateVersionAsync() diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index 6f2289a43..620b73fb3 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -1363,6 +1363,21 @@ namespace Bit.Core.Services _storageMediatorService.Save(Constants.ConfigsKey, value); } + public async Task GetActiveUserRegionAsync() + { + return await GetActiveUserCustomDataAsync(a => a?.Settings?.Region); + } + + public async Task GetPreAuthRegionAsync() + { + return await _storageMediatorService.GetAsync(Constants.RegionEnvironment); + } + + public async Task SetPreAuthRegionAsync(Region value) + { + await _storageMediatorService.SaveAsync(Constants.RegionEnvironment, value); + } + // Helpers [Obsolete("Use IStorageMediatorService instead")] @@ -1552,6 +1567,7 @@ namespace Bit.Core.Services await CheckStateAsync(); account.Settings.EnvironmentUrls = await GetPreAuthEnvironmentUrlsAsync(); + account.Settings.Region = await GetPreAuthRegionAsync(); // Storage var state = await GetStateFromStorageAsync() ?? new State(); diff --git a/src/Core/Utilities/RegionExtensions.cs b/src/Core/Utilities/RegionExtensions.cs new file mode 100644 index 000000000..60959dfa5 --- /dev/null +++ b/src/Core/Utilities/RegionExtensions.cs @@ -0,0 +1,49 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Utilities +{ + public static class RegionExtensions + { + public static EnvironmentUrlData GetUrls(this Region region) + { + switch (region) + { + case Region.US: + return EnvironmentUrlData.DefaultUS; + case Region.EU: + return EnvironmentUrlData.DefaultEU; + default: + return null; + } + } + + public static string BaseUrl(this Region region) + { + switch (region) + { + case Region.US: + return EnvironmentUrlData.DefaultUS.Base; + case Region.EU: + return EnvironmentUrlData.DefaultEU.Base; + default: + return null; + } + } + + public static string Domain(this Region region) + { + switch (region) + { + case Region.US: + return EnvironmentUrlData.DefaultUS.Domain; + case Region.EU: + return EnvironmentUrlData.DefaultEU.Domain; + default: + return null; + } + } + + } +} +