Added is dirty properties and updated to reflect linter update
This commit is contained in:
@@ -1,77 +1,179 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Core.Members
|
||||
{
|
||||
/// <summary>
|
||||
/// An Umbraco member user type
|
||||
/// TODO: use of identity classes in future
|
||||
/// </summary>
|
||||
public class UmbracoMembersIdentityUser
|
||||
//: IRememberBeingDirty
|
||||
//TODO: use of identity classes
|
||||
//: IdentityUser<int, IIdentityUserLogin, IdentityUserRole<string>, IdentityUserClaim<int>>,
|
||||
public class UmbracoMembersIdentityUser : IRememberBeingDirty
|
||||
{
|
||||
private bool _hasIdentity;
|
||||
private int _id;
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string MemberTypeAlias { get; set; }
|
||||
public bool IsLockedOut { get; set; }
|
||||
public string RawPasswordValue { get; set; }
|
||||
private string _passwordHash;
|
||||
|
||||
private string _passwordConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if an Id has been set on this object
|
||||
/// Gets or sets the member name
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the member email
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the member username
|
||||
/// </summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alias of the member type
|
||||
/// </summary>
|
||||
public string MemberTypeAlias { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the member is locked out
|
||||
/// </summary>
|
||||
public bool IsLockedOut { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether an Id has been set on this object
|
||||
/// This will be false if the object is new and not persisted to the database
|
||||
/// </summary>
|
||||
public bool HasIdentity => _hasIdentity;
|
||||
public bool HasIdentity { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the member Id
|
||||
/// </summary>
|
||||
public int Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
{
|
||||
_id = value;
|
||||
_hasIdentity = true;
|
||||
HasIdentity = true;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: track
|
||||
public string PasswordHash { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the salted/hashed form of the user password
|
||||
/// </summary>
|
||||
public string PasswordHash
|
||||
{
|
||||
get => _passwordHash;
|
||||
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash));
|
||||
}
|
||||
|
||||
//TODO: config
|
||||
public string PasswordConfig { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the password config
|
||||
/// </summary>
|
||||
public string PasswordConfig
|
||||
{
|
||||
get => _passwordConfig;
|
||||
set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
|
||||
}
|
||||
|
||||
internal bool IsApproved;
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether member Is Approved
|
||||
/// </summary>
|
||||
public bool IsApproved { get; set; }
|
||||
|
||||
//TODO: needed?
|
||||
/// <summary>
|
||||
/// Gets the <see cref="BeingDirty"/> for change tracking
|
||||
/// </summary>
|
||||
protected BeingDirty BeingDirty { get; } = new BeingDirty();
|
||||
|
||||
// TODO: implement as per base identity user
|
||||
//public bool LoginsChanged;
|
||||
//public bool RolesChanged;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new identity member
|
||||
/// </summary>
|
||||
/// <param name="username">The member username</param>
|
||||
/// <param name="email">The member email</param>
|
||||
/// <param name="memberTypeAlias">The member type alias</param>
|
||||
/// <param name="name">The member name</param>
|
||||
/// TODO: confirm <param name="password">The password (may be null in some instances)</param>
|
||||
/// <exception cref="ArgumentException">Throws is username is null or whitespace</exception>
|
||||
/// <returns>The identity member user</returns>
|
||||
public static UmbracoMembersIdentityUser CreateNew(
|
||||
string username,
|
||||
string email,
|
||||
string memberTypeAlias,
|
||||
string name)
|
||||
string name,
|
||||
string password = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
|
||||
}
|
||||
|
||||
//no groups/roles yet
|
||||
// no groups/roles yet
|
||||
var member = new UmbracoMembersIdentityUser
|
||||
{
|
||||
UserName = username,
|
||||
Email = email,
|
||||
Name = name,
|
||||
MemberTypeAlias = memberTypeAlias,
|
||||
Id = 0, //TODO: is this meant to be 0 in this circumstance?
|
||||
//false by default unless specifically set
|
||||
_hasIdentity = false
|
||||
Id = 0, // TODO: is this meant to be 0 in this circumstance?
|
||||
// false by default unless specifically set
|
||||
HasIdentity = false
|
||||
};
|
||||
|
||||
//TODO: do we use this?
|
||||
//member.EnableChangeTracking();
|
||||
member.EnableChangeTracking();
|
||||
return member;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add => BeingDirty.PropertyChanged += value;
|
||||
|
||||
remove => BeingDirty.PropertyChanged -= value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
public bool IsDirty() => BeingDirty.IsDirty();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPropertyDirty(string propName) => BeingDirty.IsPropertyDirty(propName);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetDirtyProperties() => BeingDirty.GetDirtyProperties();
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
public void ResetDirtyProperties() => BeingDirty.ResetDirtyProperties();
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
public void DisableChangeTracking() => BeingDirty.DisableChangeTracking();
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
public void EnableChangeTracking() => BeingDirty.EnableChangeTracking();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool WasDirty() => BeingDirty.WasDirty();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool WasPropertyDirty(string propertyName) => BeingDirty.WasPropertyDirty(propertyName);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ResetWereDirtyProperties() => BeingDirty.ResetWereDirtyProperties();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ResetDirtyProperties(bool rememberDirty) => BeingDirty.ResetDirtyProperties(rememberDirty);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetWereDirtyProperties() => BeingDirty.GetWereDirtyProperties();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Core.Members;
|
||||
@@ -12,11 +13,10 @@ namespace Umbraco.Infrastructure.Members
|
||||
public interface IUmbracoMembersUserManager<TUser> : IDisposable where TUser : UmbracoMembersIdentityUser
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the specified <paramref name="memberUser"/> in the backing store with a password,
|
||||
/// as an asynchronous operation.
|
||||
/// Creates the specified <paramref name="memberUser"/> user in the backing store with given password, as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="memberUser">The member to create.</param>
|
||||
/// <param name="password">The password to add</param>
|
||||
/// <param name="password">The new password</param>
|
||||
/// <returns>
|
||||
/// The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/>
|
||||
/// of the operation.
|
||||
@@ -26,7 +26,7 @@ namespace Umbraco.Infrastructure.Members
|
||||
/// <summary>
|
||||
/// Helper method to generate a password for a user based on the current password validator
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>Returns the generated password</returns>
|
||||
string GeneratePassword();
|
||||
|
||||
/// <summary>
|
||||
@@ -51,5 +51,12 @@ namespace Umbraco.Infrastructure.Members
|
||||
/// the specified <paramref name="password" /> matches the one store for the <paramref name="memberUser"/>,
|
||||
/// otherwise false.</returns>
|
||||
Task<bool> CheckPasswordAsync(TUser memberUser, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Method to validate the password without an identity user
|
||||
/// </summary>
|
||||
/// <param name="password">The password to validate</param>
|
||||
/// <returns>The result of the validation</returns>
|
||||
Task<List<IdentityResult>> ValidatePassword(string password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.Members;
|
||||
using Umbraco.Core.Security;
|
||||
using System.Threading;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
|
||||
|
||||
namespace Umbraco.Infrastructure.Members
|
||||
@@ -18,6 +18,7 @@ namespace Umbraco.Infrastructure.Members
|
||||
/// </summary>
|
||||
public class UmbracoMembersUserManager : UmbracoMembersUserManager<UmbracoMembersIdentityUser>, IUmbracoMembersUserManager
|
||||
{
|
||||
///<inheritdoc />
|
||||
public UmbracoMembersUserManager(
|
||||
IUserStore<UmbracoMembersIdentityUser> store,
|
||||
IOptions<UmbracoMembersIdentityOptions> optionsAccessor,
|
||||
@@ -34,13 +35,28 @@ namespace Umbraco.Infrastructure.Members
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manager for the member identity user
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The identity user</typeparam>
|
||||
public class UmbracoMembersUserManager<T> : UserManager<T>
|
||||
where T : UmbracoMembersIdentityUser
|
||||
{
|
||||
public IPasswordConfiguration PasswordConfiguration { get; protected set; }
|
||||
|
||||
private PasswordGenerator _passwordGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoMembersUserManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="store">The members store</param>
|
||||
/// <param name="optionsAccessor">The identity options accessor</param>
|
||||
/// <param name="passwordHasher">The password hasher</param>
|
||||
/// <param name="userValidators">The user validators</param>
|
||||
/// <param name="passwordValidators">The password validators</param>
|
||||
/// <param name="keyNormalizer">The keep lookup normalizer</param>
|
||||
/// <param name="errors">The error display messages</param>
|
||||
/// <param name="services">The service provider</param>
|
||||
/// <param name="logger">The logger</param>
|
||||
/// <param name="passwordConfiguration">The password configuration</param>
|
||||
public UmbracoMembersUserManager(
|
||||
IUserStore<T> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
@@ -51,14 +67,17 @@ namespace Umbraco.Infrastructure.Members
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<T>> logger,
|
||||
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration) :
|
||||
base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
||||
{
|
||||
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) =>
|
||||
PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Replace the underlying options property with our own strongly typed version
|
||||
/// Gets or sets the password configuration
|
||||
/// </summary>
|
||||
public IPasswordConfiguration PasswordConfiguration { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// gets or sets the underlying options property with our own strongly typed version
|
||||
/// </summary>
|
||||
public new UmbracoMembersIdentityOptions Options
|
||||
{
|
||||
@@ -67,24 +86,24 @@ namespace Umbraco.Infrastructure.Members
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets/sets the default Umbraco member user password checker
|
||||
/// Gets or sets the default Umbraco member user password checker
|
||||
/// </summary>
|
||||
public IUmbracoMembersUserPasswordChecker UmbracoMembersUserPasswordChecker { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO: from BackOfficeUserManager duplicated, could be shared]
|
||||
/// TODO: from BackOfficeUserManager duplicated, could be shared
|
||||
/// Override to determine how to hash the password
|
||||
/// </summary>
|
||||
/// <param name="memberUser"></param>
|
||||
/// <param name="newPassword"></param>
|
||||
/// <param name="validatePassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="memberUser">The member to validate</param>
|
||||
/// <param name="newPassword">The new password</param>
|
||||
/// <param name="validatePassword">Whether to validate the password</param>
|
||||
/// <returns>The identity result of updating the password hash</returns>
|
||||
/// <remarks>
|
||||
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
|
||||
/// </remarks>
|
||||
protected override async Task<IdentityResult> UpdatePasswordHash(T memberUser, string newPassword, bool validatePassword)
|
||||
{
|
||||
//memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow;
|
||||
// memberUser.LastPasswordChangeDateUtc = DateTime.UtcNow;
|
||||
|
||||
if (validatePassword)
|
||||
{
|
||||
@@ -95,8 +114,10 @@ namespace Umbraco.Infrastructure.Members
|
||||
}
|
||||
}
|
||||
|
||||
var passwordStore = Store as IUserPasswordStore<T>;
|
||||
if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
|
||||
if (!(Store is IUserPasswordStore<T> passwordStore))
|
||||
{
|
||||
throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
|
||||
}
|
||||
|
||||
var hash = newPassword != null ? PasswordHasher.HashPassword(memberUser, newPassword) : null;
|
||||
await passwordStore.SetPasswordHashAsync(memberUser, hash, CancellationToken);
|
||||
@@ -104,13 +125,13 @@ namespace Umbraco.Infrastructure.Members
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
///TODO: duplicated code from backofficeusermanager, could be shared?
|
||||
/// TODO: duplicated code from backofficeusermanager, could be shared
|
||||
/// <summary>
|
||||
/// Logic used to validate a username and password
|
||||
/// </summary>
|
||||
/// <param name="member"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="member">The member to validate</param>
|
||||
/// <param name="password">The password to validate</param>
|
||||
/// <returns>Whether the password is the correct password for this member</returns>
|
||||
/// <remarks>
|
||||
/// By default this uses the standard ASP.Net Identity approach which is:
|
||||
/// * Get password store
|
||||
@@ -136,68 +157,88 @@ namespace Umbraco.Infrastructure.Members
|
||||
return false;
|
||||
}
|
||||
|
||||
//if the result indicates to not fallback to the default, then return true if the credentials are valid
|
||||
// if the result indicates to not fallback to the default, then return true if the credentials are valid
|
||||
if (result != UmbracoMembersUserPasswordCheckerResult.FallbackToDefaultChecker)
|
||||
{
|
||||
return result == UmbracoMembersUserPasswordCheckerResult.ValidCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
//we cannot proceed if the user passed in does not have an identity
|
||||
// we cannot proceed if the user passed in does not have an identity
|
||||
if (member.HasIdentity == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//use the default behavior
|
||||
// use the default behavior
|
||||
return await base.CheckPasswordAsync(member, password);
|
||||
}
|
||||
|
||||
///[TODO: from BackOfficeUserManager duplicated, could be shared]
|
||||
/// TODO: from BackOfficeUserManager duplicated, could be shared
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="user">The user to update the security stamp for</param>
|
||||
/// <returns>Task returns</returns>
|
||||
private async Task UpdateSecurityStampInternal(T user)
|
||||
{
|
||||
if (SupportsUserSecurityStamp == false) return;
|
||||
if (SupportsUserSecurityStamp == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None);
|
||||
}
|
||||
|
||||
///[TODO: from BackOfficeUserManager duplicated, could be shared]
|
||||
/// TODO: from BackOfficeUserManager duplicated, could be shared
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>Return a user security stamp</returns>
|
||||
private IUserSecurityStampStore<T> GetSecurityStore()
|
||||
{
|
||||
var store = Store as IUserSecurityStampStore<T>;
|
||||
if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
|
||||
if (!(Store is IUserSecurityStampStore<T> store))
|
||||
{
|
||||
throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
///[TODO: from BackOfficeUserManager duplicated, could be shared]
|
||||
/// TODO: from BackOfficeUserManager duplicated, could be shared
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static string NewSecurityStamp()
|
||||
{
|
||||
return Guid.NewGuid().ToString();
|
||||
}
|
||||
/// <returns>Returns a new security stamp</returns>
|
||||
private static string NewSecurityStamp() => Guid.NewGuid().ToString();
|
||||
|
||||
///[TODO: from BackOfficeUserManager duplicated, could be shared]
|
||||
/// <summary>
|
||||
/// TODO: from BackOfficeUserManager duplicated, could be shared
|
||||
/// Helper method to generate a password for a member based on the current password validator
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>The generated password</returns>
|
||||
public string GeneratePassword()
|
||||
{
|
||||
if (_passwordGenerator == null)
|
||||
{
|
||||
_passwordGenerator = new PasswordGenerator(PasswordConfiguration);
|
||||
}
|
||||
_passwordGenerator ??= new PasswordGenerator(PasswordConfiguration);
|
||||
string password = _passwordGenerator.GeneratePassword();
|
||||
return password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to validate a password based on the current password validator
|
||||
/// </summary>
|
||||
/// <param name="password">The password to update</param>
|
||||
/// <returns>The validated password</returns>
|
||||
public async Task<List<IdentityResult>> ValidatePassword(string password)
|
||||
{
|
||||
var passwordValidators = new List<IdentityResult>();
|
||||
foreach(IPasswordValidator<T> validator in PasswordValidators)
|
||||
{
|
||||
IdentityResult result = await validator.ValidateAsync(this, null, password);
|
||||
passwordValidators.Add(result);
|
||||
}
|
||||
|
||||
return passwordValidators;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.BackOffice;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Members;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Infrastructure.Members
|
||||
@@ -25,40 +28,57 @@ namespace Umbraco.Infrastructure.Members
|
||||
//IUserTwoFactorStore<UmbracoMembersIdentityUser>
|
||||
//IUserSessionStore<UmbracoMembersIdentityUser>
|
||||
{
|
||||
private bool _disposed = false;
|
||||
private readonly bool _disposed = false;
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly UmbracoMapper _mapper;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
|
||||
public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoMembersUserStore"/> class for the members identity store
|
||||
/// </summary>
|
||||
/// <param name="memberService">The member service</param>
|
||||
/// <param name="mapper">The mapper for properties</param>
|
||||
/// <param name="scopeProvider">The scope provider</param>
|
||||
public UmbracoMembersUserStore(IMemberService memberService, UmbracoMapper mapper, IScopeProvider scopeProvider)
|
||||
{
|
||||
_memberService = memberService;
|
||||
_mapper = mapper;
|
||||
_scopeProvider = scopeProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the member as an identity user
|
||||
/// </summary>
|
||||
/// <param name="user">The identity user` for a member</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>The identity result</returns>
|
||||
public Task<IdentityResult> CreateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
//create member
|
||||
//TODO: are we keeping this method, e.g. the Member Service? The user service creates it directly, but this gets the membertype
|
||||
// create member
|
||||
// TODO: are we keeping this method, e.g. the Member Service?
|
||||
// The user service creates it directly, but this way we get the member type by alias first
|
||||
IMember member = _memberService.CreateMember(
|
||||
user.UserName,
|
||||
user.Email,
|
||||
user.Name.IsNullOrWhiteSpace() ? user.UserName : user.Name,
|
||||
user.MemberTypeAlias.IsNullOrWhiteSpace() ?
|
||||
Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias);
|
||||
user.MemberTypeAlias.IsNullOrWhiteSpace() ? Constants.Security.DefaultMemberTypeAlias : user.MemberTypeAlias);
|
||||
|
||||
UpdateMemberProperties(member, user);
|
||||
|
||||
//TODO: do we want to accept empty passwords here - if third-party for example? In other method if so?
|
||||
// TODO: do we want to accept empty passwords here - if third-party for example?
|
||||
// In other method if so?
|
||||
_memberService.Save(member);
|
||||
|
||||
//re-assign id
|
||||
// re-assign id
|
||||
user.Id = member.Id;
|
||||
|
||||
// TODO: do we need this?
|
||||
// TODO: [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
//bool isLoginsPropertyDirty = member.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Logins));
|
||||
|
||||
@@ -72,18 +92,21 @@ namespace Umbraco.Infrastructure.Members
|
||||
// x.UserData)));
|
||||
//}
|
||||
|
||||
if (!member.HasIdentity) throw new DataException("Could not create the user, check logs for details");
|
||||
if (!member.HasIdentity)
|
||||
{
|
||||
throw new DataException("Could not create the member, check logs for details");
|
||||
}
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
|
||||
//TODO: confirm
|
||||
// TODO: confirm and implement
|
||||
//if (memberUser.LoginsChanged)
|
||||
//{
|
||||
// var logins = await GetLoginsAsync(memberUser);
|
||||
// _externalLoginStore.SaveUserLogins(member.Id, logins);
|
||||
//}
|
||||
|
||||
//TODO: confirm
|
||||
// TODO: confirm and implement
|
||||
//if (memberUser.RolesChanged)
|
||||
//{
|
||||
//IMembershipRoleService<IMember> memberRoleService = _memberService;
|
||||
@@ -102,18 +125,17 @@ namespace Umbraco.Infrastructure.Members
|
||||
|
||||
private bool UpdateMemberProperties(IMember member, UmbracoMembersIdentityUser memberIdentityUser)
|
||||
{
|
||||
//[Comments as per BackOfficeUserStore & identity package]
|
||||
var anythingChanged = false;
|
||||
//don't assign anything if nothing has changed as this will trigger the track changes of the model
|
||||
if (
|
||||
//memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Name)) &&
|
||||
|
||||
// don't assign anything if nothing has changed as this will trigger the track changes of the model
|
||||
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Name)) &&
|
||||
member.Name != memberIdentityUser.Name && memberIdentityUser.Name.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.Name = memberIdentityUser.Name;
|
||||
}
|
||||
if (
|
||||
//memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Email)) &&
|
||||
|
||||
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Email)) &&
|
||||
member.Email != memberIdentityUser.Email && memberIdentityUser.Email.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
@@ -127,22 +149,20 @@ namespace Umbraco.Infrastructure.Members
|
||||
|
||||
if (member.IsLockedOut)
|
||||
{
|
||||
//need to set the last lockout date
|
||||
// need to set the last lockout date
|
||||
member.LastLockoutDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
if (
|
||||
//memberIdentityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.UserName)) &&
|
||||
|
||||
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.UserName)) &&
|
||||
member.Username != memberIdentityUser.UserName && memberIdentityUser.UserName.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.Username = memberIdentityUser.UserName;
|
||||
}
|
||||
|
||||
if (
|
||||
//member.IsPropertyDirty(nameof(BackOfficeIdentityUser.PasswordHash))&&
|
||||
member.RawPasswordValue != memberIdentityUser.PasswordHash
|
||||
&& memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false)
|
||||
if (memberIdentityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.PasswordHash))
|
||||
&& member.RawPasswordValue != memberIdentityUser.PasswordHash && memberIdentityUser.PasswordHash.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
anythingChanged = true;
|
||||
member.RawPasswordValue = memberIdentityUser.PasswordHash;
|
||||
@@ -151,7 +171,7 @@ namespace Umbraco.Infrastructure.Members
|
||||
|
||||
// TODO: Roles
|
||||
// [Comment] Same comment as per BackOfficeUserStore: Fix this for Groups too
|
||||
//if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
|
||||
//if (identityUser.IsPropertyDirty(nameof(UmbracoMembersIdentityUser.Roles)) || identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.Groups)))
|
||||
//{
|
||||
// var userGroupAliases = member.Groups.Select(x => x.Alias).ToArray();
|
||||
|
||||
@@ -182,9 +202,7 @@ namespace Umbraco.Infrastructure.Members
|
||||
// }
|
||||
//}
|
||||
|
||||
//TODO: reset all changes
|
||||
//memberIdentityUser.ResetDirtyProperties(false);
|
||||
|
||||
memberIdentityUser.ResetDirtyProperties(false);
|
||||
return anythingChanged;
|
||||
}
|
||||
|
||||
@@ -200,16 +218,17 @@ namespace Umbraco.Infrastructure.Members
|
||||
|
||||
public async Task<UmbracoMembersIdentityUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
|
||||
{
|
||||
//TODO: confirm logic
|
||||
// TODO: confirm logic
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
var member = _memberService.GetByUsername(normalizedUserName);
|
||||
|
||||
IMember member = _memberService.GetByUsername(normalizedUserName);
|
||||
if (member == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = _mapper.Map<UmbracoMembersIdentityUser>(member);
|
||||
UmbracoMembersIdentityUser result = _mapper.Map<UmbracoMembersIdentityUser>(member);
|
||||
|
||||
return await Task.FromResult(result);
|
||||
}
|
||||
@@ -223,64 +242,147 @@ namespace Umbraco.Infrastructure.Members
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
return Task.FromResult(user.Id.ToString());
|
||||
}
|
||||
|
||||
public Task<string> GetUserNameAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
//TODO: unit tests for and implement all bodies
|
||||
// TODO: unit tests for and implement all bodies
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
return Task.FromResult(user.UserName);
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken)
|
||||
{
|
||||
return SetUserNameAsync(user, normalizedName, cancellationToken); throw new NotImplementedException();
|
||||
}
|
||||
/// <summary>
|
||||
/// Sets the normalized user name
|
||||
/// </summary>
|
||||
/// <param name="user">The member identity user</param>
|
||||
/// <param name="normalizedName">The normalized member name</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>A task once complete</returns>
|
||||
public Task SetNormalizedUserNameAsync(UmbracoMembersIdentityUser user, string normalizedName, CancellationToken cancellationToken) => SetUserNameAsync(user, normalizedName, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user name as an async operation
|
||||
/// </summary>
|
||||
/// <param name="user">The member identity user</param>
|
||||
/// <param name="userName">The member user name</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>A task once complete</returns>
|
||||
public Task SetUserNameAsync(UmbracoMembersIdentityUser user, string userName, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
user.UserName = userName;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IdentityResult> UpdateAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken)
|
||||
/// <summary>
|
||||
/// Update the user asynchronously
|
||||
/// </summary>
|
||||
/// <param name="member">The member identity user</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>An identity result task</returns>
|
||||
public Task<IdentityResult> UpdateAsync(UmbracoMembersIdentityUser member, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (member == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(member));
|
||||
}
|
||||
|
||||
Attempt<int> asInt = member.Id.TryConvertTo<int>();
|
||||
if (asInt == false)
|
||||
{
|
||||
throw new InvalidOperationException("The member id must be an integer to work with the Umbraco");
|
||||
}
|
||||
|
||||
using (IScope scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
IMember found = _memberService.GetById(asInt.Result);
|
||||
if (found != null)
|
||||
{
|
||||
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
var isLoginsPropertyDirty = member.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
|
||||
|
||||
if (UpdateMemberProperties(found, member))
|
||||
{
|
||||
_memberService.Save(found);
|
||||
}
|
||||
|
||||
//if (isLoginsPropertyDirty)
|
||||
//{
|
||||
// _externalLoginService.Save(
|
||||
// found.Id,
|
||||
// member.Logins.Select(x => new ExternalLogin(
|
||||
// x.LoginProvider,
|
||||
// x.ProviderKey,
|
||||
// x.UserData)));
|
||||
//}
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
///TODO: All from BackOfficeUserStore - same. Can we share?
|
||||
/// TODO: All from BackOfficeUserStore - same. Can we share?
|
||||
/// <summary>
|
||||
/// Set the user password hash
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="passwordHash"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
/// <param name="user">The identity member user</param>
|
||||
/// <param name="passwordHash">The password hash</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <exception cref="ArgumentException">Throws if the properties are null</exception>
|
||||
/// <returns>Returns asynchronously</returns>
|
||||
public Task SetPasswordHashAsync(UmbracoMembersIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash));
|
||||
if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash));
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (passwordHash == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(passwordHash));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(passwordHash))
|
||||
{
|
||||
throw new ArgumentException("Value can't be empty.", nameof(passwordHash));
|
||||
}
|
||||
|
||||
user.PasswordHash = passwordHash;
|
||||
user.PasswordConfig = null; // Clear this so that it's reset at the repository level
|
||||
|
||||
// Clear this so that it's reset at the repository level
|
||||
user.PasswordConfig = null;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -290,12 +392,16 @@ namespace Umbraco.Infrastructure.Members
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
/// <returns/>
|
||||
public Task<string> GetPasswordHashAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
return Task.FromResult(user.PasswordHash);
|
||||
}
|
||||
@@ -303,17 +409,19 @@ namespace Umbraco.Infrastructure.Members
|
||||
/// <summary>
|
||||
/// Returns true if a user has a password set
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
/// <param name="user">The identity user</param>
|
||||
/// <param name="cancellationToken">The cancellation token</param>
|
||||
/// <returns>True if the user has a password</returns>
|
||||
public Task<bool> HasPasswordAsync(UmbracoMembersIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
return Task.FromResult(string.IsNullOrEmpty(user.PasswordHash) == false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -109,19 +109,20 @@ namespace Umbraco.Core.Services.Implement
|
||||
/// <param name="email">Email of the Member to create</param>
|
||||
/// <param name="name">Name of the Member to create</param>
|
||||
/// <param name="memberTypeAlias">Alias of the MemberType the Member should be based on</param>
|
||||
/// <exception cref="ArgumentException">Thrown when a member type for the given alias isn't found</exception>
|
||||
/// <returns><see cref="IMember"/></returns>
|
||||
public IMember CreateMember(string username, string email, string name, string memberTypeAlias)
|
||||
{
|
||||
var memberType = GetMemberType(memberTypeAlias);
|
||||
IMemberType memberType = GetMemberType(memberTypeAlias);
|
||||
if (memberType == null)
|
||||
{
|
||||
throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias));
|
||||
}
|
||||
|
||||
var member = new Member(name, email.ToLower().Trim(), username, memberType);
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
using IScope scope = ScopeProvider.CreateScope();
|
||||
CreateMember(scope, member, 0, false);
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
return member;
|
||||
}
|
||||
@@ -312,7 +313,9 @@ namespace Umbraco.Core.Services.Implement
|
||||
// if saving is cancelled, media remains without an identity
|
||||
var saveEventArgs = new SaveEventArgs<IMember>(member);
|
||||
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_memberRepository.Save(member);
|
||||
|
||||
@@ -321,7 +324,9 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
if (withIdentity == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -10,6 +10,7 @@ using NUnit.Framework;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Members;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Scoping;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Infrastructure.Members;
|
||||
using Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper;
|
||||
@@ -26,7 +27,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Members
|
||||
_mockMemberService = new Mock<IMemberService>();
|
||||
return new UmbracoMembersUserStore(
|
||||
_mockMemberService.Object,
|
||||
new UmbracoMapper(new MapDefinitionCollection(new List<IMapDefinition>())));
|
||||
new UmbracoMapper(new MapDefinitionCollection(new List<IMapDefinition>())),
|
||||
new Mock<IScopeProvider>().Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Dictionary;
|
||||
using Umbraco.Core.Events;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
@@ -54,14 +49,22 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
_serializer = serializer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles if the content for the specified ID isn't found
|
||||
/// </summary>
|
||||
/// <param name="id">The content ID to find</param>
|
||||
/// <param name="throwException">Whether to throw an exception</param>
|
||||
/// <returns>The error response</returns>
|
||||
protected NotFoundObjectResult HandleContentNotFound(object id, bool throwException = true)
|
||||
{
|
||||
ModelState.AddModelError("id", $"content with id: {id} was not found");
|
||||
var errorResponse = NotFound(ModelState);
|
||||
NotFoundObjectResult errorResponse = NotFound(ModelState);
|
||||
|
||||
if (throwException)
|
||||
{
|
||||
throw new HttpResponseException(errorResponse);
|
||||
}
|
||||
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
where TSaved : IContentSave<TPersisted>
|
||||
{
|
||||
// map the property values
|
||||
foreach (var propertyDto in dto.Properties)
|
||||
foreach (ContentPropertyDto propertyDto in dto.Properties)
|
||||
{
|
||||
// get the property editor
|
||||
if (propertyDto.PropertyEditor == null)
|
||||
@@ -89,19 +92,24 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
|
||||
// get the value editor
|
||||
// nothing to save/map if it is readonly
|
||||
var valueEditor = propertyDto.PropertyEditor.GetValueEditor();
|
||||
if (valueEditor.IsReadOnly) continue;
|
||||
IDataValueEditor valueEditor = propertyDto.PropertyEditor.GetValueEditor();
|
||||
if (valueEditor.IsReadOnly)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the property
|
||||
var property = contentItem.PersistedContent.Properties[propertyDto.Alias];
|
||||
IProperty property = contentItem.PersistedContent.Properties[propertyDto.Alias];
|
||||
|
||||
// prepare files, if any matching property and culture
|
||||
var files = contentItem.UploadedFiles
|
||||
ContentPropertyFile[] files = contentItem.UploadedFiles
|
||||
.Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment)
|
||||
.ToArray();
|
||||
|
||||
foreach (var file in files)
|
||||
foreach (ContentPropertyFile file in files)
|
||||
{
|
||||
file.FileName = file.FileName.ToSafeFileName(ShortStringHelper);
|
||||
}
|
||||
|
||||
// create the property data for the property editor
|
||||
var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration)
|
||||
@@ -112,25 +120,35 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
};
|
||||
|
||||
// let the editor convert the value that was received, deal with files, etc
|
||||
var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
|
||||
object value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
|
||||
|
||||
// set the value - tags are special
|
||||
var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
|
||||
TagsPropertyEditorAttribute tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
|
||||
if (tagAttribute != null)
|
||||
{
|
||||
var tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
|
||||
if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter;
|
||||
TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
|
||||
if (tagConfiguration.Delimiter == default)
|
||||
{
|
||||
tagConfiguration.Delimiter = tagAttribute.Delimiter;
|
||||
}
|
||||
|
||||
var tagCulture = property.PropertyType.VariesByCulture() ? culture : null;
|
||||
property.SetTagsValue(_serializer, value, tagConfiguration, tagCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
savePropertyValue(contentItem, property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles if the state is invalid
|
||||
/// </summary>
|
||||
/// <param name="display">The model state to display</param>
|
||||
protected virtual void HandleInvalidModelState(IErrorModel display)
|
||||
{
|
||||
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
display.Errors = ModelState.ToErrorDictionary();
|
||||
@@ -151,38 +169,45 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </remarks>
|
||||
protected TPersisted GetObjectFromRequest<TPersisted>(Func<TPersisted> getFromService)
|
||||
{
|
||||
//checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
|
||||
// checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
|
||||
// it from the callback
|
||||
return HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null
|
||||
? (TPersisted) HttpContext.Items[typeof (TPersisted).ToString()]
|
||||
? (TPersisted)HttpContext.Items[typeof(TPersisted).ToString()]
|
||||
: getFromService();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the action passed in means we need to create something new
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
internal static bool IsCreatingAction(ContentSaveAction action)
|
||||
{
|
||||
return (action.ToString().EndsWith("New"));
|
||||
}
|
||||
/// <param name="action">The content action</param>
|
||||
/// <returns>Returns true if this is a creating action</returns>
|
||||
internal static bool IsCreatingAction(ContentSaveAction action) => action.ToString().EndsWith("New");
|
||||
|
||||
protected void AddCancelMessage(INotificationModel display,
|
||||
string header = "speechBubbles/operationCancelledHeader",
|
||||
string message = "speechBubbles/operationCancelledText",
|
||||
bool localizeHeader = true,
|
||||
/// <summary>
|
||||
/// Adds a cancelled message to the display
|
||||
/// </summary>
|
||||
/// <param name="display"></param>
|
||||
/// <param name="header"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="localizeHeader"></param>
|
||||
/// <param name="localizeMessage"></param>
|
||||
/// <param name="headerParams"></param>
|
||||
/// <param name="messageParams"></param>
|
||||
protected void AddCancelMessage(INotificationModel display, string header = "speechBubbles/operationCancelledHeader", string message = "speechBubbles/operationCancelledText", bool localizeHeader = true,
|
||||
bool localizeMessage = true,
|
||||
string[] headerParams = null,
|
||||
string[] messageParams = null)
|
||||
{
|
||||
//if there's already a default event message, don't add our default one
|
||||
var msgs = EventMessages;
|
||||
if (msgs != null && msgs.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) return;
|
||||
// if there's already a default event message, don't add our default one
|
||||
IEventMessagesFactory messages = EventMessages;
|
||||
if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
display.AddWarningNotification(
|
||||
localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header,
|
||||
localizeMessage ? LocalizedTextService.Localize(message, messageParams): message);
|
||||
localizeMessage ? LocalizedTextService.Localize(message, messageParams) : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@@ -7,8 +7,8 @@ using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Core;
|
||||
@@ -17,6 +17,7 @@ using Umbraco.Core.Events;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Serialization;
|
||||
@@ -54,9 +55,25 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
private readonly IUmbracoMembersUserManager _memberManager;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cultureDictionary">The culture dictionary</param>
|
||||
/// <param name="loggerFactory">The logger factory</param>
|
||||
/// <param name="shortStringHelper">The string helper</param>
|
||||
/// <param name="eventMessages">The event messages factory</param>
|
||||
/// <param name="localizedTextService">The entry point for localizing key services</param>
|
||||
/// <param name="propertyEditors">The property editors</param>
|
||||
/// <param name="umbracoMapper">The mapper</param>
|
||||
/// <param name="memberService">The member service</param>
|
||||
/// <param name="memberTypeService">The member type service</param>
|
||||
/// <param name="memberManager">The member manager</param>
|
||||
/// <param name="dataTypeService">The data-type service</param>
|
||||
/// <param name="backOfficeSecurityAccessor">The back office security accessor</param>
|
||||
/// <param name="jsonSerializer">The JSON serializer</param>
|
||||
public MemberController(
|
||||
ICultureDictionary cultureDictionary,
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -69,7 +86,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
IMemberTypeService memberTypeService,
|
||||
IUmbracoMembersUserManager memberManager,
|
||||
IDataTypeService dataTypeService,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IJsonSerializer jsonSerializer)
|
||||
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer)
|
||||
{
|
||||
@@ -80,10 +97,21 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
_memberManager = memberManager;
|
||||
_dataTypeService = dataTypeService;
|
||||
_localizedTextService = localizedTextService;
|
||||
_backofficeSecurityAccessor = backofficeSecurityAccessor;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The paginated list of members
|
||||
/// </summary>
|
||||
/// <param name="pageNumber">The page number to display</param>
|
||||
/// <param name="pageSize">The size of the page</param>
|
||||
/// <param name="orderBy">The ordering of the member list</param>
|
||||
/// <param name="orderDirection">The direction of the member list</param>
|
||||
/// <param name="orderBySystemField">The system field to order by</param>
|
||||
/// <param name="filter">The current filter for the list</param>
|
||||
/// <param name="memberTypeAlias">The member type</param>
|
||||
/// <returns>The paged result of members</returns>
|
||||
public PagedResult<MemberBasic> GetPagedResults(
|
||||
int pageNumber = 1,
|
||||
int pageSize = 100,
|
||||
@@ -123,11 +151,11 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Returns a display node with a list view to render members
|
||||
/// </summary>
|
||||
/// <param name="listName"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="listName">The member type to list</param>
|
||||
/// <returns>The member list for display</returns>
|
||||
public MemberListDisplay GetListNodeDisplay(string listName)
|
||||
{
|
||||
var foundType = _memberTypeService.Get(listName);
|
||||
IMemberType foundType = _memberTypeService.Get(listName);
|
||||
var name = foundType != null ? foundType.Name : listName;
|
||||
|
||||
var apps = new List<ContentApp>
|
||||
@@ -159,25 +187,26 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Gets the content json for the member
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="key">The Guid key of the member</param>
|
||||
/// <returns>The member for display</returns>
|
||||
[OutgoingEditorModelEvent]
|
||||
public MemberDisplay GetByKey(Guid key)
|
||||
{
|
||||
//TODO: this is not finding the key currently
|
||||
// TODO: this is not finding the key currently
|
||||
IMember foundMember = _memberService.GetByKey(key);
|
||||
if (foundMember == null)
|
||||
{
|
||||
HandleContentNotFound(key);
|
||||
}
|
||||
|
||||
return _umbracoMapper.Map<MemberDisplay>(foundMember);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty content item for the
|
||||
/// </summary>
|
||||
/// <param name="contentTypeAlias"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="contentTypeAlias">The content type</param>
|
||||
/// <returns>The empty member for display</returns>
|
||||
[OutgoingEditorModelEvent]
|
||||
public MemberDisplay GetEmpty(string contentTypeAlias = null)
|
||||
{
|
||||
@@ -202,91 +231,109 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Saves member
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <param name="contentItem">The content item to save as a member</param>
|
||||
/// <returns>The resulting member display object</returns>
|
||||
[FileUploadCleanupFilter]
|
||||
[OutgoingEditorModelEvent]
|
||||
[MemberSaveValidation]
|
||||
public async Task<ActionResult<MemberDisplay>> PostSave(
|
||||
[ModelBinder(typeof(MemberBinder))]
|
||||
MemberSave contentItem)
|
||||
public async Task<ActionResult<MemberDisplay>> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem)
|
||||
{
|
||||
if (contentItem == null) throw new ArgumentNullException(nameof(contentItem));
|
||||
if (contentItem == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(contentItem));
|
||||
}
|
||||
|
||||
if (ModelState.IsValid == false)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState);
|
||||
}
|
||||
|
||||
//If we've reached here it means:
|
||||
// If we've reached here it means:
|
||||
// * Our model has been bound
|
||||
// * and validated
|
||||
// * any file attachments have been saved to their temporary location for us to use
|
||||
// * we have a reference to the DTO object and the persisted object
|
||||
// * Permissions are valid
|
||||
|
||||
//map the properties to the persisted entity
|
||||
// map the properties to the persisted entity
|
||||
MapPropertyValues(contentItem);
|
||||
|
||||
UmbracoMembersIdentityUser identityMember = ValidateMemberData(contentItem);
|
||||
ValidateMemberData(contentItem);
|
||||
|
||||
//Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
|
||||
// Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
|
||||
if (ModelState.IsValid == false)
|
||||
{
|
||||
var forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
MemberDisplay forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
forDisplay.Errors = ModelState.ToErrorDictionary();
|
||||
throw HttpResponseException.CreateValidationErrorResponse(forDisplay);
|
||||
}
|
||||
|
||||
//We're gonna look up the current roles now because the below code can cause
|
||||
IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
|
||||
if (memberType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
|
||||
}
|
||||
|
||||
// Create the member with the MemberManager
|
||||
var identityMember = UmbracoMembersIdentityUser.CreateNew(
|
||||
contentItem.Username,
|
||||
contentItem.Email,
|
||||
memberType.Alias,
|
||||
contentItem.Name);
|
||||
|
||||
// We're gonna look up the current roles now because the below code can cause
|
||||
// events to be raised and developers could be manually adding roles to members in
|
||||
// their handlers. If we don't look this up now there's a chance we'll just end up
|
||||
// removing the roles they've assigned.
|
||||
var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
|
||||
IEnumerable<string> currentRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
|
||||
|
||||
//find the ones to remove and remove them
|
||||
IEnumerable<string> roles = currRoles.ToList();
|
||||
var rolesToRemove = roles.Except(contentItem.Groups).ToArray();
|
||||
// find the ones to remove and remove them
|
||||
IEnumerable<string> roles = currentRoles.ToList();
|
||||
string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray();
|
||||
|
||||
//Depending on the action we need to first do a create or update using the membership manager
|
||||
//this ensures that passwords are formatted correctly and also performs the validation on the provider itself.
|
||||
// Depending on the action we need to first do a create or update using the membership manager
|
||||
// this ensures that passwords are formatted correctly and also performs the validation on the provider itself.
|
||||
switch (contentItem.Action)
|
||||
{
|
||||
case ContentSaveAction.Save:
|
||||
UpdateMemberData(contentItem);
|
||||
break;
|
||||
case ContentSaveAction.SaveNew:
|
||||
await CreateMemberAsync(contentItem, identityMember);
|
||||
IdentityResult identityResult = await CreateMemberAsync(contentItem, identityMember);
|
||||
break;
|
||||
default:
|
||||
//we don't support anything else for members
|
||||
// we don't support anything else for members
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
//TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope
|
||||
// TODO: There's 3 things saved here and we should do this all in one transaction,
|
||||
// which we can do here by wrapping in a scope
|
||||
// but it would be nicer to have this taken care of within the Save method itself
|
||||
|
||||
//Now let's do the role provider stuff - now that we've saved the content item (that is important since
|
||||
// Now let's do the role provider stuff - now that we've saved the content item (that is important since
|
||||
// if we are changing the username, it must be persisted before looking up the member roles).
|
||||
if (rolesToRemove.Any())
|
||||
{
|
||||
_memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove);
|
||||
}
|
||||
//find the ones to add and add them
|
||||
var toAdd = contentItem.Groups.Except(roles).ToArray();
|
||||
|
||||
// find the ones to add and add them
|
||||
string[] toAdd = contentItem.Groups.Except(roles).ToArray();
|
||||
if (toAdd.Any())
|
||||
{
|
||||
//add the ones submitted
|
||||
// add the ones submitted
|
||||
_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd);
|
||||
}
|
||||
|
||||
//return the updated model
|
||||
var display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
// return the updated model
|
||||
MemberDisplay display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
|
||||
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
HandleInvalidModelState(display);
|
||||
|
||||
var localizedTextService = _localizedTextService;
|
||||
//put the correct messages in
|
||||
ILocalizedTextService localizedTextService = _localizedTextService;
|
||||
|
||||
// put the correct messages in
|
||||
switch (contentItem.Action)
|
||||
{
|
||||
case ContentSaveAction.Save:
|
||||
@@ -303,77 +350,64 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Maps the property values to the persisted entity
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
/// <param name="contentItem">The member content item to map properties from</param>
|
||||
private void MapPropertyValues(MemberSave contentItem)
|
||||
{
|
||||
//Don't update the name if it is empty
|
||||
// Don't update the name if it is empty
|
||||
if (contentItem.Name.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
contentItem.PersistedContent.Name = contentItem.Name;
|
||||
}
|
||||
|
||||
//map the custom properties - this will already be set for new entities in our member binder
|
||||
// map the custom properties - this will already be set for new entities in our member binder
|
||||
contentItem.PersistedContent.Email = contentItem.Email;
|
||||
contentItem.PersistedContent.Username = contentItem.Username;
|
||||
|
||||
//use the base method to map the rest of the properties
|
||||
base.MapPropertyValuesForPersistence<IMember, MemberSave>(
|
||||
// use the base method to map the rest of the properties
|
||||
MapPropertyValuesForPersistence<IMember, MemberSave>(
|
||||
contentItem,
|
||||
contentItem.PropertyCollectionDto,
|
||||
(save, property) => property.GetValue(), //get prop val
|
||||
(save, property, v) => property.SetValue(v), //set prop val
|
||||
(save, property) => property.GetValue(), // get prop val
|
||||
(save, property, v) => property.SetValue(v), // set prop val
|
||||
null); // member are all invariant
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a member from the supplied member content data
|
||||
/// All member password processing and creation is done via the aspnet identity MemberUserManager
|
||||
///
|
||||
/// All member password processing and creation is done via the identity manager
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
/// <param name="identityMember"></param>
|
||||
/// <returns></returns>
|
||||
private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember)
|
||||
/// <param name="contentItem">Member content data</param>
|
||||
/// <param name="identityMember">The identity member to update</param>
|
||||
/// <returns>The identity result of the created member</returns>
|
||||
private async Task<IdentityResult> CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember)
|
||||
{
|
||||
//var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
|
||||
//if (memberType == null)
|
||||
// throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
|
||||
//var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true)
|
||||
//{
|
||||
// CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id,
|
||||
// RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
|
||||
// Comments = contentItem.Comments,
|
||||
// IsApproved = contentItem.IsApproved
|
||||
//};
|
||||
|
||||
//return member;
|
||||
|
||||
IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword);
|
||||
if (created.Succeeded == false)
|
||||
{
|
||||
throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
//now re-look the member back up which will now exist
|
||||
// now re-look the member back up which will now exist
|
||||
IMember member = _memberService.GetByEmail(contentItem.Email);
|
||||
|
||||
member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
member.CreatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
// should this be removed since we've moved passwords out?
|
||||
|
||||
//since the back office user is creating this member, they will be set to approved
|
||||
member.RawPasswordValue = identityMember.PasswordHash;
|
||||
member.Comments = contentItem.Comments;
|
||||
|
||||
// since the back office user is creating this member, they will be set to approved
|
||||
member.IsApproved = true;
|
||||
|
||||
//map the save info over onto the user
|
||||
// map the save info over onto the user
|
||||
member = _umbracoMapper.Map(contentItem, member);
|
||||
contentItem.PersistedContent = member;
|
||||
return created;
|
||||
}
|
||||
|
||||
private UmbracoMembersIdentityUser ValidateMemberData(MemberSave contentItem)
|
||||
private void ValidateMemberData(MemberSave contentItem)
|
||||
{
|
||||
var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
|
||||
if (memberType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
|
||||
}
|
||||
|
||||
if (contentItem.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
@@ -399,14 +433,13 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
|
||||
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
//TODO: check password
|
||||
//var validPassword = await _memberManager.CheckPasswordAsync(null, contentItem.Password.NewPassword);
|
||||
//if (!validPassword)
|
||||
//{
|
||||
// ModelState.AddPropertyError(
|
||||
// new ValidationResult("Invalid password", new[] { "value" }),
|
||||
// $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
|
||||
//}
|
||||
Task<List<IdentityResult>> result = _memberManager.ValidatePassword(contentItem.Password.NewPassword);
|
||||
if (result.Result.Exists(x => x.Succeeded == false))
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult($"Invalid password: {MapErrors(result.Result)}", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -414,48 +447,48 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
new ValidationResult("Password cannot be empty", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
|
||||
}
|
||||
}
|
||||
|
||||
// Create the member with the MemberManager
|
||||
var identityMember = UmbracoMembersIdentityUser.CreateNew(
|
||||
contentItem.Username,
|
||||
contentItem.Email,
|
||||
memberType.Alias,
|
||||
contentItem.Name);
|
||||
//TODO: confirm where to do this
|
||||
identityMember.RawPasswordValue = contentItem.Password.NewPassword;
|
||||
return identityMember;
|
||||
private string MapErrors(List<IdentityResult> result)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
IEnumerable<IdentityResult> errors = result.Where(x => x.Succeeded == false);
|
||||
|
||||
foreach (IdentityResult error in errors)
|
||||
{
|
||||
sb.AppendLine(error.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the member security data
|
||||
/// </summary>
|
||||
/// <param name="memberSave"></param>
|
||||
/// <returns>
|
||||
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
|
||||
/// </returns>
|
||||
private async void UpdateMemberData(MemberSave memberSave)
|
||||
/// </summary>
|
||||
/// <param name="memberSave">The member to save</param>
|
||||
private void UpdateMemberData(MemberSave memberSave)
|
||||
{
|
||||
//TODO: optimise based on new member manager
|
||||
memberSave.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
memberSave.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
|
||||
// If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types
|
||||
// have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted.
|
||||
// There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut
|
||||
// but we will take care of this in a generic way below so that it works for all props.
|
||||
if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
|
||||
if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
|
||||
{
|
||||
var memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId);
|
||||
IMemberType memberType = _memberTypeService.Get(memberSave.PersistedContent.ContentTypeId);
|
||||
var sensitiveProperties = memberType
|
||||
.PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias))
|
||||
.ToList();
|
||||
|
||||
foreach (var sensitiveProperty in sensitiveProperties)
|
||||
foreach (IPropertyType sensitiveProperty in sensitiveProperties)
|
||||
{
|
||||
var destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
|
||||
ContentPropertyBasic destProp = memberSave.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
|
||||
if (destProp != null)
|
||||
{
|
||||
//if found, change the value of the contentItem model to the persisted value so it remains unchanged
|
||||
var origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias);
|
||||
// if found, change the value of the contentItem model to the persisted value so it remains unchanged
|
||||
object origValue = memberSave.PersistedContent.GetValue(sensitiveProperty.Alias);
|
||||
destProp.Value = origValue;
|
||||
}
|
||||
}
|
||||
@@ -463,7 +496,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
|
||||
var isLockedOut = memberSave.IsLockedOut;
|
||||
|
||||
//if they were locked but now they are trying to be unlocked
|
||||
// if they were locked but now they are trying to be unlocked
|
||||
if (memberSave.PersistedContent.IsLockedOut && isLockedOut == false)
|
||||
{
|
||||
memberSave.PersistedContent.IsLockedOut = false;
|
||||
@@ -471,34 +504,34 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
else if (!memberSave.PersistedContent.IsLockedOut && isLockedOut)
|
||||
{
|
||||
//NOTE: This should not ever happen unless someone is mucking around with the request data.
|
||||
//An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
|
||||
// NOTE: This should not ever happen unless someone is mucking around with the request data.
|
||||
// An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
|
||||
ModelState.AddModelError("custom", "An admin cannot lock a user");
|
||||
}
|
||||
|
||||
//no password changes then exit ?
|
||||
if (memberSave.Password == null)
|
||||
return;
|
||||
//TODO: update member password functionality in manager// set the password
|
||||
|
||||
// no password changes then exit ?
|
||||
if (memberSave.Password != null)
|
||||
{
|
||||
// set the password
|
||||
memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permanently deletes a member
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="key">Guid of the member to delete</param>
|
||||
/// <returns>The result of the deletion</returns>
|
||||
///
|
||||
[HttpPost]
|
||||
public IActionResult DeleteByKey(Guid key)
|
||||
{
|
||||
var foundMember = _memberService.GetByKey(key);
|
||||
IMember foundMember = _memberService.GetByKey(key);
|
||||
if (foundMember == null)
|
||||
{
|
||||
return HandleContentNotFound(key, false);
|
||||
}
|
||||
|
||||
_memberService.Delete(foundMember);
|
||||
|
||||
return Ok();
|
||||
@@ -512,19 +545,23 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
[HttpGet]
|
||||
public IActionResult ExportMemberData(Guid key)
|
||||
{
|
||||
var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
|
||||
IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
|
||||
|
||||
if (currentUser.HasAccessToSensitiveData() == false)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var member = ((MemberService)_memberService).ExportMember(key);
|
||||
if (member is null) throw new NullReferenceException("No member found with key " + key);
|
||||
MemberExportModel member = ((MemberService)_memberService).ExportMember(key);
|
||||
if (member is null)
|
||||
{
|
||||
throw new NullReferenceException("No member found with key " + key);
|
||||
}
|
||||
|
||||
var json = _jsonSerializer.Serialize(member);
|
||||
|
||||
var fileName = $"{member.Name}_{member.Email}.txt";
|
||||
|
||||
// Set custom header so umbRequestHelper.downloadFile can save the correct filename
|
||||
HttpContext.Response.Headers.Add("x-filename", fileName);
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=2DA32DA040A7D74599ABE288C7224CF0/Severity/@EntryValue">HINT</s:String>
|
||||
<s:Boolean x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=37A0B37A0ABAA34AA5CB32A93653C4FE/@KeyIndexDefined">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">Default</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Umbraco/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublish/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublishing/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
||||
Reference in New Issue
Block a user