[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 <fedemkr@gmail.com>
This commit is contained in:
André Bispo 2023-11-07 12:15:32 +00:00 committed by GitHub
parent 7a65bf7fd7
commit 9506595fdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 294 additions and 134 deletions

View file

@ -3,7 +3,7 @@ using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.Data; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
@ -19,14 +19,25 @@ namespace Bit.App.Pages
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService"); _environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
PageTitle = AppResources.Settings; PageTitle = AppResources.Settings;
BaseUrl = _environmentService.BaseUrl == EnvironmentUrlData.DefaultEU.Base || EnvironmentUrlData.DefaultUS.Base == _environmentService.BaseUrl ? SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
string.Empty : _environmentService.BaseUrl; 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; WebVaultUrl = _environmentService.WebVaultUrl;
ApiUrl = _environmentService.ApiUrl; ApiUrl = _environmentService.ApiUrl;
IdentityUrl = _environmentService.IdentityUrl; IdentityUrl = _environmentService.IdentityUrl;
IconsUrl = _environmentService.IconsUrl; IconsUrl = _environmentService.IconsUrl;
NotificationsUrls = _environmentService.NotificationsUrl; NotificationsUrls = _environmentService.NotificationsUrl;
SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false);
} }
public ICommand SubmitCommand { get; } public ICommand SubmitCommand { get; }
@ -46,8 +57,7 @@ namespace Bit.App.Pages
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok); await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok);
return; return;
} }
var urls = new Core.Models.Data.EnvironmentUrlData
var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
{ {
Base = BaseUrl, Base = BaseUrl,
Api = ApiUrl, Api = ApiUrl,
@ -55,7 +65,8 @@ namespace Bit.App.Pages
WebVault = WebVaultUrl, WebVault = WebVaultUrl,
Icons = IconsUrl, Icons = IconsUrl,
Notifications = NotificationsUrls Notifications = NotificationsUrls
}); };
var resUrls = await _environmentService.SetRegionAsync(urls.Region, urls);
// re-set urls since service can change them, ex: prefixing https:// // re-set urls since service can change them, ex: prefixing https://
BaseUrl = resUrls.Base; BaseUrl = resUrls.Base;

View file

@ -74,7 +74,7 @@ namespace Bit.App.Pages
}); });
try try
{ {
await _vm.UpdateEnvironment(); await _vm.UpdateEnvironmentAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Styles;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.Core; using Bit.Core;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
@ -10,13 +11,12 @@ using Bit.Core.Models.Data;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms; using Xamarin.Forms;
using BwRegion = Bit.Core.Enums.Region;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public class HomeViewModel : BaseViewModel 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 IStateService _stateService;
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
@ -165,8 +165,8 @@ namespace Bit.App.Pages
{ {
_displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag); _displayEuEnvironment = await _configService.GetFeatureFlagBoolAsync(Constants.DisplayEuEnvironmentFlag);
var options = _displayEuEnvironment var options = _displayEuEnvironment
? new string[] { LOGGING_IN_ON_US, LOGGING_IN_ON_EU, AppResources.SelfHosted } ? new string[] { BwRegion.US.Domain(), BwRegion.EU.Domain(), AppResources.SelfHosted }
: new string[] { LOGGING_IN_ON_US, AppResources.SelfHosted }; : new string[] { BwRegion.US.Domain(), AppResources.SelfHosted };
await Device.InvokeOnMainThreadAsync(async () => await Device.InvokeOnMainThreadAsync(async () =>
{ {
@ -183,35 +183,23 @@ namespace Bit.App.Pages
return; 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); await _configService.GetAsync(true);
SelectedEnvironmentName = result; SelectedEnvironmentName = result;
}); });
} }
public async Task UpdateEnvironment() public async Task UpdateEnvironmentAsync()
{ {
var environmentsSaved = await _stateService.GetPreAuthEnvironmentUrlsAsync(); var region = _environmentService.SelectedRegion;
if (environmentsSaved == null || environmentsSaved.IsEmpty) if (region == BwRegion.SelfHosted)
{ {
await _environmentService.SetUrlsAsync(EnvironmentUrlData.DefaultUS); SelectedEnvironmentName = AppResources.SelfHosted;
environmentsSaved = EnvironmentUrlData.DefaultUS; await _configService.GetAsync(true);
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;
} }
else else
{ {
await _configService.GetAsync(true); SelectedEnvironmentName = region.Domain();
SelectedEnvironmentName = AppResources.SelfHosted;
} }
} }
} }

View file

