From 8cae995e9b613dd6491debf02147add061c26941 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Thu, 23 Sep 2021 10:58:14 +0200 Subject: [PATCH] password change for user --- zero.Core/Api/UserApi.cs | 76 +++++++++++++++- zero.Web.UI/App/components/forms/error.vue | 9 +- .../App/components/forms/form-header.vue | 2 +- zero.Web.UI/App/components/forms/property.vue | 7 +- .../App/components/messages/message.vue | 2 +- zero.Web.UI/App/editor/editor.scss | 2 +- zero.Web.UI/App/pages/password-change.vue | 90 ------------------- zero.Web.UI/App/pages/settings/user.vue | 6 +- zero.Web.UI/App/renderers/editors/user.js | 6 +- zero.Web.UI/Sass/Modules/_button.scss | 4 +- zero.Web.UI/app/api/users.js | 2 + .../components/overlays/password-change.vue | 82 +++++++++++++++++ zero.Web.UI/app/core/editor-field.ts | 11 +++ .../app/editor/fields/password-hash.vue | 54 +++++++++++ zero.Web.UI/app/renderers/editors/password.js | 10 +++ zero.Web/Controllers/UsersController.cs | 8 ++ zero.Web/Models/UserPasswordEditModel.cs | 2 + .../Resources/Localization/zero.en-us.json | 5 +- 18 files changed, 275 insertions(+), 103 deletions(-) delete mode 100644 zero.Web.UI/App/pages/password-change.vue create mode 100644 zero.Web.UI/app/components/overlays/password-change.vue create mode 100644 zero.Web.UI/app/editor/fields/password-hash.vue create mode 100644 zero.Web.UI/app/renderers/editors/password.js diff --git a/zero.Core/Api/UserApi.cs b/zero.Core/Api/UserApi.cs index e50592e1..918c5991 100644 --- a/zero.Core/Api/UserApi.cs +++ b/zero.Core/Api/UserApi.cs @@ -72,7 +72,22 @@ namespace zero.Core.Api /// public async Task> Save(BackofficeUser model) { - return await SaveModel(model); //, new UserValidator()); + bool updateSecurityStamp = false; + + if (!model.Id.IsNullOrEmpty()) + { + BackofficeUser origin = await GetUserById(model.Id); + updateSecurityStamp = origin != null && model.PasswordHash != origin.PasswordHash; + } + + EntityResult result = await SaveModel(model); //, new UserValidator()); + + if (updateSecurityStamp) + { + await UserManager.UpdateSecurityStampAsync(model); + } + + return result; } @@ -83,6 +98,60 @@ namespace zero.Core.Api } + /// + public async Task> HashPassword(BackofficeUser user, string currentPassword, string newPassword, string confirmNewPassword) + { + if (newPassword != confirmNewPassword) + { + return EntityResult.Fail(nameof(newPassword), "@errors.changepassword.newpasswordsnotmatching"); + } + + if (currentPassword.IsNullOrWhiteSpace() || newPassword.IsNullOrWhiteSpace()) + { + return EntityResult.Fail(nameof(currentPassword), "@errors.changepassword.emptyfields"); + } + + if (user == null) + { + return EntityResult.Fail("@errors.changepassword.nouser"); + } + + if (UserManager.PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, currentPassword) != PasswordVerificationResult.Success) + { + return EntityResult.Fail("@errors.changepassword.passwordincorrect"); + } + + // validate new password + List errors = new(); + bool isValid = true; + foreach (var v in UserManager.PasswordValidators) + { + var result = await v.ValidateAsync(UserManager, user, newPassword); + if (!result.Succeeded) + { + if (result.Errors.Any()) + { + errors.AddRange(result.Errors); + } + + isValid = false; + } + } + + if (!isValid) + { + EntityResult result = EntityResult.Fail(); + foreach (IdentityError error in errors) + { + result.AddError(error.Description); + } + return result; + } + + return EntityResult.Success(UserManager.PasswordHasher.HashPassword(user, newPassword)); + } + + /// public async Task> UpdatePassword(BackofficeUser user, string currentPassword, string newPassword) { @@ -200,6 +269,11 @@ namespace zero.Core.Api /// Task> UpdatePassword(BackofficeUser user, string currentPassword, string newPassword); + /// + /// Tries to hash a new password + /// + Task> HashPassword(BackofficeUser user, string currentPassword, string newPassword, string confirmNewPassword); + /// /// Enables a user /// diff --git a/zero.Web.UI/App/components/forms/error.vue b/zero.Web.UI/App/components/forms/error.vue index b21c6f63..fc9aa137 100644 --- a/zero.Web.UI/App/components/forms/error.vue +++ b/zero.Web.UI/App/components/forms/error.vue @@ -84,4 +84,11 @@ } } } - \ No newline at end of file + + + \ No newline at end of file diff --git a/zero.Web.UI/App/components/forms/form-header.vue b/zero.Web.UI/App/components/forms/form-header.vue index a0fc805e..9a45505e 100644 --- a/zero.Web.UI/App/components/forms/form-header.vue +++ b/zero.Web.UI/App/components/forms/form-header.vue @@ -24,7 +24,7 @@ - + diff --git a/zero.Web.UI/App/components/forms/property.vue b/zero.Web.UI/App/components/forms/property.vue index bad217e9..6a2712b7 100644 --- a/zero.Web.UI/App/components/forms/property.vue +++ b/zero.Web.UI/App/components/forms/property.vue @@ -63,6 +63,11 @@ { } + + .ui-error + { + margin-top: 5px; + } } .ui-property + .ui-split, @@ -220,7 +225,7 @@ { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); - grid-gap: var(--padding) var(--padding-s); + grid-gap: var(--padding) var(--padding-m); > .ui-property { diff --git a/zero.Web.UI/App/components/messages/message.vue b/zero.Web.UI/App/components/messages/message.vue index 5e147fdc..82258fc0 100644 --- a/zero.Web.UI/App/components/messages/message.vue +++ b/zero.Web.UI/App/components/messages/message.vue @@ -73,7 +73,7 @@ padding: 8px 12px 7px 12px; grid-template-columns: auto 1fr; gap: 12px; - border-radius: var(--radius); + border-radius: var(--radius-inner); position: relative; line-height: 20px; text-align: left; diff --git a/zero.Web.UI/App/editor/editor.scss b/zero.Web.UI/App/editor/editor.scss index ba10660d..4c944ff9 100644 --- a/zero.Web.UI/App/editor/editor.scss +++ b/zero.Web.UI/App/editor/editor.scss @@ -78,7 +78,7 @@ .editor-infos { display: block; - border-top: 1px solid var(--color-line-onbg); + //border-top: 1px solid var(--color-line-onbg); } .theme-dark .editor-infos diff --git a/zero.Web.UI/App/pages/password-change.vue b/zero.Web.UI/App/pages/password-change.vue deleted file mode 100644 index c1da0b0c..00000000 --- a/zero.Web.UI/App/pages/password-change.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/zero.Web.UI/App/pages/settings/user.vue b/zero.Web.UI/App/pages/settings/user.vue index ceca6a1a..e33fb951 100644 --- a/zero.Web.UI/App/pages/settings/user.vue +++ b/zero.Web.UI/App/pages/settings/user.vue @@ -4,7 +4,6 @@ @@ -29,6 +28,7 @@ + + \ No newline at end of file diff --git a/zero.Web.UI/app/core/editor-field.ts b/zero.Web.UI/app/core/editor-field.ts index f881cd69..a27990f4 100644 --- a/zero.Web.UI/app/core/editor-field.ts +++ b/zero.Web.UI/app/core/editor-field.ts @@ -201,6 +201,17 @@ class EditorField } + /** + * Render a password hash field + * @param {number} [maxLength] - Maximum length of the password + * @returns {EditorField} + */ + passwordHash(maxLength) + { + return this._setComponent(() => import('../editor/fields/password-hash.vue'), { maxLength }); + } + + /** * Render a currency input field * @param {string|function} [placeholder] - Placeholder text (can be a translation) or function diff --git a/zero.Web.UI/app/editor/fields/password-hash.vue b/zero.Web.UI/app/editor/fields/password-hash.vue new file mode 100644 index 00000000..ef7f9c00 --- /dev/null +++ b/zero.Web.UI/app/editor/fields/password-hash.vue @@ -0,0 +1,54 @@ + + + + + + \ No newline at end of file diff --git a/zero.Web.UI/app/renderers/editors/password.js b/zero.Web.UI/app/renderers/editors/password.js new file mode 100644 index 00000000..6b7a4196 --- /dev/null +++ b/zero.Web.UI/app/renderers/editors/password.js @@ -0,0 +1,10 @@ + +import Editor from 'zero/core/editor.ts'; + +const editor = new Editor('media', '@changepasswordoverlay.fields.'); + +editor.field('currentPassword').password().required(); +editor.field('newPassword').password().required(); +editor.field('confirmNewPassword').password().required(); + +export default editor; \ No newline at end of file diff --git a/zero.Web/Controllers/UsersController.cs b/zero.Web/Controllers/UsersController.cs index 0a29435d..622e9922 100644 --- a/zero.Web/Controllers/UsersController.cs +++ b/zero.Web/Controllers/UsersController.cs @@ -77,6 +77,14 @@ namespace zero.Web.Controllers } + [ZeroAuthorize] + public async Task> HashPassword([FromBody] UserPasswordEditModel model) + { + BackofficeUser user = await Api.GetUserById(model.UserId); + return await Api.HashPassword(user, model.CurrentPassword, model.NewPassword, model.ConfirmNewPassword); + } + + [ZeroAuthorize(Permissions.Settings.Users, PermissionsValue.Update)] public async Task> Disable([FromBody] BackofficeUser model) { diff --git a/zero.Web/Models/UserPasswordEditModel.cs b/zero.Web/Models/UserPasswordEditModel.cs index 665c02f6..296bbc66 100644 --- a/zero.Web/Models/UserPasswordEditModel.cs +++ b/zero.Web/Models/UserPasswordEditModel.cs @@ -2,6 +2,8 @@ { public class UserPasswordEditModel { + public string UserId { get; set; } + public string CurrentPassword { get; set; } public string NewPassword { get; set; } diff --git a/zero.Web/Resources/Localization/zero.en-us.json b/zero.Web/Resources/Localization/zero.en-us.json index a7fefddf..678a7877 100644 --- a/zero.Web/Resources/Localization/zero.en-us.json +++ b/zero.Web/Resources/Localization/zero.en-us.json @@ -195,7 +195,8 @@ "changepassword": { "emptyfields": "Please fill out all fields", "nouser": "User is invalid", - "newpasswordsnotmatching": "The new password does not match the confirmation" + "newpasswordsnotmatching": "The new password does not match the confirmation", + "passwordincorrect": "The current password for this user is wrong" }, "preview": { "notfound": "Not found", @@ -328,6 +329,8 @@ "email": "Email", "email_text": "Also used as login username", "email_placeholder": "Enter your email address", + "passwordHash": "Password", + "passwordHash_text": "Password used for login", "avatarId": "Avatar", "avatarId_text": "Upload a user image", "languageId": "Language",