Merge branch 'v9/dev' into v9/contrib

This commit is contained in:
Sebastiaan Janssen
2022-04-19 13:16:20 +02:00
42 changed files with 1527 additions and 591 deletions

2
.gitignore vendored
View File

@@ -85,6 +85,8 @@ src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/lib/*
src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/views/*
src/Umbraco.Web.UI/wwwroot/Media/*
src/Umbraco.Web.UI/Smidge/
src/Umbraco.Web.UI/App_Code/
src/Umbraco.Web.UI/App_Plugins/
src/Umbraco.Web.UI/Views/
!src/Umbraco.Web.UI/Views/Partials/blocklist/
!src/Umbraco.Web.UI/Views/Partials/grid/

View File

@@ -0,0 +1,20 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models
{
[DataContract]
public class UserTwoFactorProviderModel
{
public UserTwoFactorProviderModel(string providerName, bool isEnabledOnUser)
{
ProviderName = providerName;
IsEnabledOnUser = isEnabledOnUser;
}
[DataMember(Name = "providerName")]
public string ProviderName { get; }
[DataMember(Name = "isEnabledOnUser")]
public bool IsEnabledOnUser { get; }
}
}

View File

@@ -58,4 +58,12 @@ namespace Umbraco.Cms.Core.Services
/// </summary>
Task<IEnumerable<string>> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey);
}
[Obsolete("This will be merged into ITwoFactorLoginService in Umbraco 11")]
public interface ITwoFactorLoginService2 : ITwoFactorLoginService
{
Task<bool> DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code);
Task<bool> ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code);
}
}

View File

@@ -35,6 +35,7 @@ namespace Umbraco.Cms.Core.Security
private readonly GlobalSettings _globalSettings;
private readonly IUmbracoMapper _mapper;
private readonly AppCaches _appCaches;
private readonly ITwoFactorLoginService _twoFactorLoginService;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeUserStore"/> class.
@@ -48,7 +49,8 @@ namespace Umbraco.Cms.Core.Security
IOptions<GlobalSettings> globalSettings,
IUmbracoMapper mapper,
BackOfficeErrorDescriber describer,
AppCaches appCaches)
AppCaches appCaches,
ITwoFactorLoginService twoFactorLoginService)
: base(describer)
{
_scopeProvider = scopeProvider;
@@ -58,11 +60,36 @@ namespace Umbraco.Cms.Core.Security
_globalSettings = globalSettings.Value;
_mapper = mapper;
_appCaches = appCaches;
_twoFactorLoginService = twoFactorLoginService;
_userService = userService;
_externalLoginService = externalLoginService;
}
[Obsolete("Use ctor injecting IExternalLoginWithKeyService ")]
[Obsolete("Use non obsolete ctor")]
public BackOfficeUserStore(
IScopeProvider scopeProvider,
IUserService userService,
IEntityService entityService,
IExternalLoginWithKeyService externalLoginService,
IOptions<GlobalSettings> globalSettings,
IUmbracoMapper mapper,
BackOfficeErrorDescriber describer,
AppCaches appCaches)
: this(
scopeProvider,
userService,
entityService,
externalLoginService,
globalSettings,
mapper,
describer,
appCaches,
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
{
}
[Obsolete("Use non obsolete ctor")]
public BackOfficeUserStore(
IScopeProvider scopeProvider,
IUserService userService,
@@ -80,11 +107,24 @@ namespace Umbraco.Cms.Core.Security
globalSettings,
mapper,
describer,
appCaches)
appCaches,
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
{
}
/// <inheritdoc />
public override async Task<bool> GetTwoFactorEnabledAsync(BackOfficeIdentityUser user,
CancellationToken cancellationToken = default(CancellationToken))
{
if (!int.TryParse(user.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intUserId))
{
return await base.GetTwoFactorEnabledAsync(user, cancellationToken);
}
return await _twoFactorLoginService.IsTwoFactorEnabledAsync(user.Key);
}
/// <inheritdoc />
public override Task<IdentityResult> CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
{

View File

@@ -597,7 +597,10 @@ namespace Umbraco.Cms.Core.Security
|| (member.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false)
|| (identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value))
{
changeType = MemberDataChangeType.LoginOnly;
// If the LastLoginDate is default on the member we have to do a full save.
// This is because the umbraco property data for the member doesn't exist yet in this case
// meaning we can't just update that property data, but have to do a full save to create it
changeType = member.LastLoginDate == default ? MemberDataChangeType.FullSave : MemberDataChangeType.LoginOnly;
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();

View File

@@ -49,10 +49,10 @@ namespace Umbraco.Cms.Core.Security
public override bool SupportsQueryableUsers => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository
/// <summary>
/// Developers will need to override this to support custom 2 factor auth
/// Both users and members supports 2FA
/// </summary>
/// <inheritdoc />
public override bool SupportsUserTwoFactor => false;
public override bool SupportsUserTwoFactor => true;
/// <inheritdoc />
public override bool SupportsUserPhoneNumber => false; // We haven't needed to support this yet, though might be necessary for 2FA

View File

@@ -3,22 +3,26 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Core.Services
{
/// <inheritdoc />
public class TwoFactorLoginService : ITwoFactorLoginService
public class TwoFactorLoginService : ITwoFactorLoginService2
{
private readonly ITwoFactorLoginRepository _twoFactorLoginRepository;
private readonly IScopeProvider _scopeProvider;
private readonly IOptions<IdentityOptions> _identityOptions;
private readonly IOptions<BackOfficeIdentityOptions> _backOfficeIdentityOptions;
private readonly IDictionary<string, ITwoFactorProvider> _twoFactorSetupGenerators;
private readonly ILogger<TwoFactorLoginService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="TwoFactorLoginService"/> class.
@@ -28,16 +32,34 @@ namespace Umbraco.Cms.Core.Services
IScopeProvider scopeProvider,
IEnumerable<ITwoFactorProvider> twoFactorSetupGenerators,
IOptions<IdentityOptions> identityOptions,
IOptions<BackOfficeIdentityOptions> backOfficeIdentityOptions
)
IOptions<BackOfficeIdentityOptions> backOfficeIdentityOptions,
ILogger<TwoFactorLoginService> logger)
{
_twoFactorLoginRepository = twoFactorLoginRepository;
_scopeProvider = scopeProvider;
_identityOptions = identityOptions;
_backOfficeIdentityOptions = backOfficeIdentityOptions;
_logger = logger;
_twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName);
}
[Obsolete("Use ctor with all params - This will be removed in v11")]
public TwoFactorLoginService(
ITwoFactorLoginRepository twoFactorLoginRepository,
IScopeProvider scopeProvider,
IEnumerable<ITwoFactorProvider> twoFactorSetupGenerators,
IOptions<IdentityOptions> identityOptions,
IOptions<BackOfficeIdentityOptions> backOfficeIdentityOptions)
: this(twoFactorLoginRepository,
scopeProvider,
twoFactorSetupGenerators,
identityOptions,
backOfficeIdentityOptions,
StaticServiceProvider.Instance.GetRequiredService<ILogger<TwoFactorLoginService>>())
{
}
/// <inheritdoc />
public async Task DeleteUserLoginsAsync(Guid userOrMemberKey)
{
@@ -51,6 +73,56 @@ namespace Umbraco.Cms.Core.Services
return await GetEnabledProviderNamesAsync(userOrMemberKey);
}
public async Task<bool> DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code)
{
var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName);
if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator))
{
throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}");
}
var isValid = generator.ValidateTwoFactorPIN(secret, code);
if (!isValid)
{
return false;
}
return await DisableAsync(userOrMemberKey, providerName);
}
public async Task<bool> ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code)
{
try
{
var isValid = ValidateTwoFactorSetup(providerName, secret, code);
if (isValid == false)
{
return false;
}
var twoFactorLogin = new TwoFactorLogin()
{
Confirmed = true,
Secret = secret,
UserOrMemberKey = userOrMemberKey,
ProviderName = providerName
};
await SaveAsync(twoFactorLogin);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not log in with the provided one-time-password");
}
return false;
}
private async Task<IEnumerable<string>> GetEnabledProviderNamesAsync(Guid userOrMemberKey)
{
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);

View File

@@ -74,11 +74,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions;
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly WebRoutingSettings _webRoutingSettings;
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
[ActivatorUtilitiesConstructor]
public AuthenticationController(
public AuthenticationController(
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeUserManager backOfficeUserManager,
IBackOfficeSignInManager signInManager,
@@ -97,7 +98,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
IBackOfficeExternalLoginProviders externalAuthenticationOptions,
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
IHttpContextAccessor httpContextAccessor,
IOptions<WebRoutingSettings> webRoutingSettings)
IOptions<WebRoutingSettings> webRoutingSettings,
ITwoFactorLoginService twoFactorLoginService)
{
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_userManager = backOfficeUserManager;
@@ -118,9 +120,56 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
_httpContextAccessor = httpContextAccessor;
_webRoutingSettings = webRoutingSettings.Value;
_twoFactorLoginService = twoFactorLoginService;
}
[Obsolete("Use constructor that also takes IHttpAccessor and IOptions<WebRoutingSettings>, scheduled for removal in V11")]
[Obsolete("Use constructor that takes all params, scheduled for removal in V11")]
public AuthenticationController(
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeUserManager backOfficeUserManager,
IBackOfficeSignInManager signInManager,
IUserService userService,
ILocalizedTextService textService,
IUmbracoMapper umbracoMapper,
IOptions<GlobalSettings> globalSettings,
IOptions<SecuritySettings> securitySettings,
ILogger<AuthenticationController> logger,
IIpResolver ipResolver,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration,
IEmailSender emailSender,
ISmsSender smsSender,
IHostingEnvironment hostingEnvironment,
LinkGenerator linkGenerator,
IBackOfficeExternalLoginProviders externalAuthenticationOptions,
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
IHttpContextAccessor httpContextAccessor,
IOptions<WebRoutingSettings> webRoutingSettings)
: this(
backofficeSecurityAccessor,
backOfficeUserManager,
signInManager,
userService,
textService,
umbracoMapper,
globalSettings,
securitySettings,
logger,
ipResolver,
passwordConfiguration,
emailSender,
smsSender,
hostingEnvironment,
linkGenerator,
externalAuthenticationOptions,
backOfficeTwoFactorOptions,
StaticServiceProvider.Instance.GetRequiredService<IHttpContextAccessor>(),
StaticServiceProvider.Instance.GetRequiredService<IOptions<WebRoutingSettings>>(),
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
{
}
[Obsolete("Use constructor that takes all params, scheduled for removal in V11")]
public AuthenticationController(
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeUserManager backOfficeUserManager,
@@ -469,7 +518,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return NotFound();
}
var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user);
var userFactors = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key);
return new ObjectResult(userFactors);
}

View File

@@ -235,6 +235,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
"authenticationApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<AuthenticationController>(
controller => controller.PostLogin(null))
},
{
"twoFactorLoginApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<TwoFactorLoginController>(
controller => controller.SetupInfo(null))
},
{
"currentUserApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl<CurrentUserController>(
controller => controller.PostChangePassword(null))

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.BackOffice.Security;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Web.BackOffice.Controllers
{
public class TwoFactorLoginController : UmbracoAuthorizedJsonController
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly ILogger<TwoFactorLoginController> _logger;
private readonly ITwoFactorLoginService2 _twoFactorLoginService;
private readonly IBackOfficeSignInManager _backOfficeSignInManager;
private readonly IBackOfficeUserManager _backOfficeUserManager;
private readonly IOptionsSnapshot<TwoFactorLoginViewOptions> _twoFactorLoginViewOptions;
public TwoFactorLoginController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ILogger<TwoFactorLoginController> logger,
ITwoFactorLoginService twoFactorLoginService,
IBackOfficeSignInManager backOfficeSignInManager,
IBackOfficeUserManager backOfficeUserManager,
IOptionsSnapshot<TwoFactorLoginViewOptions> twoFactorLoginViewOptions)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_logger = logger;
if (twoFactorLoginService is not ITwoFactorLoginService2 twoFactorLoginService2)
{
throw new ArgumentException("twoFactorLoginService needs to implement ITwoFactorLoginService2 until the interfaces are merged", nameof(twoFactorLoginService));
}
_twoFactorLoginService = twoFactorLoginService2;
_backOfficeSignInManager = backOfficeSignInManager;
_backOfficeUserManager = backOfficeUserManager;
_twoFactorLoginViewOptions = twoFactorLoginViewOptions;
}
/// <summary>
/// Used to retrieve the 2FA providers for code submission
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<string>>> GetEnabled2FAProvidersForCurrentUser()
{
var user = await _backOfficeSignInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
_logger.LogWarning("No verified user found, returning 404");
return NotFound();
}
var userFactors = await _backOfficeUserManager.GetValidTwoFactorProvidersAsync(user);
return new ObjectResult(userFactors);
}
[HttpGet]
public async Task<ActionResult<IEnumerable<UserTwoFactorProviderModel>>> Get2FAProvidersForUser(int userId)
{
var user = await _backOfficeUserManager.FindByIdAsync(userId.ToString());
var enabledProviderNameHashSet = new HashSet<string>(await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key));
var providerNames = await _backOfficeUserManager.GetValidTwoFactorProvidersAsync(user);
return providerNames.Select(providerName =>
new UserTwoFactorProviderModel(providerName, enabledProviderNameHashSet.Contains(providerName))).ToArray();
}
[HttpGet]
public async Task<ActionResult<object>> SetupInfo(string providerName)
{
var user = _backOfficeSecurityAccessor?.BackOfficeSecurity.CurrentUser;
var setupInfo = await _twoFactorLoginService.GetSetupInfoAsync(user.Key, providerName);
return setupInfo;
}
[HttpPost]
public async Task<ActionResult<bool>> ValidateAndSave(string providerName, string secret, string code)
{
var user = _backOfficeSecurityAccessor?.BackOfficeSecurity.CurrentUser;
return await _twoFactorLoginService.ValidateAndSaveAsync(providerName, user.Key, secret, code);
}
[HttpPost]
[Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)]
public async Task<ActionResult<bool>> Disable(string providerName, Guid userKey)
{
return await _twoFactorLoginService.DisableAsync(userKey, providerName);
}
[HttpPost]
public async Task<ActionResult<bool>> DisableWithCode(string providerName, string code)
{
Guid key = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Key;
return await _twoFactorLoginService.DisableWithCodeAsync(providerName, key, code);
}
[HttpGet]
public ActionResult<string> ViewPathForProviderName(string providerName)
{
var options = _twoFactorLoginViewOptions.Get(providerName);
return options.SetupViewPath;
}
}
}

View File

@@ -45,6 +45,11 @@ namespace Umbraco.Extensions
{
o.Cookie.Name = Constants.Security.BackOfficeTwoFactorAuthenticationType;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})
.AddCookie(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType, o =>
{
o.Cookie.Name = Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
builder.Services.ConfigureOptions<ConfigureBackOfficeCookieOptions>();

View File

@@ -42,7 +42,8 @@ namespace Umbraco.Extensions
factory.GetRequiredService<IOptions<GlobalSettings>>(),
factory.GetRequiredService<IUmbracoMapper>(),
factory.GetRequiredService<BackOfficeErrorDescriber>(),
factory.GetRequiredService<AppCaches>()
factory.GetRequiredService<AppCaches>(),
factory.GetRequiredService<ITwoFactorLoginService>()
))
.AddUserManager<IBackOfficeUserManager, BackOfficeUserManager>()
.AddSignInManager<IBackOfficeSignInManager, BackOfficeSignInManager>()
@@ -64,7 +65,7 @@ namespace Umbraco.Extensions
services.TryAddScoped<IIpResolver, AspNetCoreIpResolver>();
services.TryAddSingleton<IBackOfficeExternalLoginProviders, BackOfficeExternalLoginProviders>();
services.TryAddSingleton<IBackOfficeTwoFactorOptions, NoopBackOfficeTwoFactorOptions>();
services.TryAddSingleton<IBackOfficeTwoFactorOptions, DefaultBackOfficeTwoFactorOptions>();
return new BackOfficeIdentityBuilder(services);
}

View File

@@ -0,0 +1,10 @@
using System;
namespace Umbraco.Cms.Web.BackOffice.Security
{
public class DefaultBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions
{
public string GetTwoFactorView(string username) => "views\\common\\login-2fa.html";
}
}

View File

@@ -1,5 +1,8 @@
namespace Umbraco.Cms.Web.BackOffice.Security
using System;
namespace Umbraco.Cms.Web.BackOffice.Security
{
[Obsolete("Not used anymore")]
public class NoopBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions
{
public string GetTwoFactorView(string username) => null;

View File

@@ -0,0 +1,13 @@
namespace Umbraco.Cms.Web.BackOffice.Security
{
/// <summary>
/// Options used as named options for 2fa providers
/// </summary>
public class TwoFactorLoginViewOptions
{
/// <summary>
/// Gets or sets the path of the view to show when setting up this 2fa provider
/// </summary>
public string SetupViewPath { get; set; }
}
}

View File

@@ -1,14 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Trees;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Constants = Umbraco.Cms.Core.Constants;
namespace Umbraco.Cms.Web.BackOffice.Trees
@@ -21,6 +24,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
{
private readonly IMemberGroupService _memberGroupService;
[ActivatorUtilitiesConstructor]
public MemberGroupTreeController(
ILocalizedTextService localizedTextService,
UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection,
@@ -31,6 +35,24 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
: base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator, memberTypeService)
=> _memberGroupService = memberGroupService;
[Obsolete("Use ctor with all params")]
public MemberGroupTreeController(
ILocalizedTextService localizedTextService,
UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection,
IMenuItemCollectionFactory menuItemCollectionFactory,
IMemberGroupService memberGroupService,
IEventAggregator eventAggregator)
: this(localizedTextService,
umbracoApiControllerTypeCollection,
menuItemCollectionFactory,
memberGroupService,
eventAggregator,
StaticServiceProvider.Instance.GetRequiredService<IMemberTypeService>())
{
}
protected override IEnumerable<TreeNode> GetTreeNodesFromService(string id, FormCollection queryStrings)
=> _memberGroupService.GetAll()
.OrderBy(x => x.Name)

View File

@@ -74,6 +74,7 @@ namespace Umbraco.Cms.Web.Common.Security
return await base.VerifyPasswordAsync(store, user, password);
}
/// <summary>
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date
/// </summary>

View File

@@ -45,9 +45,6 @@ namespace Umbraco.Cms.Web.Common.Security
_httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc />
public override bool SupportsUserTwoFactor => true;
/// <inheritdoc />
public async Task<bool> IsMemberAuthorizedAsync(IEnumerable<string> allowTypes = null, IEnumerable<string> allowGroups = null, IEnumerable<int> allowMembers = null)
{

View File

@@ -1,7 +1,7 @@
(function () {
"use strict";
function AppHeaderDirective(eventsService, appState, userService, focusService, overlayService, $timeout) {
function AppHeaderDirective(eventsService, appState, userService, focusService, $timeout, editorService) {
function link(scope, element) {
@@ -72,17 +72,15 @@
};
scope.avatarClick = function () {
const dialog = {
view: "user",
position: "right",
name: "overlay-user",
close: function () {
overlayService.close();
}
const userEditor = {
size: "small",
view: "views/common/infiniteeditors/user/user.html",
close: function() {
editorService.close();
}
};
overlayService.open(dialog);
editorService.open(userEditor);
};
scope.logoModal = {

View File

@@ -443,12 +443,16 @@
vm.twoFactor.submitCallback = function submitCallback() {
vm.onLogin();
}
vm.twoFactor.cancelCallback = function cancelCallback() {
vm.showLogin();
}
vm.twoFactor.view = viewPath;
vm.view = "2fa-login";
SetTitle();
}
function resetInputValidation() {
vm.loginStates.submitButton = "init";
vm.confirmPassword = "";
vm.password = "";
vm.login = "";

View File

@@ -0,0 +1,136 @@
/**
* @ngdoc service
* @name umbraco.resources.twoFactorLoginResource
* @function
*
* @description
* Used by the users section to get users 2FA information
*/
(function () {
'use strict';
function twoFactorLoginResource($http, umbRequestHelper) {
/**
* @ngdoc method
* @name umbraco.resources.twoFactorLoginResource#viewPathForProviderName
* @methodOf umbraco.resources.twoFactorLoginResource
*
* @description
* Gets the view path for the specified two factor provider
*
* ##usage
* <pre>
* twoFactorLoginResource.viewPathForProviderName(providerName)
* .then(function(viewPath) {
* alert("It's here");
* });
* </pre>
*
* @returns {Promise} resourcePromise object containing the view path.
*
*/
function viewPathForProviderName(providerName) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"twoFactorLoginApiBaseUrl",
"ViewPathForProviderName",
{providerName : providerName })),
"Failed to retrieve data");
}
/**
* @ngdoc method
* @name umbraco.resources.twoFactorLoginResource#get2FAProvidersForUser
* @methodOf umbraco.resources.twoFactorLoginResource
*
* @description
* Gets the 2fa provider names that is available
*
* ##usage
* <pre>
* twoFactorLoginResource.get2FAProvidersForUser(userKey)
* .then(function(providers) {
* alert("It's here");
* });
* </pre>
*
* @returns {Promise} resourcePromise object containing the an array of { providerName, isEnabledOnUser} .
*
*/
function get2FAProvidersForUser(userId) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"twoFactorLoginApiBaseUrl",
"get2FAProvidersForUser",
{ userId: userId })),
"Failed to retrieve data");
}
function setupInfo(providerName) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"twoFactorLoginApiBaseUrl",
"setupInfo",
{ providerName: providerName })),
"Failed to retrieve data");
}
function validateAndSave(providerName, secret, code) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"twoFactorLoginApiBaseUrl",
"validateAndSave",
{
providerName: providerName,
secret: secret,
code: code
})),
"Failed to retrieve data");
}
function disable(providerName, userKey) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"twoFactorLoginApiBaseUrl",
"disable",
{
providerName: providerName,
userKey: userKey
})),
"Failed to retrieve data");
}
function disableWithCode(providerName, code) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"twoFactorLoginApiBaseUrl",
"disableWithCode",
{
providerName: providerName,
code: code
})),
"Failed to retrieve data");
}
var resource = {
viewPathForProviderName: viewPathForProviderName,
get2FAProvidersForUser:get2FAProvidersForUser,
setupInfo:setupInfo,
validateAndSave:validateAndSave,
disable: disable,
disableWithCode: disableWithCode
};
return resource;
}
angular.module('umbraco.resources').factory('twoFactorLoginResource', twoFactorLoginResource);
})();