@ -206,13 +206,8 @@ namespace Bit.App.Pages
_logger.Exception(new NullReferenceException("Email not found in storage")); _logger.Exception(new NullReferenceException("Email not found in storage"));
return; return;
} }
var webVault = _environmentService.GetWebVaultUrl(true);
if (string.IsNullOrWhiteSpace(webVault)) LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, _environmentService.GetCurrentDomain());
{
webVault = "https://bitwarden.com";
}
var webVaultHostname = CoreHelpers.GetHostname(webVault);
LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, webVaultHostname);
if (PinEnabled) if (PinEnabled)
{ {
PageTitle = AppResources.VerifyPIN; PageTitle = AppResources.VerifyPIN;

View file

@ -162,7 +162,7 @@ namespace Bit.App.Pages
Email = await _stateService.GetRememberedEmailAsync(); Email = await _stateService.GetRememberedEmailAsync();
} }
CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email; CanRemoveAccount = await _stateService.GetActiveUserEmailAsync() != Email;
EnvironmentDomainName = CoreHelpers.GetDomain((await _stateService.GetPreAuthEnvironmentUrlsAsync())?.Base); EnvironmentDomainName = _environmentService.GetCurrentDomain();
IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync()); IsKnownDevice = await _apiService.GetKnownDeviceAsync(Email, await _appIdService.GetAppIdAsync());
} }
catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized) catch (ApiException apiEx) when (apiEx.Error.StatusCode == System.Net.HttpStatusCode.Unauthorized)

View file

@ -405,14 +405,15 @@ namespace Bit.App.Utilities
var settingValue = string.IsNullOrWhiteSpace(setting.Value) ? null : setting.Value; var settingValue = string.IsNullOrWhiteSpace(setting.Value) ? null : setting.Value;
if (environmentService.BaseUrl != settingValue) if (environmentService.BaseUrl != settingValue)
{ {
await environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData var urls = new EnvironmentUrlData
{ {
Base = settingValue, Base = settingValue,
Api = environmentService.ApiUrl, Api = environmentService.ApiUrl,
Identity = environmentService.IdentityUrl, Identity = environmentService.IdentityUrl,
WebVault = environmentService.WebVaultUrl, WebVault = environmentService.WebVaultUrl,
Icons = environmentService.IconsUrl Icons = environmentService.IconsUrl
}); };
await environmentService.SetRegionAsync(urls.Region, urls);
} }
return; return;
default: default:

View file

@ -1,11 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Domain; using Bit.Core.Models.Data;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
@ -51,7 +50,7 @@ namespace Bit.Core.Abstractions
Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path, Task<TResponse> SendAsync<TRequest, TResponse>(HttpMethod method, string path,
TRequest body, bool authed, bool hasResponse, Action<HttpRequestMessage> alterRequest, bool logoutOnUnauthorized = true, bool sendToIdentity = false); TRequest body, bool authed, bool hasResponse, Action<HttpRequestMessage> alterRequest, bool logoutOnUnauthorized = true, bool sendToIdentity = false);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default); Task<HttpResponseMessage> 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.")] [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<CipherResponse> PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data); Task<CipherResponse> PostCipherAttachmentLegacyAsync(string id, MultipartFormDataContent data);
Task<AttachmentUploadDataResponse> PostCipherAttachmentAsync(string id, AttachmentRequest request); Task<AttachmentUploadDataResponse> PostCipherAttachmentAsync(string id, AttachmentRequest request);

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
@ -12,10 +13,12 @@ namespace Bit.Core.Abstractions
string NotificationsUrl { get; set; } string NotificationsUrl { get; set; }
string WebVaultUrl { get; set; } string WebVaultUrl { get; set; }
string EventsUrl { get; set; } string EventsUrl { get; set; }
Region SelectedRegion { get; set; }
string GetWebVaultUrl(bool returnNullIfDefault = false); string GetWebVaultUrl(bool returnNullIfDefault = false);
string GetWebSendUrl(); string GetWebSendUrl();
Task<EnvironmentUrlData> SetUrlsAsync(EnvironmentUrlData urls); string GetCurrentDomain();
Task SetUrlsFromStorageAsync(); Task SetUrlsFromStorageAsync();
Task<EnvironmentUrlData> SetRegionAsync(Region region, EnvironmentUrlData selfHostedUrls = null);
} }
} }

View file

@ -185,6 +185,10 @@ namespace Bit.Core.Abstractions
void SetConfigs(ConfigResponse value); void SetConfigs(ConfigResponse value);
Task<bool> GetShouldTrustDeviceAsync(); Task<bool> GetShouldTrustDeviceAsync();
Task SetShouldTrustDeviceAsync(bool value); Task SetShouldTrustDeviceAsync(bool value);
Task<Region?> GetActiveUserRegionAsync();
Task<Region?> GetPreAuthRegionAsync();
Task SetPreAuthRegionAsync(Region value);
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")] [Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
Task<string> GetPinProtectedAsync(string userId = null); Task<string> GetPinProtectedAsync(string userId = null);
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")] [Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]

View file

