Simplified setup of 2FA for users (#12142)
* Added functionality to enable 2FA for users.. * Do not use the obsolete ctor in tests * cleanup * Cleanup * Convert User view from overlay to infinite editor * Add support for having additional editors on top of the user (2fa) which overlay does not support * Add controllerAs syntax in the template * Remove unused dependencies * Adjustments to 2fa login view * organize elements * add translations * add a11y helpers * add autocompletion = one-time-code * change to controllerAs syntax * add callback to cancel 2fa and fix error where submit button was not reset when all other validations were * add a cancel/go back button to the 2fa view * replace header with something less obstrusive * move logout button to the footer in the new editor view * change 'edit profile' to an umb-box and move ng-if for password fields out to reduce amount of checks * Add umb-box to external login provider section * add umb-box to user history section * bug: fix bug where notificationsService would not allow new notifications if removeAll had been called * add styling and a11y to configureTwoFactor view - also ensure that the view reloads when changes happen in the custom user view to enable 2fa - ensure that view updates when disabling 2fa - add extra button to show options (disable) for each 2fa provider * add notification when 2fa is disabled * add data-element to support the intro tour also changed a minor selector in the cypress test * correct usage of umb-box with umb-box-content * do not use the .form class twice to prevent double box-shadow * make tranlastion for 2fa placeholder shorter * ensure that field with 2fa provider is always visible when more than 1 provider * move error state of 2fa field to token field * update translation of multiple 2fa providers * move CTA buttons to right side to follow general UI practices * rename options to disable * add disabled state * add helper folders to gitignore so you can work with plugins and custom code without committing it accidentally * move the disable functionality to its own infinite editor view * use properties from umb-control-group correctly * add 'track by' to repeater * make use of umb-control-group * remove unused functions * clean up translations * add Danish translations * copy translations to english * Only return enabled 2fa providers as expected Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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/
|
||||
|
||||
# Tests
|
||||
cypress.env.json
|
||||
|
||||
20
src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs
Normal file
20
src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
})();
|
||||
@@ -273,7 +273,7 @@ angular.module('umbraco.services')
|
||||
*/
|
||||
removeAll: function () {
|
||||
angularHelper.safeApply($rootScope, function() {
|
||||
nArray = [];
|
||||
nArray.length = 0;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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> {{login.caption}} <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> {{login.caption}}
|
||||
<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>
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
44
src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html
Normal file
44
src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html
Normal 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>
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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> {{login.caption}} <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> {{login.caption}} <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>
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
var vm = this;
|
||||
|
||||
vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin();
|
||||
|
||||
}
|
||||
|
||||
angular.module("umbraco").controller("Umbraco.Editors.Users.DetailsController", DetailsController);
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user