View File

@@ -273,7 +273,7 @@ angular.module('umbraco.services')
*/
removeAll: function () {
angularHelper.safeApply($rootScope, function() {
nArray = [];
nArray.length = 0;
});
},

View File

@@ -0,0 +1,79 @@
//used for the user editor overlay
angular.module("umbraco").controller("Umbraco.Editors.ConfigureTwoFactorController",
function ($scope,
localizationService,
notificationsService,
overlayService,
twoFactorLoginResource,
editorService) {
let vm = this;
vm.close = close;
vm.enable = enable;
vm.disable = disable;
vm.code = "";
vm.buttonState = "init";
localizationService.localize("user_configureTwoFactor").then(function (value) {
vm.title = value;
});
function onInit() {
vm.code = "";
twoFactorLoginResource.get2FAProvidersForUser($scope.model.user.id)
.then(function (providers) {
vm.providers = providers;
});
}
function close() {
if ($scope.model.close) {
$scope.model.close();
}
}
function enable(providerName) {
twoFactorLoginResource.viewPathForProviderName(providerName)
.then(function (viewPath) {
var providerSettings = {
user: $scope.model.user,
providerName: providerName,
size: "small",
view: viewPath,
close: function () {
notificationsService.removeAll();
editorService.close();
onInit();
}
};
editorService.open(providerSettings);
}).catch(onError);
}
function disable(provider) {
const disableTwoFactorSettings = {
provider,
user: vm.user,
size: "small",
view: "views/common/infiniteeditors/twofactor/disabletwofactor.html",
close: function () {
editorService.close();
onInit();
}
};
editorService.open(disableTwoFactorSettings);
}
function onError(error) {
vm.buttonState = "error";
overlayService.ysod(error);
}
//initialize
onInit();
});

