From 76f27726c51ec0c889a90047d51fe6733cae920c Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 9 Apr 2025 08:28:12 +0100 Subject: [PATCH] V15: User password resetting notification (#18679) * Introduce UserPasswordResettingNotification * Removed changes to IUserService interface. --------- Co-authored-by: Andy Butland --- .../Security/SecurityControllerBase.cs | 4 ++ .../UserPasswordResettingNotification.cs | 13 +++++ src/Umbraco.Core/Services/UserService.cs | 52 ++++++++++++++----- 3 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs index 09b493c484..1482517a15 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs @@ -21,6 +21,10 @@ public abstract class SecurityControllerBase : ManagementApiControllerBase .WithTitle("The password reset token was invalid") .WithDetail("The specified password reset token was either used already or wrong.") .Build()), + UserOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder + .WithTitle("Cancelled by notification") + .WithDetail("A notification handler prevented the user operation.") + .Build()), UserOperationStatus.UnknownFailure => BadRequest(problemDetailsBuilder .WithTitle("Unknown failure") .WithDetail(errorMessageResult?.Error?.ErrorMessage ?? "The error was unknown") diff --git a/src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs b/src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs new file mode 100644 index 0000000000..9edf9ce265 --- /dev/null +++ b/src/Umbraco.Core/Notifications/UserPasswordResettingNotification.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Core.Notifications; + +public class UserPasswordResettingNotification : CancelableObjectNotification +{ + public UserPasswordResettingNotification(IUser target, EventMessages messages) : base(target, messages) + { + } + + public IUser User => Target; +} diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 02a1337007..8fe2d4bbc7 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1180,7 +1180,21 @@ internal partial class UserService : RepositoryService, IUserService return keys; } + /// public async Task> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model) + { + IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); + IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService(); + IUser? performingUser = await userStore.GetAsync(performingUserKey); + if (performingUser is null) + { + return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel()); + } + + return await ChangePasswordAsync(performingUser, model); + } + + private async Task> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model) { IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); using ICoreScope scope = ScopeProvider.CreateCoreScope(); @@ -1197,12 +1211,6 @@ internal partial class UserService : RepositoryService, IUserService return Attempt.FailWithStatus(UserOperationStatus.InvalidUserType, new PasswordChangedModel()); } - IUser? performingUser = await userStore.GetAsync(performingUserKey); - if (performingUser is null) - { - return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel()); - } - // require old password for self change when outside of invite or resetByToken flows if (performingUser.UserState != UserState.Invited && performingUser.Username == user.Username && string.IsNullOrEmpty(model.OldPassword) && string.IsNullOrEmpty(model.ResetPasswordToken)) { @@ -1226,12 +1234,13 @@ internal partial class UserService : RepositoryService, IUserService IBackOfficePasswordChanger passwordChanger = serviceScope.ServiceProvider.GetRequiredService(); Attempt result = await passwordChanger.ChangeBackOfficePassword( new ChangeBackOfficeUserPasswordModel - { - NewPassword = model.NewPassword, - OldPassword = model.OldPassword, - User = user, - ResetPasswordToken = model.ResetPasswordToken, - }, performingUser); + { + NewPassword = model.NewPassword, + OldPassword = model.OldPassword, + User = user, + ResetPasswordToken = model.ResetPasswordToken, + }, + performingUser); if (result.Success is false) { @@ -2184,9 +2193,26 @@ internal partial class UserService : RepositoryService, IUserService public async Task> ResetPasswordAsync(Guid userKey, string token, string password) { using ICoreScope scope = ScopeProvider.CreateCoreScope(); + IServiceScope serviceScope = _serviceScopeFactory.CreateScope(); + + EventMessages evtMsgs = EventMessagesFactory.Get(); + IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService(); + + IUser? user = await userStore.GetAsync(userKey); + if (user is null) + { + return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel()); + } + + var savingNotification = new UserPasswordResettingNotification(user, evtMsgs); + if (await scope.Notifications.PublishCancelableAsync(savingNotification)) + { + scope.Complete(); + return Attempt.FailWithStatus(UserOperationStatus.CancelledByNotification, new PasswordChangedModel()); + } Attempt changePasswordAttempt = - await ChangePasswordAsync(userKey, new ChangeUserPasswordModel + await ChangePasswordAsync(user, new ChangeUserPasswordModel { NewPassword = password, UserKey = userKey,