Bulk delete functionality for management api (#14735)
* Bulk delete * Bulk delete * Added bulk delete user groups * Clean --------- Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.User;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.User;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class BulkDeleteUsersController : UserControllerBase
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public BulkDeleteUsersController(IUserService userService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_userService = userService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> DeleteUsers(DeleteUsersRequestModel model)
|
||||
{
|
||||
UserOperationStatus result = await _userService.DeleteAsync(CurrentUserKey(_backOfficeSecurityAccessor), model.UserIds);
|
||||
|
||||
return result is UserOperationStatus.Success
|
||||
? Ok()
|
||||
: UserOperationStatusResult(result);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
@@ -8,15 +9,20 @@ namespace Umbraco.Cms.Api.Management.Controllers.User;
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteUserController : UserControllerBase
|
||||
{
|
||||
public DeleteUserController(IUserService userService) => _userService = userService;
|
||||
public DeleteUserController(IUserService userService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_userService = userService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
private readonly IUserService _userService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> DeleteUser(Guid id)
|
||||
{
|
||||
UserOperationStatus result = await _userService.DeleteAsync(id);
|
||||
UserOperationStatus result = await _userService.DeleteAsync(CurrentUserKey(_backOfficeSecurityAccessor), id);
|
||||
|
||||
return result is UserOperationStatus.Success
|
||||
? Ok()
|
||||
|
||||
@@ -64,6 +64,10 @@ public abstract class UserControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Cannot disable")
|
||||
.WithDetail("A user cannot disable itself.")
|
||||
.Build()),
|
||||
UserOperationStatus.CannotDeleteSelf => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Cannot delete")
|
||||
.WithDetail("A user cannot delete itself.")
|
||||
.Build()),
|
||||
UserOperationStatus.OldPasswordRequired => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Old password required")
|
||||
.WithDetail("The old password is required to change the password of the specified user.")
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroup;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class BulkDeleteUserGroupsController : UserGroupControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
|
||||
public BulkDeleteUserGroupsController(IUserGroupService userGroupService)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> BulkDelete(DeleteUserGroupsRequestModel model)
|
||||
{
|
||||
Attempt<UserGroupOperationStatus> result = await _userGroupService.DeleteAsync(model.UserGroupIds);
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: UserGroupOperationStatusResult(result.Result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.User;
|
||||
|
||||
public class DeleteUsersRequestModel
|
||||
{
|
||||
public HashSet<Guid> UserIds { get; set; } = new();
|
||||
}
|
||||
@@ -3,4 +3,4 @@
|
||||
public class DisableUserRequestModel
|
||||
{
|
||||
public HashSet<Guid> UserIds { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup;
|
||||
|
||||
public class DeleteUserGroupsRequestModel
|
||||
{
|
||||
public HashSet<Guid> UserGroupIds { get; set; } = new();
|
||||
}
|
||||
@@ -12,4 +12,9 @@ public sealed class UserGroupDeletedNotification : DeletedNotification<IUserGrou
|
||||
: base(target, messages)
|
||||
{
|
||||
}
|
||||
|
||||
public UserGroupDeletedNotification(IEnumerable<IUserGroup> target, EventMessages messages)
|
||||
: base(target, messages)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,10 @@ public interface IUserGroupService
|
||||
/// <summary>
|
||||
/// Deletes a UserGroup
|
||||
/// </summary>
|
||||
/// <param name="key">The key of the user group to delete.</param>
|
||||
/// <param name="userGroupKeys">The keys of the user groups to delete.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
Task<Attempt<UserGroupOperationStatus>> DeleteAsync(Guid key);
|
||||
Task<Attempt<UserGroupOperationStatus>> DeleteAsync(ISet<Guid> userGroupKeys);
|
||||
Task<Attempt<UserGroupOperationStatus>> DeleteAsync(Guid userGroupKey) => DeleteAsync(new HashSet<Guid>(){userGroupKey});
|
||||
|
||||
/// <summary>
|
||||
/// Updates the users to have the groups specified.
|
||||
|
||||
@@ -67,7 +67,9 @@ public interface IUserService : IMembershipUserService
|
||||
|
||||
Task<UserOperationStatus> SetAvatarAsync(Guid userKey, Guid temporaryFileKey);
|
||||
|
||||
Task<UserOperationStatus> DeleteAsync(Guid key);
|
||||
Task<UserOperationStatus> DeleteAsync(Guid userKey, ISet<Guid> keys);
|
||||
|
||||
Task<UserOperationStatus> DeleteAsync(Guid userKey, Guid key) => DeleteAsync(userKey, new HashSet<Guid> { key });
|
||||
|
||||
Task<UserOperationStatus> DisableAsync(Guid userKey, ISet<Guid> keys);
|
||||
|
||||
|
||||
@@ -18,5 +18,5 @@ public enum UserGroupOperationStatus
|
||||
LanguageNotFound,
|
||||
NameTooLong,
|
||||
AliasTooLong,
|
||||
MissingName,
|
||||
MissingName
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public enum UserOperationStatus
|
||||
CannotInvite,
|
||||
CannotDelete,
|
||||
CannotDisableSelf,
|
||||
CannotDeleteSelf,
|
||||
CannotDisableInvitedUser,
|
||||
OldPasswordRequired,
|
||||
InvalidAvatar,
|
||||
|
||||
@@ -135,35 +135,48 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Attempt<UserGroupOperationStatus>> DeleteAsync(Guid key)
|
||||
public async Task<Attempt<UserGroupOperationStatus>> DeleteAsync(ISet<Guid> keys)
|
||||
{
|
||||
IUserGroup? userGroup = await GetAsync(key);
|
||||
|
||||
Attempt<UserGroupOperationStatus> validationResult = ValidateUserGroupDeletion(userGroup);
|
||||
if (validationResult.Success is false)
|
||||
if (keys.Any() is false)
|
||||
{
|
||||
return validationResult;
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
IUserGroup[] userGroupsToDelete = (await GetAsync(keys)).ToArray();
|
||||
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
if (userGroupsToDelete.Length != keys.Count)
|
||||
{
|
||||
var deletingNotification = new UserGroupDeletingNotification(userGroup!, eventMessages);
|
||||
return Attempt.Fail(UserGroupOperationStatus.NotFound);
|
||||
}
|
||||
|
||||
if (await scope.Notifications.PublishCancelableAsync(deletingNotification))
|
||||
foreach (IUserGroup userGroup in userGroupsToDelete)
|
||||
{
|
||||
Attempt<UserGroupOperationStatus> validationResult = ValidateUserGroupDeletion(userGroup);
|
||||
if (validationResult.Success is false)
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.Fail(UserGroupOperationStatus.CancelledByNotification);
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
_userGroupRepository.Delete(userGroup!);
|
||||
|
||||
scope.Notifications.Publish(new UserGroupDeletedNotification(userGroup!, eventMessages).WithStateFrom(deletingNotification));
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
var deletingNotification = new UserGroupDeletingNotification(userGroupsToDelete, eventMessages);
|
||||
|
||||
if (await scope.Notifications.PublishCancelableAsync(deletingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.Fail(UserGroupOperationStatus.CancelledByNotification);
|
||||
}
|
||||
|
||||
foreach (IUserGroup userGroup in userGroupsToDelete)
|
||||
{
|
||||
_userGroupRepository.Delete(userGroup);
|
||||
}
|
||||
|
||||
scope.Notifications.Publish(new UserGroupDeletedNotification(userGroupsToDelete, eventMessages).WithStateFrom(deletingNotification));
|
||||
|
||||
scope.Complete();
|
||||
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
|
||||
@@ -1324,24 +1324,49 @@ internal class UserService : RepositoryService, IUserService
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<UserOperationStatus> DeleteAsync(Guid key)
|
||||
public async Task<UserOperationStatus> DeleteAsync(Guid userKey, ISet<Guid> keys)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
IUser? user = await GetAsync(key);
|
||||
if(keys.Any() is false)
|
||||
{
|
||||
return UserOperationStatus.Success;
|
||||
}
|
||||
|
||||
if (user is null)
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
IUser? performingUser = await GetAsync(userKey);
|
||||
|
||||
if (performingUser is null)
|
||||
{
|
||||
return UserOperationStatus.MissingUser;
|
||||
}
|
||||
|
||||
if (keys.Contains(performingUser.Key))
|
||||
{
|
||||
return UserOperationStatus.CannotDeleteSelf;
|
||||
}
|
||||
|
||||
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
||||
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
||||
IUser[] usersToDisable = (await userStore.GetUsersAsync(keys.ToArray())).ToArray();
|
||||
|
||||
if (usersToDisable.Length != keys.Count)
|
||||
{
|
||||
return UserOperationStatus.UserNotFound;
|
||||
}
|
||||
|
||||
// Check user hasn't logged in. If they have they may have made content changes which will mean
|
||||
// the Id is associated with audit trails, versions etc. and can't be removed.
|
||||
if (user.LastLoginDate is not null && user.LastLoginDate != default(DateTime))
|
||||
foreach (IUser user in usersToDisable)
|
||||
{
|
||||
return UserOperationStatus.CannotDelete;
|
||||
}
|
||||
// Check user hasn't logged in. If they have they may have made content changes which will mean
|
||||
// the Id is associated with audit trails, versions etc. and can't be removed.
|
||||
if (user.LastLoginDate is not null && user.LastLoginDate != default(DateTime))
|
||||
{
|
||||
return UserOperationStatus.CannotDelete;
|
||||
}
|
||||
|
||||
Delete(user, true);
|
||||
user.IsApproved = false;
|
||||
user.InvitedDate = null;
|
||||
|
||||
Delete(user, true);
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
return UserOperationStatus.Success;
|
||||
|
||||
@@ -12,7 +12,7 @@ public partial class UserServiceCrudTests
|
||||
public async Task Delete_Returns_Not_Found_If_Not_Found()
|
||||
{
|
||||
var userService = CreateUserService();
|
||||
var result = await userService.DeleteAsync(Guid.NewGuid());
|
||||
var result = await userService.DeleteAsync(Constants.Security.SuperUserKey, Guid.NewGuid());
|
||||
Assert.AreEqual(UserOperationStatus.UserNotFound, result);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class UserServiceCrudTests
|
||||
createdUser!.LastLoginDate = DateTime.Now;
|
||||
userService.Save(createdUser);
|
||||
|
||||
var result = await userService.DeleteAsync(createdUser.Key);
|
||||
var result = await userService.DeleteAsync(Constants.Security.SuperUserKey, createdUser.Key);
|
||||
Assert.AreEqual(UserOperationStatus.CannotDelete, result);
|
||||
|
||||
// Asset that it is in fact not deleted
|
||||
@@ -61,7 +61,7 @@ public partial class UserServiceCrudTests
|
||||
var creationResult = await userService.CreateAsync(Constants.Security.SuperUserKey, userCreateModel, true);
|
||||
Assert.IsTrue(creationResult.Success);
|
||||
|
||||
var deletionResult = await userService.DeleteAsync(creationResult.Result.CreatedUser!.Key);
|
||||
var deletionResult = await userService.DeleteAsync(Constants.Security.SuperUserKey, creationResult.Result.CreatedUser!.Key);
|
||||
Assert.AreEqual(UserOperationStatus.Success, deletionResult);
|
||||
// Make sure it's actually deleted
|
||||
var postDeletedUser = await userService.GetAsync(creationResult.Result.CreatedUser.Key);
|
||||
|
||||
Reference in New Issue
Block a user