View File

@@ -0,0 +1,58 @@
<div ng-controller="Umbraco.Editors.ConfigureTwoFactorController as vm">
<umb-editor-view>
<umb-editor-header name="vm.title" name-locked="true" hide-alias="true" hide-icon="true" hide-description="true">
</umb-editor-header>
<umb-editor-container>
<umb-box ng-repeat="provider in vm.providers track by provider.providerName">
<umb-box-header title="{{provider.providerName}}"></umb-box-header>
<umb-box-content>
<umb-control-group>
<div ng-if="provider.isEnabledOnUser">
<p>
<localize key="user_2faProviderIsEnabled"></localize>
<umb-icon icon="icon-check" class="success"></umb-icon>
</p>
<umb-button type="button" ng-if="!model.isCurrentUser" button-style="[warning,block]"
action="vm.disable(provider.providerName)" label="Disable" label-key="actions_disable" size="m"
state="vm.buttonState">
</umb-button>
<div ng-if="model.isCurrentUser">
<umb-button type="button" action="vm.disable(provider)" label="Disable" label-key="actions_disable">
</umb-button>
</div>
</div>
<umb-button type="button" ng-if="!provider.isEnabledOnUser" button-style="[action,block]"
disabled="!model.isCurrentUser" action="vm.enable(provider.providerName)" label="Enable"
label-key="actions_enable" size="m" state="vm.buttonState">
</umb-button>
</umb-control-group>
</umb-box-content>
</umb-box>
</umb-editor-container>
<umb-editor-footer>
<umb-editor-footer-content-right>
<umb-button type="button" button-style="link" label-key="general_close" shortcut="esc" action="vm.close()">
</umb-button>
</umb-editor-footer-content-right>
</umb-editor-footer>
</umb-editor-view>
</div>

View File

@@ -0,0 +1,53 @@
//used for the user editor overlay
angular.module("umbraco").controller("Umbraco.Editors.DisableTwoFactorController",
function ($scope,
localizationService,
notificationsService,
overlayService,
twoFactorLoginResource) {
let vm = this;
vm.close = close;
vm.disableWithCode = disableWithCode;
vm.code = "";
vm.buttonState = "init";
vm.authForm = {};
if (!$scope.model.provider) {
notificationsService.error("No provider specified");
}
vm.provider = $scope.model.provider;
vm.title = vm.provider.providerName;
function close() {
if ($scope.model.close) {
$scope.model.close();
}
}
function disableWithCode() {
vm.authForm.token.$setValidity("token", true);
vm.buttonState = "busy";
twoFactorLoginResource.disableWithCode(vm.provider.providerName, vm.code)
.then(onResponse)
.catch(onError);
}
function onResponse(response) {
if (response) {
vm.buttonState = "success";
localizationService.localize("user_2faProviderIsDisabledMsg").then(function (value) {
notificationsService.info(value);
});
close();
} else {
vm.buttonState = "error";
vm.authForm.token.$setValidity("token", false);
}
}
function onError(error) {
vm.buttonState = "error";
overlayService.ysod(error);
}
});

View File

@@ -0,0 +1,56 @@
<div ng-controller="Umbraco.Editors.DisableTwoFactorController as vm">
<umb-editor-view>
<form name="vm.authForm" method="POST" ng-submit="vm.disableWithCode()">
<umb-editor-header name="vm.title" name-locked="true" hide-alias="true" hide-icon="true" hide-description="true">
</umb-editor-header>
<umb-editor-container>
<umb-box>
<umb-box-header title-key="actions_disable"></umb-box-header>
<umb-box-content>
<umb-control-group label-for="token" alias="2facode" label="@login_2faCodeInput"
description="@user_2faDisableText" required="true">
<input umb-auto-focus id="2facode" class="-full-width-input input-xlarge" type="text" name="token"
inputmode="numeric" autocomplete="one-time-code" ng-model="vm.code" localize="placeholder"
placeholder="@login_2faCodeInputHelp" aria-required="true" required no-dirty-check />
<div ng-messages="vm.authForm.token.$error" role="alert">
<span class="umb-validation-label" ng-message="token">
<localize key="login_2faInvalidCode">Invalid code entered</localize>
</span>
</div>
</umb-control-group>
</umb-box-content>
</umb-box>
</umb-editor-container>
<umb-editor-footer>
<umb-editor-footer-content-right>
<umb-button type="button" button-style="link" label-key="general_close" shortcut="esc" action="vm.close()">
</umb-button>
<umb-button type="submit" button-style="warning" label="Disable" label-key="actions_disable" size="m"
disabled="vm.code.length === 0" state="vm.buttonState">
</umb-button>
</umb-editor-footer-content-right>
</umb-editor-footer>
</form>
</umb-editor-view>
</div>

View File

@@ -0,0 +1,219 @@
angular.module("umbraco")
.controller("Umbraco.Editors.UserController", function ($scope, $location, $timeout,
dashboardResource, userService, historyService, eventsService,
externalLoginInfoService, authResource,
currentUserResource, formHelper, localizationService, editorService, twoFactorLoginResource) {
let vm = this;
vm.history = historyService.getCurrent();
vm.showPasswordFields = false;
vm.changePasswordButtonState = "init";
vm.hasTwoFactorProviders = false;
localizationService.localize("general_user").then(function (value) {
vm.title = value;
});
// Set flag if any have deny local login, in which case we must disable all password functionality
vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin();
// Only include login providers that have editable options
vm.externalLoginProviders = externalLoginInfoService.getLoginProvidersWithOptions();
vm.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl;
var evts = [];
evts.push(eventsService.on("historyService.add", function (e, args) {
vm.history = args.all;
}));
evts.push(eventsService.on("historyService.remove", function (e, args) {
vm.history = args.all;
}));
evts.push(eventsService.on("historyService.removeAll", function (e, args) {
vm.history = [];
}));
vm.logout = function () {
//Add event listener for when there are pending changes on an editor which means our route was not successful
var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) {
//one time listener, remove the event
pendingChangeEvent();
vm.close();
});
//perform the path change, if it is successful then the promise will resolve otherwise it will fail
vm.close();
$location.path("/logout").search('');
};
vm.gotoHistory = function (link) {
$location.path(link);
vm.close();
};
/*
//Manually update the remaining timeout seconds
function updateTimeout() {
$timeout(function () {
if (vm.remainingAuthSeconds > 0) {
vm.remainingAuthSeconds--;
$scope.$digest();
//recurse
updateTimeout();
}
}, 1000, false); // 1 second, do NOT execute a global digest
}
*/
function updateUserInfo() {
//get the user
userService.getCurrentUser().then(function (user) {
vm.user = user;
if (vm.user) {
vm.remainingAuthSeconds = vm.user.remainingAuthSeconds;
vm.canEditProfile = _.indexOf(vm.user.allowedSections, "users") > -1;
//set the timer
//updateTimeout();
currentUserResource.getCurrentUserLinkedLogins().then(function (logins) {
//reset all to be un-linked
vm.externalLoginProviders.forEach(provider => provider.linkedProviderKey = undefined);
//set the linked logins
for (var login in logins) {
var found = _.find(vm.externalLoginProviders, function (i) {
return i.authType == login;
});
if (found) {
found.linkedProviderKey = logins[login];
}
}
});
//go get the config for the membership provider and add it to the model
authResource.getPasswordConfig(user.id).then(function (data) {
vm.changePasswordModel.config = data;
//ensure the hasPassword config option is set to true (the user of course has a password already assigned)
//this will ensure the oldPassword is shown so they can change it
// disable reset password functionality beacuse it does not make sense inside the backoffice
vm.changePasswordModel.config.hasPassword = true;
vm.changePasswordModel.config.disableToggle = true;
});
twoFactorLoginResource.get2FAProvidersForUser(vm.user.id).then(function (providers) {
vm.hasTwoFactorProviders = providers.length > 0;
});
}
});
}
vm.linkProvider = function (e) {
e.target.submit();
}
vm.unlink = function (e, loginProvider, providerKey) {
var result = confirm("Are you sure you want to unlink this account?");
if (!result) {
e.preventDefault();
return;
}
authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) {
updateUserInfo();
});
}
//create the initial model for change password
vm.changePasswordModel = {
config: {},
value: {}
};
updateUserInfo();
//remove all event handlers
$scope.$on('$destroy', function () {
for (var e = 0; e < evts.length; e++) {
evts[e]();
}
});
vm.changePassword = function () {
if (formHelper.submitForm({ scope: $scope })) {
vm.changePasswordButtonState = "busy";
currentUserResource.changePassword(vm.changePasswordModel.value).then(function (data) {
//reset old data
clearPasswordFields();
formHelper.resetForm({ scope: $scope });
vm.changePasswordButtonState = "success";
$timeout(function () {
vm.togglePasswordFields();
}, 2000);
}, function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
formHelper.handleError(err);
vm.changePasswordButtonState = "error";
});
}
};
vm.togglePasswordFields = function () {
clearPasswordFields();
vm.showPasswordFields = !vm.showPasswordFields;
}
function clearPasswordFields() {
vm.changePasswordModel.value.oldPassword = "";
vm.changePasswordModel.value.newPassword = "";
vm.changePasswordModel.value.confirm = "";
}
vm.editUser = function () {
$location
.path('/users/users/user/' + vm.user.id);
vm.close();
}
vm.toggleConfigureTwoFactor = function () {
const configureTwoFactorSettings = {
create: true,
user: vm.user,
isCurrentUser: true,// From this view we are always current user (used by the overlay)
size: "small",
view: "views/common/infiniteeditors/twofactor/configuretwofactor.html",
close: function () {
editorService.close();
}
};
editorService.open(configureTwoFactorSettings);
}
vm.close = function () {
if ($scope.model.close) {
$scope.model.close();
}
}
dashboardResource.getDashboard("user-dialog").then(function (dashboard) {
vm.dashboard = dashboard;
});
});

