diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/SecurityControllerBase.cs index 09b493c48412..1482517a15dc 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 @@ protected IActionResult UserOperationStatusResult(UserOperationStatus status, Er .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 000000000000..9edf9ce26513 --- /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 02a1337007df..8fe2d4bbc72b 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1180,7 +1180,21 @@ private UserOperationStatus ValidateUserUpdateModel(IUser existingUser, UserUpda 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 @@ public async Task> ChangePass 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 @@ public async Task> ChangePass 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 @@ public async Task> CreateInit 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,