@ -46,6 +46,7 @@ namespace Bit.Core
public const string PreLoginEmailKey = "preLoginEmailKey"; public const string PreLoginEmailKey = "preLoginEmailKey";
public const string ConfigsKey = "configsKey"; public const string ConfigsKey = "configsKey";
public const string DisplayEuEnvironmentFlag = "display-eu-environment"; public const string DisplayEuEnvironmentFlag = "display-eu-environment";
public const string RegionEnvironment = "regionEnvironment";
/// <summary> /// <summary>
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in /// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in

10
src/Core/Enums/Region.cs Normal file
View file

@ -0,0 +1,10 @@
namespace Bit.Core.Enums
{
public enum Region
{
US,
EU,
SelfHosted
}
}

View file

@ -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 class EnvironmentUrlData
{ {
public static EnvironmentUrlData DefaultUS = new EnvironmentUrlData { Base = "https://vault.bitwarden.com" }; public static EnvironmentUrlData DefaultUS = new EnvironmentUrlData
public static EnvironmentUrlData DefaultEU = new EnvironmentUrlData { Base = "https://vault.bitwarden.eu" }; {
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 Base { get; set; }
public string Api { get; set; } public string Api { get; set; }
@ -12,6 +37,7 @@
public string Notifications { get; set; } public string Notifications { get; set; }
public string WebVault { get; set; } public string WebVault { get; set; }
public string Events { get; set; } public string Events { get; set; }
public string Domain { get; set; }
public bool IsEmpty => string.IsNullOrEmpty(Base) public bool IsEmpty => string.IsNullOrEmpty(Base)
&& string.IsNullOrEmpty(Api) && string.IsNullOrEmpty(Api)
@ -20,5 +46,63 @@
&& string.IsNullOrEmpty(Notifications) && string.IsNullOrEmpty(Notifications)
&& string.IsNullOrEmpty(WebVault) && string.IsNullOrEmpty(WebVault)
&& string.IsNullOrEmpty(Events); && 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);
}
} }
} }

View file

@ -102,12 +102,14 @@ namespace Bit.Core.Models.Domain
return; return;
} }
Region = copy.Region;
EnvironmentUrls = copy.EnvironmentUrls; EnvironmentUrls = copy.EnvironmentUrls;
VaultTimeout = copy.VaultTimeout; VaultTimeout = copy.VaultTimeout;
VaultTimeoutAction = copy.VaultTimeoutAction; VaultTimeoutAction = copy.VaultTimeoutAction;
ScreenCaptureAllowed = copy.ScreenCaptureAllowed; ScreenCaptureAllowed = copy.ScreenCaptureAllowed;
} }
public Region? Region;
public EnvironmentUrlData EnvironmentUrls; 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.")] [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; public int? VaultTimeout;

View file

@ -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; }
}
}

View file

@ -22,21 +22,7 @@ namespace Bit.Core.Models.View
Email = a.Profile?.Email; Email = a.Profile?.Email;
Name = a.Profile?.Name; Name = a.Profile?.Name;
AvatarColor = a.Profile?.AvatarColor; AvatarColor = a.Profile?.AvatarColor;
Hostname = ParseEndpoint(a.Settings?.EnvironmentUrls); Hostname = a.Settings?.EnvironmentUrls?.GetDomainOrHostname();
}
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;
} }
public bool IsAccount { get; set; } public bool IsAccount { get; set; }

View file