View File

@@ -0,0 +1,145 @@
<div ng-controller="Umbraco.Editors.UserController as vm">
<umb-editor-view data-element="overlay-user">
<umb-editor-header name="vm.user.name" name-locked="true" hide-alias="true" hide-icon="true"
hide-description="true">
</umb-editor-header>
<umb-editor-container>
<div ng-if="!vm.showPasswordFields">
<umb-box>
<umb-box-header title-key="user_yourProfile"></umb-box-header>
<umb-box-content>
<umb-control-group>
<div class="flex">
<umb-button action="vm.editUser()" alias="editUser" button-style="action" label="Edit"
label-key="general_edit" ng-if="vm.canEditProfile" type="button">
</umb-button>
<umb-button action="vm.togglePasswordFields()" alias="changePassword" button-style="action"
label="Change password" label-key="general_changePassword" ng-if="!vm.denyLocalLogin" type="button">
</umb-button>
<umb-button type="button" button-style="action" action="vm.toggleConfigureTwoFactor()"
label="Configure Two-Factor" label-key="user_configureTwoFactor" ng-if="vm.hasTwoFactorProviders">
</umb-button>
</div>
</umb-control-group>
</umb-box-content>
</umb-box>
<umb-box class="umb-control-group external-logins" ng-if="vm.externalLoginProviders.length > 0">
<umb-box-header title-key="defaultdialogs_externalLoginProviders" title="External login providers">
</umb-box-header>
<umb-box-content>
<umb-control-group>
<div ng-repeat="login in vm.externalLoginProviders">
<div ng-if="login.customView" ng-include="login.customView"></div>
<div ng-if="!login.customView && login.properties.AutoLinkOptions.AllowManualLinking">
<form action="{{vm.externalLinkLoginFormAction}}" id="oauthloginform-{{login.authType}}" method="POST"
name="oauthloginform" ng-if="login.linkedProviderKey == undefined"
ng-submit="vm.linkProvider($event)">
<input name="provider" type="hidden" value="{{login.authType}}" />
<button class="btn btn-block btn-social" id="{{login.authType}}"
ng-class="login.properties.ButtonStyle" title="{{login.caption}}">
<umb-icon icon="{{login.properties.Icon}}" style="height:100%;"></umb-icon>
<localize key="defaultdialogs_linkYour">Link your</localize>&nbsp;{{login.caption}}&nbsp;<localize
key="defaultdialogs_account">account
</localize>
</button>
</form>
<button class="btn btn-block btn-social" id="{{login.authType}}" name="provider"
ng-class="login.properties.ButtonStyle"
ng-click="vm.unlink($event, login.authType, login.linkedProviderKey)"
ng-if="login.linkedProviderKey != undefined" value="{{login.authType}}">
<umb-icon icon="{{login.properties.Icon}}" style="height:100%;"></umb-icon>
<localize key="defaultdialogs_unLinkYour">Un-link your</localize>&nbsp;{{login.caption}}&nbsp;
<localize key="defaultdialogs_account">account
</localize>
</button>
</div>
</div>
</umb-control-group>
</umb-box-content>
</umb-box>
<umb-box ng-if="vm.history.length">
<umb-box-header title-key="user_yourHistory"></umb-box-header>
<umb-box-content>
<umb-control-group>
<ul class="umb-tree">
<li ng-repeat="item in vm.history | orderBy:'time':true">
<a ng-click="vm.gotoHistory(item.link)" ng-href="{{item.link}}" prevent-default>
<umb-icon icon="{{item.icon}}"></umb-icon>
{{item.name}}
({{ item.time | date : 'medium' }})
</a>
</li>
</ul>
</umb-control-group>
</umb-box-content>
</umb-box>
</div>
<div ng-if="vm.showPasswordFields && !vm.denyLocalLogin">
<h5>
<localize key="general_changePassword">Change password</localize>
</h5>
<form class="block-form" name="passwordForm" ng-submit="vm.changePassword()" novalidate val-form-manager>
<change-password config="vm.changePasswordModel.config" password-values="vm.changePasswordModel.value">
</change-password>
<umb-button action="vm.togglePasswordFields()" button-style="cancel" label="Back" label-key="general_back"
type="button">
</umb-button>
<umb-button button-style="success" label="Change password" label-key="general_changePassword"
state="changePasswordButtonState" type="submit">
</umb-button>
</form>
</div>
<div class="umb-control-group" ng-if="vm.dashboard.length > 0">
<div ng-repeat="tab in vm.dashboard">
<h5 ng-if="tab.label">{{tab.label}}</h5>
<div ng-repeat="property in tab.properties">
<div ng-include="property.view"></div>
</div>
</div>
</div>
</umb-editor-container>
<umb-editor-footer>
<umb-editor-footer-content-right>
<umb-button class="btn-group-vertical" data-element="button-overlayClose" type="button" button-style="link"
label-key="general_close" shortcut="esc" action="vm.close()">
</umb-button>
<umb-button class="btn-group-vertical" type="button" label-key="general_logout" shortcut="ctrl+shift+l"
action="vm.logout()" alias="logOut" button-style="danger" label="Log out">
</umb-button>
</umb-editor-footer-content-right>
</umb-editor-footer>
</umb-editor-view>
</div>

View File

@@ -0,0 +1,38 @@
angular.module("umbraco").controller("Umbraco.Login2faController",
function ($scope, userService, authResource) {
let vm = this;
vm.code = "";
vm.provider = "";
vm.providers = [];
vm.stateValidateButton = "init";
vm.authForm = {};
authResource.get2FAProviders()
.then(function (data) {
vm.providers = data;
if (vm.providers.length > 0) {
vm.provider = vm.providers[0];
}
});
vm.validate = function () {
vm.error = "";
vm.stateValidateButton = "busy";
vm.authForm.token.$setValidity('token', true);
authResource.verify2FACode(vm.provider, vm.code)
.then(function (data) {
vm.stateValidateButton = "success";
userService.setAuthenticationSuccessful(data);
$scope.vm.twoFactor.submitCallback();
})
.catch(function () {
vm.stateValidateButton = "error";
vm.authForm.token.$setValidity('token', false);
});
};
vm.goBack = function () {
$scope.vm.twoFactor.cancelCallback();
}
});

View File

@@ -0,0 +1,44 @@
<div ng-controller="Umbraco.Login2faController as cvm" class="umb-login-container">
<div id="twoFactorlogin" ng-cloak="">
<form name="cvm.authForm" method="POST" name="twoFactorCodeForm" ng-submit="cvm.validate()">
<header class="h4">
<localize key="login_2fatitle">One last step</localize>
</header>
<p>
<localize key="login_2faText">You have enabled 2-factor authentication and must verify your identity.</localize>
</p>
<br>
<!-- if there's only one provider active, it will skip this step! -->
<umb-control-group ng-if="cvm.providers.length > 1" label="@login_2faMultipleText" label-for="provider"
alias="2faprovider">
<select id="2faprovider" name="provider" ng-model="cvm.provider">
<option ng-repeat="provider in cvm.providers" ng-value="provider">{{provider}}</option>
</select>
</umb-control-group>
<umb-control-group label-for="token" alias="2facode" label="@login_2faCodeInput"
description="@user_2faDisableText" required="true">
<input umb-auto-focus id="2facode" class="-full-width-input input-xlarge" type="text" name="token"
inputmode="numeric" autocomplete="one-time-code" ng-model="cvm.code" localize="placeholder"
placeholder="@login_2faCodeInputHelp" aria-required="true" required no-dirty-check />
<div ng-messages="cvm.authForm.token.$error" role="alert">
<span class="umb-validation-label" ng-message="token">
<localize key="login_2faInvalidCode">Invalid code entered</localize>
</span>
</div>
</umb-control-group>
<div class="flex justify-between items-center">
<umb-button button-style="success" size="m" label-key="general_validate" state="cvm.stateValidateButton"
type="submit" disabled="cvm.code.length === 0"></umb-button>
<umb-button size="m" label-key="general_back" type="button" action="cvm.goBack()">
</umb-button>
</div>
</form>
</div>
</div>

View File

