From f9319c0db9fa5e1a4059d156a9d0465fa6e18f80 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 20:56:30 +1100 Subject: [PATCH] Lots more work done with members mostly around passwords and membership provider - can't yet update your password but it's close --- .../src/common/services/util.service.js | 7 +- .../src/views/member/edit.html | 1 + .../changepassword.controller.js | 22 ++- .../changepassword/changepassword.html | 64 +++++-- src/Umbraco.Web/Editors/MemberController.cs | 159 ++++++++++++++++-- .../Models/ContentEditing/MemberPassword.cs | 37 ++++ .../Models/ContentEditing/MemberSave.cs | 12 +- .../Models/Mapping/MemberModelMapper.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../WebApi/Binders/MemberBinder.cs | 2 +- 10 files changed, 264 insertions(+), 42 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/MemberPassword.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index a66c3b549e..1fc040527a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -221,12 +221,7 @@ function umbDataFormatter() { }); saveModel.email = propEmail.value; saveModel.username = propLogin.value; - //NOTE: This would only be set for new members! - if (angular.isString(propPass.value)) { - // if we are resetting or changing passwords then that data will come from the property editor and - // it's value will be an object not just a string. - saveModel.password = propPass.value; - } + saveModel.password = propPass.value; return saveModel; }, diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index 3fccfda95d..5501ccb381 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -36,6 +36,7 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js index 00d2776510..e5d8739d51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js @@ -9,26 +9,40 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordCont hasPassword: true/false, requiresQuestionAnswer: true/false, enableReset: true/false, + enablePasswordRetrieval: true/false, minPasswordLength: 10 } */ //set defaults if they are not available - if (!$scope.model.config || !$scope.model.config.hasPassword) { + if (!$scope.model.config || $scope.model.config.hasPassword === undefined) { $scope.model.config.hasPassword = false; } - if (!$scope.model.config || !$scope.model.config.requiresQuestionAnswer) { + if (!$scope.model.config || $scope.model.config.enablePasswordRetrieval === undefined) { + $scope.model.config.enablePasswordRetrieval = true; + } + if (!$scope.model.config || $scope.model.config.requiresQuestionAnswer === undefined) { $scope.model.config.requiresQuestionAnswer = false; } - if (!$scope.model.config || !$scope.model.config.enableReset) { + if (!$scope.model.config || $scope.model.config.enableReset === undefined) { $scope.model.config.enableReset = true; } - if (!$scope.model.config || !$scope.model.config.minPasswordLength) { + if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { $scope.model.config.minPasswordLength = 0; } + //set the model defaults - we never get supplied a password from the server so this is ok to overwrite. + $scope.model.value = { + newPassword: "", + oldPassword: null, + reset: null, + answer: null + }; + //the value to compare to match passwords $scope.confirm = ""; + //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there + //with validators turned on. $scope.changing = !$scope.model.config.hasPassword; $scope.doChange = function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html index b15fced660..fa67b50b53 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html @@ -3,28 +3,56 @@ Change password
-
- - - - Required - Minimum {{model.config.minPasswordLength}} characters - - + +
+ +
+ + +
+
+ + +
+ +
+ + Required + +
-
- - - Passwords must match +
+ +
+ + Required + Minimum {{model.config.minPasswordLength}} characters + +
+
+ +
+ +
+ + Passwords must match +
+ Cancel
\ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 8ac0d6bdc6..8ad5f68422 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -39,7 +39,7 @@ namespace Umbraco.Web.Editors /// public MemberController() : this(UmbracoContext.Current) - { + { } /// @@ -72,7 +72,7 @@ namespace Umbraco.Web.Editors //TODO: Support this throw new HttpResponseException(Request.CreateValidationErrorResponse("Editing member with a non-umbraco membership provider is currently not supported")); } - + } /// @@ -119,7 +119,7 @@ namespace Umbraco.Web.Editors //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 = Mapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); @@ -145,7 +145,7 @@ namespace Umbraco.Web.Editors default: //we don't support anything else for members throw new HttpResponseException(HttpStatusCode.NotFound); - } + } //If we've had problems creating/updating the user with the provider then return the error if (ModelState.IsValid == false) @@ -154,7 +154,7 @@ namespace Umbraco.Web.Editors forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } - + //save the item //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password // so it will not get overwritten! @@ -199,8 +199,16 @@ namespace Umbraco.Web.Editors /// /// Update the membership user using the membership provider (for things like email, etc...) + /// If a password change is detected then we'll try that too. /// /// + /// + /// + /// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes! + /// + /// TODO: We need to update this method to return the new password if it has been reset and then show that to the UI! + /// + /// private void UpdateWithMembershipProvider(MemberSave contentItem) { //Get the member from the provider @@ -213,7 +221,7 @@ namespace Umbraco.Web.Editors //ok, first thing to do is check if they've changed their email //TODO: When we support the other membership provider data then we'll check if any of that's changed too. - if (contentItem.Email.Trim().InvariantEquals(membershipUser.Email)) + if (contentItem.Email.Trim().InvariantEquals(membershipUser.Email) == false) { membershipUser.Email = contentItem.Email.Trim(); @@ -229,6 +237,135 @@ namespace Umbraco.Web.Editors new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); } } + + //password changes ? + if (contentItem.Password == null) return; + + //Are we resetting the password?? + if (contentItem.Password.Reset.HasValue && contentItem.Password.Reset.Value) + { + if (Membership.Provider.EnablePasswordReset == false) + { + ModelState.AddPropertyError( + new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + else if (Membership.Provider.RequiresQuestionAndAnswer && contentItem.Password.Answer.IsNullOrWhiteSpace()) + { + ModelState.AddPropertyError( + new ValidationResult("Password reset requires a password answer", new[] {"resetPassword"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + else + { + //ok, we should be able to reset it + try + { + var newPass = Membership.Provider.ResetPassword( + membershipUser.UserName, + Membership.Provider.RequiresQuestionAndAnswer ? contentItem.Password.Answer : null); + + //TODO: How do we show this new password to the front-end ??? + } + catch (Exception ex) + { + LogHelper.WarnWithException("Could not reset member password", ex); + ModelState.AddPropertyError( + new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] {"resetPassword"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + } + } + else if (contentItem.Password.NewPassword.IsNullOrWhiteSpace() == false) + { + //we're not resetting it so we need to try to change it. + + if (contentItem.Password.OldPassword.IsNullOrWhiteSpace() && Membership.Provider.EnablePasswordRetrieval == false) + { + //if password retrieval is not enabled but there is no old password we cannot continue + + ModelState.AddPropertyError( + new ValidationResult("Password cannot be changed without the old password", new[] {"value"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + else if (contentItem.Password.OldPassword.IsNullOrWhiteSpace() == false) + { + //if an old password is suplied try to change it + + try + { + var result = Membership.Provider.ChangePassword(membershipUser.UserName, contentItem.Password.OldPassword, contentItem.Password.NewPassword); + if (result == false) + { + ModelState.AddPropertyError( + new ValidationResult("Could not change password", new[] {"value"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + } + catch (Exception ex) + { + LogHelper.WarnWithException("Could not change member password", ex); + ModelState.AddPropertyError( + new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] {"value"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + } + else if (Membership.Provider.EnablePasswordRetrieval == false) + { + //we cannot continue if we cannot get the current password + + ModelState.AddPropertyError( + new ValidationResult("Password cannot be changed without the old password", new[] {"value"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + else if (Membership.Provider.RequiresQuestionAndAnswer && contentItem.Password.Answer.IsNullOrWhiteSpace()) + { + //if the question answer is required but there isn't one, we cannot continue + + ModelState.AddPropertyError( + new ValidationResult("Password cannot be changed without the password answer", new[] {"value"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + + } + else + { + //lets try to get the old one so we can change it + + try + { + var oldPassword = Membership.Provider.GetPassword( + membershipUser.UserName, + Membership.Provider.RequiresQuestionAndAnswer ? contentItem.Password.Answer : null); + + try + { + var result = Membership.Provider.ChangePassword(membershipUser.UserName, oldPassword, contentItem.Password.NewPassword); + if (result == false) + { + ModelState.AddPropertyError( + new ValidationResult("Could not change password", new[] {"value"}), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + } + catch (Exception ex1) + { + LogHelper.WarnWithException("Could not change member password", ex1); + ModelState.AddPropertyError( + new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + + } + catch (Exception ex2) + { + LogHelper.WarnWithException("Could not retrieve member password", ex2); + ModelState.AddPropertyError( + new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + + } + } } /// @@ -247,18 +384,20 @@ namespace Umbraco.Web.Editors //TODO: I think we should detect if the Umbraco membership provider is active, if so then we'll create the member first and the provider key doesn't matter // but if we are using a 3rd party membership provider - then we should create our IMember first and use it's key as their provider user key! - + //NOTE: We are casting directly to the umbraco membership provider so we can specify the member type that we want to use! - + var umbracoMembershipProvider = (global::umbraco.providers.members.UmbracoMembershipProvider)Membership.Provider; var membershipUser = umbracoMembershipProvider.CreateUser( - contentItem.ContentTypeAlias, contentItem.Username, contentItem.Password, contentItem.Email, "", "", true, Guid.NewGuid(), out status); + contentItem.ContentTypeAlias, contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, "", "", true, Guid.NewGuid(), out status); //TODO: Localize these! switch (status) { case MembershipCreateStatus.Success: - + //Go and re-fetch the persisted item contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); //remap the values to save diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberPassword.cs b/src/Umbraco.Web/Models/ContentEditing/MemberPassword.cs new file mode 100644 index 0000000000..3a1c67842c --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberPassword.cs @@ -0,0 +1,37 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// A model representing the data required to set a member/user password depending on the provider installed. + /// + public class MemberPassword + { + /// + /// The password value + /// + /// + /// This + /// + [DataMember(Name = "newPassword")] + public string NewPassword { get; set; } + + /// + /// The old password - used to change a password when: EnablePasswordRetrieval = false + /// + [DataMember(Name = "oldPassword")] + public string OldPassword { get; set; } + + /// + /// Set to true if the password is to be reset - only valid when: EnablePasswordReset = true + /// + [DataMember(Name = "reset")] + public bool? Reset { get; set; } + + /// + /// The password answer - required for reset when: RequiresQuestionAndAnswer = true + /// + [DataMember(Name = "answer")] + public string Answer { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs b/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs index 8de5f3647c..ace4d576f9 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberSave.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using Newtonsoft.Json.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Validation; @@ -9,6 +10,11 @@ namespace Umbraco.Web.Models.ContentEditing /// public class MemberSave : ContentBaseItemSave { + public MemberSave() + { + Password = new MemberPassword(); + } + [DataMember(Name = "username", IsRequired = true)] [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] public string Username { get; set; } @@ -16,8 +22,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "email", IsRequired = true)] [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] public string Email { get; set; } - - [DataMember(Name = "password")] - public string Password { get; set; } + + [DataMember(Name = "password")] + public MemberPassword Password { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index ba9108a73e..da490d6f31 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -91,6 +91,7 @@ namespace Umbraco.Web.Models.Mapping { "hasPassword", member.Password.IsNullOrWhiteSpace() == false }, { "minPasswordLength", membershipProvider.MinRequiredPasswordLength }, { "enableReset", membershipProvider.EnablePasswordReset }, + { "enablePasswordRetrieval" , membershipProvider.EnablePasswordRetrieval }, { "requiresQuestionAnswer", membershipProvider.RequiresQuestionAndAnswer } //TODO: Inject the other parameters in here to change the behavior of this control - based on the membership provider settings. } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dbf529f4ce..e1747a481f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -311,6 +311,7 @@ + diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs index aabfdcb7b9..44499fd6b2 100644 --- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.WebApi.Binders } //return the new member with the details filled in - return new Member(model.Name, model.Email, model.Username, model.Password, -1, contentType); + return new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, -1, contentType); } protected override ContentItemDto MapFromPersisted(MemberSave model)