Set hasIdentity if there is an ID, and logical adjustments to set passwords corectly
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
namespace Umbraco.Core.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// The password configuration for back office users
|
||||
/// The password configuration for members
|
||||
/// </summary>
|
||||
public class MemberPasswordConfiguration : PasswordConfiguration, IMemberPasswordConfiguration
|
||||
{
|
||||
|
||||
@@ -13,14 +13,13 @@ namespace Umbraco.Core.Members
|
||||
//: IdentityUser<int, IIdentityUserLogin, IdentityUserRole<string>, IdentityUserClaim<int>>,
|
||||
{
|
||||
private bool _hasIdentity;
|
||||
private int _id;
|
||||
|
||||
public int Id { get; set; }
|
||||
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; }
|
||||
public DateTime LastPasswordChangeDateUtc { get; set; }
|
||||
|
||||
@@ -30,6 +29,16 @@ namespace Umbraco.Core.Members
|
||||
/// </summary>
|
||||
public bool HasIdentity => _hasIdentity;
|
||||
|
||||
public int Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
{
|
||||
_id = value;
|
||||
_hasIdentity = true;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: track
|
||||
public string PasswordHash { get; set; }
|
||||
|
||||
@@ -48,7 +57,11 @@ namespace Umbraco.Core.Members
|
||||
//public bool RolesChanged;
|
||||
|
||||
|
||||
public static UmbracoMembersIdentityUser CreateNew(string username, string email, string name = null)
|
||||
public static UmbracoMembersIdentityUser CreateNew(
|
||||
string username,
|
||||
string email,
|
||||
string memberTypeAlias,
|
||||
string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
|
||||
|
||||
@@ -58,7 +71,9 @@ namespace Umbraco.Core.Members
|
||||
UserName = username,
|
||||
Email = email,
|
||||
Name = name,
|
||||
Id = 0, //TODO
|
||||
MemberTypeAlias = memberTypeAlias,
|
||||
Id = 0, //TODO: is this meant to be 0 in this circumstance?
|
||||
//false by default unless specifically set
|
||||
_hasIdentity = false
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.Key = source.Key;
|
||||
target.Username = source.Username;
|
||||
target.Id = (int)(long)source.Id;
|
||||
//TODO: map more properties as required
|
||||
target.Comments = source.Comments;
|
||||
target.IsApproved = source.IsApproved;
|
||||
|
||||
//TODO: ensure all properties are mapped as required
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,16 @@ namespace Umbraco.Infrastructure.Members
|
||||
public interface IUmbracoMembersUserManager<TUser> : IDisposable where TUser : UmbracoMembersIdentityUser
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the specified <paramref name="memberUser"/> in the backing store with no password,
|
||||
/// Creates the specified <paramref name="memberUser"/> in the backing store with a password,
|
||||
/// as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="memberUser">The member to create.</param>
|
||||
/// <param name="password">The password to add</param>
|
||||
/// <returns>
|
||||
/// The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/>
|
||||
/// of the operation.
|
||||
/// </returns>
|
||||
Task<IdentityResult> CreateAsync(TUser memberUser);
|
||||
Task<IdentityResult> CreateAsync(TUser memberUser, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to generate a password for a user based on the current password validator
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace Umbraco.Infrastructure.Members
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
///TODO: duplicated code
|
||||
///TODO: duplicated code from backofficeusermanager
|
||||
/// <summary>
|
||||
/// Logic used to validate a username and password
|
||||
/// </summary>
|
||||
|
||||
@@ -52,19 +52,7 @@ namespace Umbraco.Infrastructure.Members
|
||||
|
||||
UpdateMemberProperties(member, user);
|
||||
|
||||
if (member.RawPasswordValue.IsNullOrWhiteSpace())
|
||||
{
|
||||
// [Comments from Identity package and BackOfficeUser - can/should we share this functionality]
|
||||
// the password must be 'something' it could be empty if authenticating
|
||||
// with an external provider so we'll just generate one and prefix it, the
|
||||
// prefix will help us determine if the password hasn't actually been specified yet.
|
||||
//this will hash the guid with a salt so should be nicely random
|
||||
var aspHasher = new PasswordHasher<UmbracoMembersIdentityUser>();
|
||||
var emptyPasswordValue =
|
||||
Constants.Security.EmptyPasswordPrefix +
|
||||
aspHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
|
||||
member.RawPasswordValue = emptyPasswordValue;
|
||||
}
|
||||
//TODO: do we want to accept empty passwords here - if thirdparty for example? In other method if so?
|
||||
|
||||
_memberService.Save(member);
|
||||
|
||||
|
||||
@@ -11,13 +11,10 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.Dictionary;
|
||||
using Umbraco.Core.Events;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Members;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
@@ -38,6 +35,7 @@ using Umbraco.Web.Common.Filters;
|
||||
using Umbraco.Web.ContentApps;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
using UmbracoMembersIdentityUser = Umbraco.Core.Members.UmbracoMembersIdentityUser;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Controllers
|
||||
{
|
||||
@@ -213,6 +211,12 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
[ModelBinder(typeof(MemberBinder))]
|
||||
MemberSave 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:
|
||||
// * Our model has been bound
|
||||
@@ -224,7 +228,44 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
//map the properties to the persisted entity
|
||||
MapPropertyValues(contentItem);
|
||||
|
||||
await ValidateMemberDataAsync(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(
|
||||
new ValidationResult("Invalid user name", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
}
|
||||
|
||||
IMember byUsername = _memberService.GetByUsername(contentItem.Username);
|
||||
if (byUsername != null && byUsername.Key != contentItem.Key)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Username is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
}
|
||||
|
||||
IMember byEmail = _memberService.GetByEmail(contentItem.Email);
|
||||
if (byEmail != null && byEmail.Key != contentItem.Key)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Email address is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
//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)
|
||||
@@ -238,21 +279,21 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
// 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> currRoles = _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();
|
||||
|
||||
//Depending on the action we need to first do a create or update using the membership provider
|
||||
// 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:
|
||||
contentItem.PersistedContent = await CreateMemberData(contentItem);
|
||||
await CreateMemberAsync(contentItem, identityMember);
|
||||
break;
|
||||
default:
|
||||
//we don't support anything else for members
|
||||
@@ -262,9 +303,6 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
//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
|
||||
|
||||
//TODO: create/save the IMember: this is now saved in CreateAsync, remove once logic is clarified
|
||||
//_memberService.Save(contentItem.PersistedContent);
|
||||
|
||||
//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())
|
||||
@@ -300,13 +338,46 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
return display;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the member
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
/// <param name="identityMember"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
private async Task CreateMemberAsync(MemberSave contentItem, UmbracoMembersIdentityUser identityMember)
|
||||
{
|
||||
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
|
||||
IMember member = _memberService.GetByEmail(contentItem.Email);
|
||||
|
||||
member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
member.RawPasswordValue = identityMember.RawPasswordValue;
|
||||
|
||||
//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
|
||||
member = _umbracoMapper.Map(contentItem, member);
|
||||
contentItem.PersistedContent = member;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the property values to the persisted entity
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
private void MapPropertyValues(MemberSave contentItem)
|
||||
{
|
||||
UpdateName(contentItem);
|
||||
//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
|
||||
contentItem.PersistedContent.Email = contentItem.Email;
|
||||
@@ -321,83 +392,9 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="memberSave"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<IMember> CreateMemberData(MemberSave memberSave)
|
||||
{
|
||||
if (memberSave == null) throw new ArgumentNullException("memberSave");
|
||||
|
||||
if (ModelState.IsValid == false)
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState);
|
||||
}
|
||||
//TODO: check if unique
|
||||
|
||||
IMemberType memberType = _memberTypeService.Get(memberSave.ContentTypeAlias);
|
||||
if (memberType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No member type found with alias {memberSave.ContentTypeAlias}");
|
||||
}
|
||||
|
||||
// Create the member with the UserManager
|
||||
// The 'empty' (special) password format is applied without us having to duplicate that logic
|
||||
UmbracoMembersIdentityUser identityMember = UmbracoMembersIdentityUser.CreateNew(
|
||||
memberSave.Username,
|
||||
memberSave.Email,
|
||||
memberSave.Name);
|
||||
|
||||
//TODO: confirm
|
||||
identityMember.MemberTypeAlias = memberType.Alias;
|
||||
|
||||
IdentityResult created = await _memberManager.CreateAsync(identityMember);
|
||||
if (created.Succeeded == false)
|
||||
{
|
||||
throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
//string resetPassword;
|
||||
//string password = _memberManager.GeneratePassword();
|
||||
|
||||
//IdentityResult result = await _memberManager.AddPasswordAsync(identityMember, password);
|
||||
//if (result.Succeeded == false)
|
||||
//{
|
||||
// throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage());
|
||||
//}
|
||||
|
||||
//resetPassword = password;
|
||||
|
||||
//now re-look the member back up which will now exist
|
||||
IMember member = _memberService.GetByEmail(memberSave.Email);
|
||||
|
||||
//TODO: previous implementation
|
||||
//IMember member = new Member(
|
||||
// memberSave.Name,
|
||||
// memberSave.Email,
|
||||
// memberSave.Username,
|
||||
// memberType,
|
||||
// true)
|
||||
//{
|
||||
// CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id,
|
||||
// RawPasswordValue = _memberManager.GeneratePassword(),
|
||||
// Comments = memberSave.Comments,
|
||||
// IsApproved = memberSave.IsApproved
|
||||
//};
|
||||
|
||||
|
||||
//since the back office user is creating this member, they will be set to approved
|
||||
member.IsApproved = true;
|
||||
member.CreatorId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
member.Comments = memberSave.Comments;
|
||||
member.IsApproved = memberSave.IsApproved;
|
||||
|
||||
//map the save info over onto the user
|
||||
member = _umbracoMapper.Map(memberSave, member);
|
||||
return member;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the member security data
|
||||
@@ -406,7 +403,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <returns>
|
||||
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
|
||||
/// </returns>
|
||||
private void UpdateMemberData(MemberSave memberSave)
|
||||
private async void UpdateMemberData(MemberSave memberSave)
|
||||
{
|
||||
//TODO: optimise based on new member manager
|
||||
memberSave.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
@@ -452,66 +449,12 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
//no password changes then exit ?
|
||||
if (memberSave.Password == null)
|
||||
return;
|
||||
//TODO: update member password functionality in manager// set the password
|
||||
|
||||
// set the password
|
||||
memberSave.PersistedContent.RawPasswordValue = _memberManager.GeneratePassword();
|
||||
}
|
||||
|
||||
private static void UpdateName(MemberSave memberSave)
|
||||
{
|
||||
//Don't update the name if it is empty
|
||||
if (memberSave.Name.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
memberSave.PersistedContent.Name = memberSave.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This logic should be pulled into the service layer
|
||||
private async Task<bool> ValidateMemberDataAsync(MemberSave contentItem)
|
||||
{
|
||||
if (contentItem.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Invalid user name", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
//TODO: implement as per backoffice user
|
||||
//var validPassword = await _memberManager.CheckPasswordAsync(null, contentItem.Password.NewPassword);
|
||||
//if (!validPassword)
|
||||
//{
|
||||
// ModelState.AddPropertyError(
|
||||
// new ValidationResult("Invalid password: TODO", new[] { "value" }),
|
||||
// $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
|
||||
// return false;
|
||||
//}
|
||||
return true;
|
||||
}
|
||||
|
||||
var byUsername = _memberService.GetByUsername(contentItem.Username);
|
||||
if (byUsername != null && byUsername.Key != contentItem.Key)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Username is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
return false;
|
||||
}
|
||||
|
||||
var byEmail = _memberService.GetByEmail(contentItem.Email);
|
||||
if (byEmail != null && byEmail.Key != contentItem.Key)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Email address is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permanently deletes a member
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user