@@ -1,194 +0,0 @@
angular.module("umbraco")
.controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout,
dashboardResource, userService, historyService, eventsService,
externalLoginInfo, externalLoginInfoService, authResource,
currentUserResource, formHelper, localizationService) {
$scope.history = historyService.getCurrent();
//$scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion;
$scope.showPasswordFields = false;
$scope.changePasswordButtonState = "init";
$scope.model.title = "user.name";
//$scope.model.subtitle = "Umbraco version" + " " + $scope.version;
/*
if(!$scope.model.title) {
localizationService.localize("general_user").then(function(value){
$scope.model.title = value;
});
}
*/
// Set flag if any have deny local login, in which case we must disable all password functionality
$scope.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin();
// Only include login providers that have editable options
$scope.externalLoginProviders = externalLoginInfoService.getLoginProvidersWithOptions();
$scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl;
var evts = [];
evts.push(eventsService.on("historyService.add", function (e, args) {
$scope.history = args.all;
}));
evts.push(eventsService.on("historyService.remove", function (e, args) {
$scope.history = args.all;
}));
evts.push(eventsService.on("historyService.removeAll", function (e, args) {
$scope.history = [];
}));
$scope.logout = function () {
//Add event listener for when there are pending changes on an editor which means our route was not successful
var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) {
//one time listener, remove the event
pendingChangeEvent();
$scope.model.close();
});
//perform the path change, if it is successful then the promise will resolve otherwise it will fail
$scope.model.close();
$location.path("/logout").search('');
};
$scope.gotoHistory = function (link) {
$location.path(link);
$scope.model.close();
};
/*
//Manually update the remaining timeout seconds
function updateTimeout() {
$timeout(function () {
if ($scope.remainingAuthSeconds > 0) {
$scope.remainingAuthSeconds--;
$scope.$digest();
//recurse
updateTimeout();
}
}, 1000, false); // 1 second, do NOT execute a global digest
}
*/
function updateUserInfo() {
//get the user
userService.getCurrentUser().then(function (user) {
$scope.user = user;
if ($scope.user) {
$scope.model.title = user.name;
$scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds;
$scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1;
//set the timer
//updateTimeout();
currentUserResource.getCurrentUserLinkedLogins().then(function(logins) {
//reset all to be un-linked
$scope.externalLoginProviders.forEach(provider => provider.linkedProviderKey = undefined);
//set the linked logins
for (var login in logins) {
var found = _.find($scope.externalLoginProviders, function (i) {
return i.authType == login;
});
if (found) {
found.linkedProviderKey = logins[login];
}
}
});
//go get the config for the membership provider and add it to the model
authResource.getPasswordConfig(user.id).then(function (data) {
$scope.changePasswordModel.config = data;
//ensure the hasPassword config option is set to true (the user of course has a password already assigned)
//this will ensure the oldPassword is shown so they can change it
// disable reset password functionality beacuse it does not make sense inside the backoffice
$scope.changePasswordModel.config.hasPassword = true;
$scope.changePasswordModel.config.disableToggle = true;
});
}
});
}
$scope.linkProvider = function (e) {
e.target.submit();
}
$scope.unlink = function (e, loginProvider, providerKey) {
var result = confirm("Are you sure you want to unlink this account?");
if (!result) {
e.preventDefault();
return;
}
authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) {
updateUserInfo();
});
}
//create the initial model for change password
$scope.changePasswordModel = {
config: {},
value: {}
};
updateUserInfo();
//remove all event handlers
$scope.$on('$destroy', function () {
for (var e = 0; e < evts.length; e++) {
evts[e]();
}
});
$scope.changePassword = function() {
if (formHelper.submitForm({ scope: $scope })) {
$scope.changePasswordButtonState = "busy";
currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) {
//reset old data
clearPasswordFields();
formHelper.resetForm({ scope: $scope });
$scope.changePasswordButtonState = "success";
$timeout(function() {
$scope.togglePasswordFields();
}, 2000);
}, function (err) {
formHelper.resetForm({ scope: $scope, hasErrors: true });
formHelper.handleError(err);
$scope.changePasswordButtonState = "error";
});
}
};
$scope.togglePasswordFields = function() {
clearPasswordFields();
$scope.showPasswordFields = !$scope.showPasswordFields;
}
function clearPasswordFields() {
$scope.changePasswordModel.value.oldPassword = "";
$scope.changePasswordModel.value.newPassword = "";
$scope.changePasswordModel.value.confirm = "";
}
$scope.editUser = function() {
$location
.path('/users/users/user/' + $scope.user.id);
$scope.model.close();
}
dashboardResource.getDashboard("user-dialog").then(function (dashboard) {
$scope.dashboard = dashboard;
});
});

View File

@@ -1,143 +0,0 @@
<div ng-controller="Umbraco.Overlays.UserController">
<div class="umb-control-group" ng-if="!showPasswordFields">
<h5>
<localize key="user_yourProfile">Your profile</localize>
</h5>
<umb-button
action="editUser()"
alias="editUser"
button-style="action"
label="Edit"
label-key="general_edit"
ng-if="canEditProfile"
type="button">
</umb-button>
<umb-button
action="togglePasswordFields()"
alias="changePassword"
button-style="action"
label="Change password"
label-key="general_changePassword"
ng-if="!denyLocalLogin"
type="button">
</umb-button>
<umb-button
action="logout()"
alias="logOut"
button-style="danger"
label="Log out"
label-key="general_logout"
shortcut="ctrl+shift+l"
type="button">
</umb-button>
</div>
<div class="umb-control-group external-logins" ng-if="externalLoginProviders.length > 0 && !showPasswordFields">
<h5>
<localize key="defaultdialogs_externalLoginProviders">External login providers</localize>
</h5>
<div ng-repeat="login in externalLoginProviders">
<div ng-if="login.customView" ng-include="login.customView"></div>
<div ng-if="!login.customView && login.properties.AutoLinkOptions.AllowManualLinking">
<form action="{{externalLinkLoginFormAction}}" id="oauthloginform-{{login.authType}}" method="POST"
name="oauthloginform" ng-if="login.linkedProviderKey == undefined" ng-submit="linkProvider($event)">
<input name="provider" type="hidden" value="{{login.authType}}"/>
<button class="btn btn-block btn-social"
id="{{login.authType}}"
ng-class="login.properties.ButtonStyle">
<umb-icon icon="{{login.properties.Icon}}" style="height:100%;"></umb-icon>
<localize key="defaultdialogs_linkYour">Link your</localize>&nbsp;{{login.caption}}&nbsp;<localize
key="defaultdialogs_account">account
</localize>
</button>
</form>
<button class="btn btn-block btn-social"
id="{{login.authType}}"
name="provider"
ng-class="login.properties.ButtonStyle"
ng-click="unlink($event, login.authType, login.linkedProviderKey)"
ng-if="login.linkedProviderKey != undefined"
value="{{login.authType}}">
<umb-icon icon="{{login.properties.Icon}}" style="height:100%;"></umb-icon>
<localize key="defaultdialogs_unLinkYour">Un-link your</localize>&nbsp;{{login.caption}}&nbsp;<localize
key="defaultdialogs_account">account
</localize>
</button>
</div>
</div>
</div>
<div class="umb-control-group" ng-if="!showPasswordFields && history.length">
<h5>
<localize key="user_yourHistory">Your recent history</localize>
</h5>
<ul class="umb-tree">
<li ng-repeat="item in history | orderBy:'time':true">
<a ng-click="gotoHistory(item.link)" ng-href="{{item.link}}" prevent-default>
<umb-icon icon="{{item.icon}}"></umb-icon>
{{item.name}}
</a>
</li>
</ul>
</div>
<div ng-if="showPasswordFields && !denyLocalLogin">
<h5>
<localize key="general_changePassword">Change password</localize>
</h5>
<form
class="block-form"
name="passwordForm"
ng-submit="changePassword()"
novalidate
val-form-manager>
<change-password config="changePasswordModel.config"
password-values="changePasswordModel.value">
</change-password>
<umb-button
action="togglePasswordFields()"
button-style="cancel"
label="Back"
label-key="general_back"
type="button">
</umb-button>
<umb-button
button-style="success"
label="Change password"
label-key="general_changePassword"
state="changePasswordButtonState"
type="submit">
</umb-button>
</form>
</div>
<div class="umb-control-group" ng-if="dashboard.length > 0">
<div ng-repeat="tab in dashboard">
<h5 ng-if="tab.label">{{tab.label}}</h5>
<div ng-repeat="property in tab.properties">
<div ng-include="property.view"></div>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
(function () {
"use strict";
function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource,
function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource, twoFactorLoginResource,
userService, contentEditingHelper, localizationService, mediaHelper, Upload, umbRequestHelper,
usersHelper, authResource, dateHelper, editorService, overlayService, externalLoginInfoService) {
@@ -16,6 +16,7 @@
};
vm.breadcrumbs = [];
vm.showBackButton = true;
vm.hasTwoFactorProviders = false;
vm.avatarFile = {};
vm.labels = {};
vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB";
@@ -38,6 +39,7 @@
vm.disableUser = disableUser;
vm.enableUser = enableUser;
vm.unlockUser = unlockUser;
vm.toggleConfigureTwoFactor = toggleConfigureTwoFactor;
vm.resendInvite = resendInvite;
vm.deleteNonLoggedInUser = deleteNonLoggedInUser;
vm.changeAvatar = changeAvatar;
@@ -101,6 +103,10 @@
$scope.$emit("$setAccessibleHeader", false, "general_user", false, vm.user.name, "", true);
vm.loading = false;
});
twoFactorLoginResource.get2FAProvidersForUser(vm.user.id).then(function (providers) {
vm.hasTwoFactorProviders = providers.length > 0;
});
});
}
@@ -397,6 +403,23 @@
});
}
function toggleConfigureTwoFactor() {
var configureTwoFactorSettings = {
create: true,
user: vm.user,
isCurrentUser: vm.user.isCurrentUser,
size: "small",
view: "views/common/infiniteeditors/twofactor/configuretwofactor.html",
close: function() {
editorService.close();
}
};
editorService.open(configureTwoFactorSettings);
}
function resendInvite() {
vm.resendInviteButtonState = "busy";

View File

@@ -6,6 +6,7 @@
var vm = this;
vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin();
}
angular.module("umbraco").controller("Umbraco.Editors.Users.DetailsController", DetailsController);

View File

@@ -274,6 +274,16 @@
size="s">
</umb-button>
</div>
<div ng-if="model.hasTwoFactorProviders">
<umb-button type="button"
button-style="[action,block]"
action="model.toggleConfigureTwoFactor()"
label="Configure Two-Factor"
label-key="user_configureTwoFactor"
size="s">
</umb-button>
</div>
<div>
<umb-button type="button" ng-if="!model.user.lastLoginDate"
button-style="[danger,block]"

View File

@@ -37,6 +37,10 @@
<PackageReference Include="Umbraco.Code" Version="1.2.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\umbraco\views\common\infiniteeditors\twofactor\enabletwofactor.html" />
</ItemGroup>
<PropertyGroup>
<RazorCompileOnBuild>false</RazorCompileOnBuild>
<RazorCompileOnPublish>false</RazorCompileOnPublish>

View File