@ -9,7 +9,7 @@ using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Domain; using Bit.Core.Models.Data;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -54,7 +54,7 @@ namespace Bit.Core.Services
public string IdentityBaseUrl { get; set; } public string IdentityBaseUrl { get; set; }
public string EventsBaseUrl { get; set; } public string EventsBaseUrl { get; set; }
public void SetUrls(EnvironmentUrls urls) public void SetUrls(EnvironmentUrlData urls)
{ {
UrlsSet = true; UrlsSet = true;
if (!string.IsNullOrWhiteSpace(urls.Base)) if (!string.IsNullOrWhiteSpace(urls.Base))

View file

@ -1,6 +1,7 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -33,6 +34,7 @@ namespace Bit.Core.Services
public string IconsUrl { get; set; } public string IconsUrl { get; set; }
public string NotificationsUrl { get; set; } public string NotificationsUrl { get; set; }
public string EventsUrl { get; set; } public string EventsUrl { get; set; }
public Region SelectedRegion { get; set; }
public string GetWebVaultUrl(bool returnNullIfDefault = false) 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; 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() public async Task SetUrlsFromStorageAsync()
{ {
try try
{ {
var region = await _stateService.GetActiveUserRegionAsync();
var urls = await _stateService.GetEnvironmentUrlsAsync(); var urls = await _stateService.GetEnvironmentUrlsAsync();
if (urls == null) urls ??= await _stateService.GetPreAuthEnvironmentUrlsAsync();
{
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);
if (urls == null || urls.IsEmpty)
{
await SetRegionAsync(Region.US);
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited); _conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited);
return; return;
} }
BaseUrl = urls.Base; await SetRegionAsync(region.Value, urls);
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);
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited); _conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.EnvironmentUrlsInited);
} }
catch (System.Exception ex) catch (System.Exception ex)
@ -96,15 +93,26 @@ namespace Bit.Core.Services
} }
public async Task<EnvironmentUrlData> SetUrlsAsync(EnvironmentUrlData urls) public async Task<EnvironmentUrlData> SetRegionAsync(Region region, EnvironmentUrlData selfHostedUrls = null)
{ {
urls.Base = FormatUrl(urls.Base); EnvironmentUrlData urls;
urls.WebVault = FormatUrl(urls.WebVault);
urls.Api = FormatUrl(urls.Api); if (region == Region.SelfHosted)
urls.Identity = FormatUrl(urls.Identity); {
urls.Icons = FormatUrl(urls.Icons); // If user saves a self-hosted region with empty fields, default to US
urls.Notifications = FormatUrl(urls.Notifications); if (selfHostedUrls.IsEmpty)
urls.Events = FormatUrl(urls.Events); {
return await SetRegionAsync(Region.US);
}
urls = selfHostedUrls.FormatUrls();
}
else
{
urls = region.GetUrls();
}
SelectedRegion = region;
await _stateService.SetPreAuthRegionAsync(region);
await _stateService.SetPreAuthEnvironmentUrlsAsync(urls); await _stateService.SetPreAuthEnvironmentUrlsAsync(urls);
BaseUrl = urls.Base; BaseUrl = urls.Base;
WebVaultUrl = urls.WebVault; WebVaultUrl = urls.WebVault;
@ -113,35 +121,8 @@ namespace Bit.Core.Services
IconsUrl = urls.Icons; IconsUrl = urls.Icons;
NotificationsUrl = urls.Notifications; NotificationsUrl = urls.Notifications;
EventsUrl = urls.Events; EventsUrl = urls.Events;
_apiService.SetUrls(urls);
var envUrls = new EnvironmentUrls();
if (!string.IsNullOrWhiteSpace(BaseUrl))
{
envUrls.Base = BaseUrl;
}
else
{
envUrls.Api = ApiUrl;
envUrls.Identity = IdentityUrl;
envUrls.Events = EventsUrl;
}
_apiService.SetUrls(envUrls);
return 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();
}
} }
} }

View file

@ -8,12 +8,13 @@ using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Newtonsoft.Json;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public class StateMigrationService : IStateMigrationService public class StateMigrationService : IStateMigrationService
{ {
private const int StateVersion = 6; private const int StateVersion = 7;
private readonly DeviceType _deviceType; private readonly DeviceType _deviceType;
private readonly IStorageService _preferencesStorageService; private readonly IStorageService _preferencesStorageService;
@ -86,6 +87,9 @@ namespace Bit.Core.Services
goto case 5; goto case 5;
case 5: case 5:
await MigrateFrom5To6Async(); await MigrateFrom5To6Async();
goto case 6;
case 6:
await MigrateFrom6To7Async();
break; break;
} }
} }
@ -837,6 +841,42 @@ namespace Bit.Core.Services
#endregion #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<State>(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<EnvironmentUrlData>(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 // Helpers
private async Task<int> GetLastStateVersionAsync() private async Task<int> GetLastStateVersionAsync()

View file

@ -1363,6 +1363,21 @@ namespace Bit.Core.Services
_storageMediatorService.Save(Constants.ConfigsKey, value); _storageMediatorService.Save(Constants.ConfigsKey, value);
} }
public async Task<Region?> GetActiveUserRegionAsync()
{
return await GetActiveUserCustomDataAsync(a => a?.Settings?.Region);
}
public async Task<Region?> GetPreAuthRegionAsync()
{
return await _storageMediatorService.GetAsync<Region?>(Constants.RegionEnvironment);
}
public async Task SetPreAuthRegionAsync(Region value)
{
await _storageMediatorService.SaveAsync(Constants.RegionEnvironment, value);
}
// Helpers // Helpers
[Obsolete("Use IStorageMediatorService instead")] [Obsolete("Use IStorageMediatorService instead")]
@ -1552,6 +1567,7 @@ namespace Bit.Core.Services
await CheckStateAsync(); await CheckStateAsync();
account.Settings.EnvironmentUrls = await GetPreAuthEnvironmentUrlsAsync(); account.Settings.EnvironmentUrls = await GetPreAuthEnvironmentUrlsAsync();
account.Settings.Region = await GetPreAuthRegionAsync();
// Storage // Storage
var state = await GetStateFromStorageAsync() ?? new State(); var state = await GetStateFromStorageAsync() ?? new State();

View file

@ -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;
}
}
}
}