@@ -565,11 +565,11 @@
<area alias="dictionaryItem">
<key alias="description"><![CDATA[
Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder.<br />Du tilføjer flere sprog under 'sprog' i menuen til venstre </key>
]]></key>
]]> </key>
<key alias="displayName">Kulturnavn</key>
<key alias="changeKeyError"><![CDATA[
Navnet '%0%' eksisterer allerede.
]]></key>
]]> </key>
<key alias="overviewTitle">Ordbogsoversigt</key>
</area>
<area alias="examineManagement">
@@ -902,7 +902,7 @@
<key alias="databaseHeader">Database konfiguration</key>
<key alias="databaseInstall"><![CDATA[
Klik på <strong>installér</strong> knappen for at installere Umbraco %0% databasen
]]></key>
]]> </key>
<key alias="databaseInstallDone">
<![CDATA[Umbraco %0% er nu blevet kopieret til din database. Tryk på <string>Næste</strong> for at fortsætte.]]></key>
<key alias="databaseNotFound"><![CDATA[<p>Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.</p>
@@ -981,7 +981,7 @@
<key alias="theEndInstallFailed">
<![CDATA[For at afslutte installationen er du nødt til manuelt at rette <strong>/web.config filen</strong> og opdatére 'AppSetting' feltet <strong>UmbracoConfigurationStatus</strong> i bunden til <strong>'%0%'</strong>.]]></key>
<key alias="theEndInstallSuccess"><![CDATA[Du kan <strong>komme igang med det samme</strong> ved at klikke på "Start Umbraco" knappen nedenfor.<br/>Hvis du er <strong>ny med Umbraco</strong>, kan du finde masser af ressourcer på vores 'getting started' sider.
]]></key>
]]> </key>
<key alias="theEndOpenUmbraco">
<![CDATA[<h3>Start Umbraco</h3>For at administrere dit website skal du blot åbne Umbraco administrationen og begynde at tilføje indhold, opdatere skabelonerne og stylesheets'ene eller tilføje ny funktionalitet.]]></key>
<key alias="Unavailable">Forbindelse til databasen fejlede.</key>
@@ -1028,6 +1028,12 @@
<key alias="resetPasswordEmailCopySubject">Umbraco: Nulstil adgangskode</key>
<key alias="resetPasswordEmailCopyFormat">
<![CDATA[<p>Dit brugernavn til at logge på Umbraco backoffice er: <strong>%0%</strong></p><p>Klik <a href="%1%"><strong>her</strong></a> for at nulstille din adgangskode eller kopier/indsæt denne URL i din browser:</p><p><em>%1%</em></p>]]></key>
<key alias="2faTitle">Sidste skridt</key>
<key alias="2faText">Det er påkrævet at du verificerer din identitet.</key>
<key alias="2faMultipleText">Vælg venligst en autentificeringsmetode</key>
<key alias="2faCodeInput">Kode</key>
<key alias="2faCodeInputHelp">Indtast venligst koden fra dit device</key>
<key alias="2faInvalidCode">Koden kunne ikke genkendes</key>
</area>
<area alias="main">
<key alias="dashboard">Skrivebord</key>
@@ -1065,7 +1071,7 @@ Gå til http://%4%/#/content/content/edit/%5% for at redigere.
Ha' en dejlig dag!
Mange hilsner fra Umbraco robotten
]]></key>
]]> </key>
<key alias="mailBodyHtml"><![CDATA[<p>Hej %0%</p>
<p>Dette er en automatisk mail for at informere dig om at opgaven <strong>'%1%'</strong>
er blevet udførtpå siden <a href="http://%4%/#/content/content/edit/%5%"><strong>'%2%'</strong></a> af brugeren <strong>'%3%'</strong> </p>
@@ -1166,14 +1172,14 @@ Mange hilsner fra Umbraco robotten
<key alias="contentPublishedFailedAwaitingRelease">Udgivelsen kunne ikke udgives da publiceringsdato er sat</key>
<key alias="contentPublishedFailedIsTrashed"><![CDATA[
%0% kunne ikke publiceres da elementet er i skraldespanden.
]]></key>
]]> </key>
<key alias="contentPublishedFailedExpired">
<![CDATA[
%0% Udgivelsen kunne ikke blive publiceret da publiceringsdatoen er overskredet
]]></key>
]]> </key>
<key alias="contentPublishedFailedInvalid"><![CDATA[
%0% kunne ikke publiceres da følgende egenskaber : %1% ikke overholdte valderingsreglerne.
]]></key>
]]> </key>
<key alias="contentPublishedFailedByEvent">%0% kunne ikke udgives, fordi et 3. parts modul annullerede handlingen
</key>
<key alias="contentPublishedFailedByMissingName"><![CDATA[%0% kan ikke udgives, fordi det mangler et navn.]]></key>
@@ -1453,24 +1459,24 @@ Mange hilsner fra Umbraco robotten
<key alias="renderBodyDesc"><![CDATA[
Henter indholdet af en underliggende skabelon ind, ved at
indsætte et <code>@RenderBody()</code> element.
]]></key>
]]> </key>
<key alias="defineSection">Definer en sektion</key>
<key alias="defineSectionDesc"><![CDATA[
Definerer en del af din skabelon som en navngivet sektion, ved at
omkranse den i <code>@section { ... }</code>. Herefter kan denne sektion flettes ind i
overliggende skabelon ved at indsætte et <code>@RenderSection</code> element.
]]></key>
]]> </key>
<key alias="renderSection">Indsæt en sektion</key>
<key alias="renderSectionDesc"><![CDATA[
Henter indholdet af en sektion fra den underliggende skabelon ind, ved at indsætte et
<code>@RenderSection(name)</code> element. Den underliggende skabelon skal have
defineret en sektion via et <code>@section [name]{ ... }</code> element.
]]></key>
]]> </key>
<key alias="sectionName">Sektionsnavn</key>
<key alias="sectionMandatory">Sektionen er obligatorisk</key>
<key alias="sectionMandatoryDesc"><![CDATA[
Hvis obligatorisk, skal underskabelonen indeholde en <code>@section</code> -definition.
]]></key>
]]> </key>
<key alias="queryBuilder">Query builder</key>
<key alias="itemsReturned">sider returneret, på</key>
<key alias="iWant">Returner</key>
@@ -1918,6 +1924,9 @@ Mange hilsner fra Umbraco robotten
<key alias="sortCreateDateDescending">Ældste</key>
<key alias="sortLastLoginDateDescending">Sidst logget ind</key>
<key alias="noUserGroupsAdded">Ingen brugere er blevet tilføjet</key>
<key alias="2faDisableText">Hvis du ønsker at slå denne autentificeringsmetode fra, så skal du nu indtaste koden fra dit device:</key>
<key alias="2faProviderIsEnabled">Denne autentificeringsmetode er slået til</key>
<key alias="2faProviderIsDisabledMsg">Den valgte autentificeringsmetode er nu slået fra</key>
</area>
<area alias="validation">
<key alias="validation">Validering</key>

View File

@@ -1149,6 +1149,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
]]></key>
<key alias="mfaSecurityCodeSubject">Umbraco: Security Code</key>
<key alias="mfaSecurityCodeMessage">Your security code is: %0%</key>
<key alias="2faTitle">One last step</key>
<key alias="2faText">You have enabled 2-factor authentication and must verify your identity.</key>
<key alias="2faMultipleText">Please choose a 2-factor provider</key>
<key alias="2faCodeInput">Verification code</key>
<key alias="2faCodeInputHelp">Please enter the verification code</key>
<key alias="2faInvalidCode">Invalid code entered</key>
</area>
<area alias="main">
<key alias="dashboard">Dashboard</key>
@@ -2219,6 +2225,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="sortCreateDateDescending">Oldest</key>
<key alias="sortLastLoginDateDescending">Last login</key>
<key alias="noUserGroupsAdded">No user groups have been added</key>
<key alias="2faDisableText">If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device:</key>
<key alias="2faProviderIsEnabled">This two-factor provider is enabled</key>
<key alias="2faProviderIsDisabledMsg">This two-factor provider is now disabled</key>
</area>
<area alias="validation">
<key alias="validation">Validation</key>

View File

@@ -587,11 +587,11 @@
<area alias="dictionaryItem">
<key alias="description"><![CDATA[
Edit the different language versions for the dictionary item '<em>%0%</em>' below
]]></key>
]]> </key>
<key alias="displayName">Culture Name</key>
<key alias="changeKeyError"><![CDATA[
The key '%0%' already exists.
]]></key>
]]> </key>
<key alias="overviewTitle">Dictionary overview</key>
</area>
<area alias="examineManagement">
@@ -862,6 +862,7 @@
<key alias="url">URL</key>
<key alias="user">User</key>
<key alias="username">Username</key>
<key alias="validate">Validate</key>
<key alias="value">Value</key>
<key alias="view">View</key>
<key alias="welcome">Welcome...</key>
@@ -933,7 +934,7 @@
<key alias="databaseHeader">Database configuration</key>
<key alias="databaseInstall"><![CDATA[
Press the <strong>install</strong> button to install the Umbraco %0% database
]]></key>
]]> </key>
<key alias="databaseInstallDone">
<![CDATA[Umbraco %0% has now been copied to your database. Press <strong>Next</strong> to proceed.]]></key>
<key alias="databaseNotFound"><![CDATA[<p>Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.</p>
@@ -951,7 +952,7 @@
<p>
Don't worry - no content will be deleted and everything will continue working afterwards!
</p>
]]></key>
]]> </key>
<key alias="databaseUpgradeDone"><![CDATA[Your database has been upgraded to the final version %0%.<br/>Press <strong>Next</strong> to
proceed. ]]></key>
<key alias="databaseUpToDate">
@@ -997,19 +998,19 @@
<key alias="permissionsText"><![CDATA[
Umbraco needs write/modify access to certain directories in order to store files like pictures and PDF's.
It also stores temporary data (aka: cache) for enhancing the performance of your website.
]]></key>
]]> </key>
<key alias="runwayFromScratch">I want to start from scratch</key>
<key alias="runwayFromScratchText"><![CDATA[
Your website is completely empty at the moment, so that's perfect if you want to start from scratch and create your own Document Types and templates.
(<a href="https://umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">learn how</a>)
You can still choose to install Runway later on. Please go to the Developer section and choose Packages.
]]></key>
]]> </key>
<key alias="runwayHeader">You've just set up a clean Umbraco platform. What do you want to do next?</key>
<key alias="runwayInstalled">Runway is installed</key>
<key alias="runwayInstalledText"><![CDATA[
You have the foundation in place. Select what modules you wish to install on top of it.<br />
This is our list of recommended modules, check off the ones you would like to install, or view the <a href="#" onclick="toggleModules(); return false;" id="toggleModuleList">full list of modules</a>
]]></key>
]]> </key>
<key alias="runwayOnlyProUsers">Only recommended for experienced users</key>
<key alias="runwaySimpleSite">I want to start with a simple website</key>
<key alias="runwaySimpleSiteText"><![CDATA[
@@ -1023,7 +1024,7 @@
<em>Included with Runway:</em> Home page, Getting Started page, Installing Modules page.<br />
<em>Optional Modules:</em> Top Navigation, Sitemap, Contact, Gallery.
</small>
]]></key>
]]> </key>
<key alias="runwayWhatIsRunway">What is Runway</key>
<key alias="step1">Step 1/5 Accept license</key>
<key alias="step2">Step 2/5: Database configuration</key>
@@ -1100,72 +1101,78 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<tr>
<td background="https://umbraco.com/umbraco/assets/img/application/logo.png" bgcolor="#1d1333" width="28" height="28" valign="top" style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<!--[if gte mso 9]> <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:30px;height:30px;"> <v:fill type="tile" src="https://umbraco.com/umbraco/assets/img/application/logo.png" color="#1d1333" /> <v:textbox inset="0,0,0,0"> <![endif]-->
<div> </div>
<!--[if gte mso 9]> </v:textbox> </v:rect> <![endif]-->
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0' class='body' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #1d1333;' bgcolor='#1d1333'>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'> </td>
<td class='container' style='font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 560px; width: 560px; margin: 0 auto; padding: 10px;' valign='top'>
<div class='content' style='box-sizing: border-box; display: block; max-width: 560px; margin: 0 auto; padding: 10px;'>
<br>
<table class='main' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px; background: #FFFFFF;' bgcolor='#FFFFFF'>
<tr>
<td class='wrapper' style='font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 50px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;'>
<tr>
<td style='line-height: 24px; font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'>
<h1 style='color: #392F54; font-family: sans-serif; font-weight: bold; line-height: 1.4; font-size: 24px; text-align: left; text-transform: capitalize; margin: 0 0 30px;' align='left'>
<div></div>
<!--[if gte mso 9]> </v:textbox> </v:rect> <![endif]-->
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0' class='body' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #1d1333;' bgcolor='#1d1333'>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'></td>
<td class='container' style='font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 560px; width: 560px; margin: 0 auto; padding: 10px;' valign='top'>
<div class='content' style='box-sizing: border-box; display: block; max-width: 560px; margin: 0 auto; padding: 10px;'>
<br>
<table class='main' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px; background: #FFFFFF;' bgcolor='#FFFFFF'>
<tr>
<td class='wrapper' style='font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 50px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;'>
<tr>
<td style='line-height: 24px; font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'>
<h1 style='color: #392F54; font-family: sans-serif; font-weight: bold; line-height: 1.4; font-size: 24px; text-align: left; text-transform: capitalize; margin: 0 0 30px;' align='left'>
Password reset requested
</h1>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
Your username to login to the Umbraco backoffice is: <strong>%0%</strong>
</p>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;'>
<tbody>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background: #35C786;' align='center' bgcolor='#35C786' valign='top'>
<a href='%1%' target='_blank' rel='noopener' style='color: #FFFFFF; text-decoration: none; -ms-word-break: break-all; word-break: break-all; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; text-transform: capitalize; background: #35C786; margin: 0; padding: 12px 30px; border: 1px solid #35c786;'>
</p>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;'>
<tbody>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background: #35C786;' align='center' bgcolor='#35C786' valign='top'>
<a href='%1%' target='_blank' rel='noopener' style='color: #FFFFFF; text-decoration: none; -ms-word-break: break-all; word-break: break-all; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; text-transform: capitalize; background: #35C786; margin: 0; padding: 12px 30px; border: 1px solid #35c786;'>
Click this link to reset your password
</a>
</td>
</tr>
</tbody>
</table>
</p>
<p style='max-width: 400px; display: block; color: #392F54; font-family: sans-serif; font-size: 14px; line-height: 20px; font-weight: normal; margin: 15px 0;'>If you cannot click on the link, copy and paste this URL into your browser window:</p>
<table border='0' cellpadding='0' cellspacing='0'>
<tr>
<td style='-ms-word-break: break-all; word-break: break-all; font-family: sans-serif; font-size: 11px; line-height:14px;'>
<font style="-ms-word-break: break-all; word-break: break-all; font-size: 11px; line-height:14px;">
<a style='-ms-word-break: break-all; word-break: break-all; color: #392F54; text-decoration: underline; font-size: 11px; line-height:15px;' href='%1%'>%1%</a>
</font>
</td>
</tr>
</table>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<br><br><br>
</div>
</td>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'> </td>
</tr>
</table>
</body>
</html>
]]></key>
</td>
</tr>
</tbody>
</table>
</p>
<p style='max-width: 400px; display: block; color: #392F54; font-family: sans-serif; font-size: 14px; line-height: 20px; font-weight: normal; margin: 15px 0;'>If you cannot click on the link, copy and paste this URL into your browser window:</p>
<table border='0' cellpadding='0' cellspacing='0'>
<tr>
<td style='-ms-word-break: break-all; word-break: break-all; font-family: sans-serif; font-size: 11px; line-height:14px;'>
<font style="-ms-word-break: break-all; word-break: break-all; font-size: 11px; line-height:14px;">
<a style='-ms-word-break: break-all; word-break: break-all; color: #392F54; text-decoration: underline; font-size: 11px; line-height:15px;' href='%1%'>%1%</a>
</font>
</td>
</tr>
</table>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<br><br><br>
</div>
</td>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'></td>
</tr>
</table>
</body>
</html>
]]> </key>
<key alias="2faTitle">One last step</key>
<key alias="2faText">You have enabled 2-factor authentication and must verify your identity.</key>
<key alias="2faMultipleText">Please choose a 2-factor provider</key>
<key alias="2faCodeInput">Verification code</key>
<key alias="2faCodeInputHelp">Please enter the verification code</key>
<key alias="2faInvalidCode">Invalid code entered</key>
</area>
<area alias="main">
<key alias="dashboard">Dashboard</key>
@@ -1205,7 +1212,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Have a nice day!
Cheers from the Umbraco robot
]]></key>
]]> </key>
<key alias="mailBodyVariantSummary">The following languages have been modified %0%</key>
<key alias="mailBodyHtml"><![CDATA[
<html>
@@ -1222,70 +1229,70 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<tr>
<td background="https://umbraco.com/umbraco/assets/img/application/logo.png" bgcolor="#1d1333" width="28" height="28" valign="top" style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<!--[if gte mso 9]> <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:30px;height:30px;"> <v:fill type="tile" src="https://umbraco.com/umbraco/assets/img/application/logo.png" color="#1d1333" /> <v:textbox inset="0,0,0,0"> <![endif]-->
<div> </div>
<!--[if gte mso 9]> </v:textbox> </v:rect> <![endif]-->
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0' class='body' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #1d1333;' bgcolor='#1d1333'>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'> </td>
<td class='container' style='font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 560px; width: 560px; margin: 0 auto; padding: 10px;' valign='top'>
<div class='content' style='box-sizing: border-box; display: block; max-width: 560px; margin: 0 auto; padding: 10px;'>
<br>
<table class='main' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px; background: #FFFFFF;' bgcolor='#FFFFFF'>
<tr>
<td class='wrapper' style='font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 50px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;'>
<tr>
<td style='line-height: 24px; font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'>
<h1 style='color: #392F54; font-family: sans-serif; font-weight: bold; line-height: 1.4; font-size: 24px; text-align: left; text-transform: capitalize; margin: 0 0 30px;' align='left'>
<div></div>
<!--[if gte mso 9]> </v:textbox> </v:rect> <![endif]-->
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0' class='body' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #1d1333;' bgcolor='#1d1333'>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'></td>
<td class='container' style='font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 560px; width: 560px; margin: 0 auto; padding: 10px;' valign='top'>
<div class='content' style='box-sizing: border-box; display: block; max-width: 560px; margin: 0 auto; padding: 10px;'>
<br>
<table class='main' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px; background: #FFFFFF;' bgcolor='#FFFFFF'>
<tr>
<td class='wrapper' style='font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 50px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;'>
<tr>
<td style='line-height: 24px; font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'>
<h1 style='color: #392F54; font-family: sans-serif; font-weight: bold; line-height: 1.4; font-size: 24px; text-align: left; text-transform: capitalize; margin: 0 0 30px;' align='left'>
Hi %0%,
</h1>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
This is an automated mail to inform you that the task <strong>'%1%'</strong> has been performed on the page <a style="color: #392F54; text-decoration: none; -ms-word-break: break-all; word-break: break-all;" href="http://%4%/#/content/content/edit/%5%"><strong>'%2%'</strong></a> by the user <strong>'%3%'</strong>
</p>
<table border='0' cellpadding='0' cellspacing='0' class='btn btn-primary' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;'>
<tbody>
<tr>
<td align='left' style='font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;'><tbody><tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background: #35C786;' align='center' bgcolor='#35C786' valign='top'>
<a href='http://%4%/#/content/content/edit/%5%' target='_blank' rel='noopener' style='color: #FFFFFF; text-decoration: none; -ms-word-break: break-all; word-break: break-all; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; text-transform: capitalize; background: #35C786; margin: 0; padding: 12px 30px; border: 1px solid #35c786;'>EDIT</a> </td> </tr></tbody></table>
</td>
</tr>
</tbody>
</table>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<h3>Update summary:</h3>
</p>
<table border='0' cellpadding='0' cellspacing='0' class='btn btn-primary' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;'>
<tbody>
<tr>
<td align='left' style='font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;'><tbody><tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background: #35C786;' align='center' bgcolor='#35C786' valign='top'>
<a href='http://%4%/#/content/content/edit/%5%' target='_blank' rel='noopener' style='color: #FFFFFF; text-decoration: none; -ms-word-break: break-all; word-break: break-all; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; text-transform: capitalize; background: #35C786; margin: 0; padding: 12px 30px; border: 1px solid #35c786;'>EDIT</a></td></tr></tbody></table>
</td>
</tr>
</tbody>
</table>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<h3>Update summary:</h3>
%6%
</p>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
Have a nice day!<br /><br />
Cheers from the Umbraco robot
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<br><br><br>
</div>
</td>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'> </td>
</tr>
</table>
</body>
</html>
]]></key>
</td>
</tr>
</table>
</td>
</tr>
</table>
<br><br><br>
</div>
</td>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'></td>
</tr>
</table>
</body>
</html>
]]> </key>
<key alias="mailBodyVariantHtmlSummary"><![CDATA[<p>The following languages have been modified:</p>
%0%
]]></key>
]]> </key>
<key alias="mailSubject">[%0%] Notification about %1% performed on %2%</key>
<key alias="notifications">Notifications</key>
</area>
@@ -1296,7 +1303,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="chooseLocalPackageText"><![CDATA[
Choose Package from your machine, by clicking the Browse<br />
button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension.
]]></key>
]]> </key>
<key alias="deletewarning">This will delete the package</key>
<key alias="includeAllChildNodes">Include all child nodes</key>
<key alias="installed">Installed</key>
@@ -1388,22 +1395,22 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="invalidPublishBranchPermissions">Insufficient user permissions to publish all descendant documents</key>
<key alias="contentPublishedFailedIsTrashed"><![CDATA[
%0% could not be published because the item is in the recycle bin.
]]></key>
]]> </key>
<key alias="contentPublishedFailedAwaitingRelease"><![CDATA[
%0% could not be published because the item is scheduled for release.
]]></key>
]]> </key>
<key alias="contentPublishedFailedExpired"><![CDATA[
%0% could not be published because the item has expired.
]]></key>
]]> </key>
<key alias="contentPublishedFailedInvalid"><![CDATA[
%0% could not be published because some properties did not pass validation rules.
]]></key>
]]> </key>
<key alias="contentPublishedFailedByEvent"><![CDATA[
%0% could not be published, a 3rd party add-in cancelled the action.
]]></key>
]]> </key>
<key alias="contentPublishedFailedByParent"><![CDATA[
%0% can not be published, because a parent page is not published.
]]></key>
]]> </key>
<key alias="contentPublishedFailedByMissingName">
<![CDATA[%0% can not be published, because its missing a name.]]></key>
<key alias="contentPublishedFailedReqCultureValidationError">Validation failed for required language '%0%'. This
@@ -1416,7 +1423,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="publishAll">Publish %0% and all its subpages</key>
<key alias="publishHelp"><![CDATA[Click <em>Publish</em> to publish <strong>%0%</strong> and thereby making its content publicly available.<br/><br />
You can publish this page and all its subpages by checking <em>Include unpublished subpages</em> below.
]]></key>
]]> </key>
</area>
<area alias="colorpicker">
<key alias="noColors">You have not configured any approved colors</key>
@@ -1694,23 +1701,23 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="renderBodyDesc"><![CDATA[
Renders the contents of a child template, by inserting a
<code>@RenderBody()</code> placeholder.
]]></key>
]]> </key>
<key alias="defineSection">Define a named section</key>
<key alias="defineSectionDesc"><![CDATA[
Defines a part of your template as a named section by wrapping it in
<code>@section { ... }</code>. This can be rendered in a
specific area of the parent of this template, by using <code>@RenderSection</code>.
]]></key>
]]> </key>
<key alias="renderSection">Render a named section</key>
<key alias="renderSectionDesc"><![CDATA[
Renders a named area of a child template, by inserting a <code>@RenderSection(name)</code> placeholder.
This renders an area of a child template which is wrapped in a corresponding <code>@section [name]{ ... }</code> definition.
]]></key>
]]> </key>
<key alias="sectionName">Section Name</key>
<key alias="sectionMandatory">Section is mandatory</key>
<key alias="sectionMandatoryDesc"><![CDATA[
If mandatory, the child template must contain a <code>@section</code> definition, otherwise an error is shown.
]]></key>
]]> </key>
<key alias="queryBuilder">Query builder</key>
<key alias="itemsReturned">items returned, in</key>
<key alias="iWant">I want</key>
@@ -2002,7 +2009,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Have a nice day!
Cheers from the Umbraco robot
]]></key>
]]> </key>
<key alias="noTranslators">No translator users found. Please create a translator user before you start sending
content to translation
</key>
@@ -2178,6 +2185,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
you. Click the circle above to upload your photo.
</key>
<key alias="writer">Writer</key>
<key alias="configureTwoFactor">Configure Two-Factor</key>
<key alias="change">Change</key>
<key alias="yourProfile" version="7.0">Your profile</key>
<key alias="yourHistory" version="7.0">Your recent history</key>
@@ -2202,82 +2210,82 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<tr>
<td background="https://umbraco.com/umbraco/assets/img/application/logo.png" bgcolor="#1d1333" width="28" height="28" valign="top" style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<!--[if gte mso 9]> <v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:30px;height:30px;"> <v:fill type="tile" src="https://umbraco.com/umbraco/assets/img/application/logo.png" color="#1d1333" /> <v:textbox inset="0,0,0,0"> <![endif]-->
<div> </div>
<!--[if gte mso 9]> </v:textbox> </v:rect> <![endif]-->
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0' class='body' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #1d1333;' bgcolor='#1d1333'>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'> </td>
<td class='container' style='font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 560px; width: 560px; margin: 0 auto; padding: 10px;' valign='top'>
<div class='content' style='box-sizing: border-box; display: block; max-width: 560px; margin: 0 auto; padding: 10px;'>
<br>
<table class='main' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px; background: #FFFFFF;' bgcolor='#FFFFFF'>
<tr>
<td class='wrapper' style='font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 50px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;'>
<tr>
<td style='line-height: 24px; font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'>
<h1 style='color: #392F54; font-family: sans-serif; font-weight: bold; line-height: 1.4; font-size: 24px; text-align: left; text-transform: capitalize; margin: 0 0 30px;' align='left'>
<div></div>
<!--[if gte mso 9]> </v:textbox> </v:rect> <![endif]-->
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</td>
</tr>
</table>
<table border='0' cellpadding='0' cellspacing='0' class='body' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #1d1333;' bgcolor='#1d1333'>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'></td>
<td class='container' style='font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 560px; width: 560px; margin: 0 auto; padding: 10px;' valign='top'>
<div class='content' style='box-sizing: border-box; display: block; max-width: 560px; margin: 0 auto; padding: 10px;'>
<br>
<table class='main' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; border-radius: 3px; background: #FFFFFF;' bgcolor='#FFFFFF'>
<tr>
<td class='wrapper' style='font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 50px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;'>
<tr>
<td style='line-height: 24px; font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'>
<h1 style='color: #392F54; font-family: sans-serif; font-weight: bold; line-height: 1.4; font-size: 24px; text-align: left; text-transform: capitalize; margin: 0 0 30px;' align='left'>
Hi %0%,
</h1>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
You have been invited by <a href="mailto:%4%" style="text-decoration: underline; color: #392F54; -ms-word-break: break-all; word-break: break-all;">%1%</a> to the Umbraco Back Office.
</p>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
<p style='color: #392F54; font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0 0 15px;'>
Message from <a href="mailto:%1%" style="text-decoration: none; color: #392F54; -ms-word-break: break-all; word-break: break-all;">%1%</a>:
<br/>
<em>%2%</em>
</p>
<table border='0' cellpadding='0' cellspacing='0' class='btn btn-primary' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;'>
<tbody>
<tr>
<td align='left' style='font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;'>
<tbody>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background: #35C786;' align='center' bgcolor='#35C786' valign='top'>
<a href='%3%' target='_blank' rel='noopener' style='color: #FFFFFF; text-decoration: none; -ms-word-break: break-all; word-break: break-all; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; text-transform: capitalize; background: #35C786; margin: 0; padding: 12px 30px; border: 1px solid #35c786;'>
<em>%2%</em>
</p>
<table border='0' cellpadding='0' cellspacing='0' class='btn btn-primary' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;'>
<tbody>
<tr>
<td align='left' style='font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;' valign='top'>
<table border='0' cellpadding='0' cellspacing='0' style='border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;'>
<tbody>
<tr>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background: #35C786;' align='center' bgcolor='#35C786' valign='top'>
<a href='%3%' target='_blank' rel='noopener' style='color: #FFFFFF; text-decoration: none; -ms-word-break: break-all; word-break: break-all; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; text-transform: capitalize; background: #35C786; margin: 0; padding: 12px 30px; border: 1px solid #35c786;'>
Click this link to accept the invite
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style='max-width: 400px; display: block; color: #392F54; font-family: sans-serif; font-size: 14px; line-height: 20px; font-weight: normal; margin: 15px 0;'>If you cannot click on the link, copy and paste this URL into your browser window:</p>
<table border='0' cellpadding='0' cellspacing='0'>
<tr>
<td style='-ms-word-break: break-all; word-break: break-all; font-family: sans-serif; font-size: 11px; line-height:14px;'>
<font style="-ms-word-break: break-all; word-break: break-all; font-size: 11px; line-height:14px;">
<a style='-ms-word-break: break-all; word-break: break-all; color: #392F54; text-decoration: underline; font-size: 11px; line-height:15px;' href='%3%'>%3%</a>
</font>
</td>
</tr>
</table>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<br><br><br>
</div>
</td>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'> </td>
</tr>
</table>
</body>
</html>]]></key>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style='max-width: 400px; display: block; color: #392F54; font-family: sans-serif; font-size: 14px; line-height: 20px; font-weight: normal; margin: 15px 0;'>If you cannot click on the link, copy and paste this URL into your browser window:</p>
<table border='0' cellpadding='0' cellspacing='0'>
<tr>
<td style='-ms-word-break: break-all; word-break: break-all; font-family: sans-serif; font-size: 11px; line-height:14px;'>
<font style="-ms-word-break: break-all; word-break: break-all; font-size: 11px; line-height:14px;">
<a style='-ms-word-break: break-all; word-break: break-all; color: #392F54; text-decoration: underline; font-size: 11px; line-height:15px;' href='%3%'>%3%</a>
</font>
</td>
</tr>
</table>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<br><br><br>
</div>
</td>
<td style='font-family: sans-serif; font-size: 14px; vertical-align: top;' valign='top'></td>
</tr>
</table>
</body>
</html>]]></key>
<key alias="defaultInvitationMessage">Resending invitation...</key>
<key alias="deleteUser">Delete User</key>
<key alias="deleteUserConfirmation">Are you sure you wish to delete this user account?</key>
@@ -2293,6 +2301,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="sortCreateDateDescending">Oldest</key>
<key alias="sortLastLoginDateDescending">Last login</key>
<key alias="noUserGroupsAdded">No user groups have been added</key>
<key alias="2faDisableText">If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device:</key>
<key alias="2faProviderIsEnabled">This two-factor provider is enabled</key>
<key alias="2faProviderIsDisabledMsg">This two-factor provider is now disabled</key>
</area>
<area alias="validation">
<key alias="validation">Validation</key>

View File

@@ -2,9 +2,9 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Web;
@@ -122,8 +122,9 @@ namespace Umbraco.Cms.Web.Website.ActionResults
}
HttpContext httpContext = context.HttpContext;
IIOHelper ioHelper = httpContext.RequestServices.GetRequiredService<IIOHelper>();
string destinationUrl = ioHelper.ResolveUrl(Url);
IUrlHelperFactory urlHelperFactory = httpContext.RequestServices.GetRequiredService<IUrlHelperFactory>();
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(context);
string destinationUrl = urlHelper.Content(Url);
if (_queryString.HasValue)
{
@@ -134,6 +135,5 @@ namespace Umbraco.Cms.Web.Website.ActionResults
return Task.CompletedTask;
}
}
}

View File

@@ -93,7 +93,7 @@ function runBackOfficeIntroTour(percentageComplete, buttonText, timeout) {
cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('9/13');
cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click();
cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('10/13');
cy.get('.umb-overlay-drawer__align-right .umb-button').should('be.visible').click();
cy.get('[data-element~="overlay-user"] [data-element="button-overlayClose"]').should('be.visible').click();
cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('11/13');
cy.umbracoGlobalHelp().click()

View File

@@ -26,6 +26,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
private IExternalLoginWithKeyService ExternalLoginService => GetRequiredService<IExternalLoginWithKeyService>();
private IUmbracoMapper UmbracoMapper => GetRequiredService<IUmbracoMapper>();
private ILocalizedTextService TextService => GetRequiredService<ILocalizedTextService>();
private ITwoFactorLoginService TwoFactorLoginService => GetRequiredService<ITwoFactorLoginService>();
private BackOfficeUserStore GetUserStore()
=> new BackOfficeUserStore(
@@ -36,7 +37,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
Options.Create(GlobalSettings),
UmbracoMapper,
new BackOfficeErrorDescriber(TextService),
AppCaches);
AppCaches,
TwoFactorLoginService
);
[Test]
public async Task Can_Persist_Is_Approved()