diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 0ff2282d36..cd3b179f52 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -594,7 +594,7 @@ public class OrganizationUsersController : Controller var deletionResult = await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); - return deletionResult.result.MapToActionResult(); + return deletionResult.MapToActionResult(); } [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] diff --git a/src/Core/AdminConsole/Errors/BadRequestError.cs b/src/Core/AdminConsole/Errors/BadRequestError.cs new file mode 100644 index 0000000000..71540bc89d --- /dev/null +++ b/src/Core/AdminConsole/Errors/BadRequestError.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.AdminConsole.Errors; + +public record BadRequestError : Error +{ + public BadRequestError(string code, T invalidRequest) + : base(code, invalidRequest) { } +} diff --git a/src/Core/AdminConsole/Errors/Error.cs b/src/Core/AdminConsole/Errors/Error.cs index 6c8eed41a4..7ad057d6ed 100644 --- a/src/Core/AdminConsole/Errors/Error.cs +++ b/src/Core/AdminConsole/Errors/Error.cs @@ -1,3 +1,8 @@ namespace Bit.Core.AdminConsole.Errors; public record Error(string Message, T ErroredValue); + +public static class ErrorMappers +{ + public static Error ToError(this Error errorA, B erroredValue) => new(errorA.Message, erroredValue); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs index 1778a483d9..a114646995 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteManagedOrganizationUserAccountCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Shared.Validation; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -11,103 +11,108 @@ using Bit.Core.Services; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; -using Bit.Core.Validators; -#nullable enable - namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +#nullable enable public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganizationUserAccountCommand { private readonly IUserService _userService; private readonly IEventService _eventService; + private readonly IDeleteManagedOrganizationUserAccountValidator _deleteManagedOrganizationUserAccountValidator; private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IUserRepository _userRepository; private readonly ICurrentContext _currentContext; private readonly IReferenceEventService _referenceEventService; private readonly IPushNotificationService _pushService; - private readonly IProviderUserRepository _providerUserRepository; + public DeleteManagedOrganizationUserAccountCommand( IUserService userService, IEventService eventService, + IDeleteManagedOrganizationUserAccountValidator deleteManagedOrganizationUserAccountValidator, IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery, IOrganizationUserRepository organizationUserRepository, IUserRepository userRepository, ICurrentContext currentContext, IReferenceEventService referenceEventService, - IPushNotificationService pushService, - IProviderUserRepository providerUserRepository - ) + IPushNotificationService pushService) { _userService = userService; _eventService = eventService; + _deleteManagedOrganizationUserAccountValidator = deleteManagedOrganizationUserAccountValidator; _getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery; _organizationUserRepository = organizationUserRepository; _userRepository = userRepository; _currentContext = currentContext; _referenceEventService = referenceEventService; _pushService = pushService; - _providerUserRepository = providerUserRepository; } - public async Task<(Guid OrganizationUserId, CommandResult result)> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) + public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) { var result = await InternalDeleteManyUsersAsync(organizationId, new[] { organizationUserId }, deletingUserId); - return result.FirstOrDefault(); + + if (result.InvalidResults.Count > 0) + { + + var error = result.InvalidResults.FirstOrDefault()?.Errors.FirstOrDefault(); + + return new Failure(); + } + + return new Success(); } public async Task> DeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid? deletingUserId) { - return await InternalDeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId); + var results = await InternalDeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId); } - private async Task> InternalDeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid? deletingUserId) + private async Task> InternalDeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid? deletingUserId) { var orgUsers = await _organizationUserRepository.GetManyAsync(orgUserIds); var users = await GetUsersAsync(orgUsers); - var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds); + var managementStatuses = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds); - var userDeletionRequests = new List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)>(); + var requests = CreateRequests(organizationId, deletingUserId, orgUserIds, orgUsers, users, managementStatuses); + var validationResults = await _deleteManagedOrganizationUserAccountValidator.ValidateAsync(requests); + await CancelPremiumsAsync(validationResults.ValidResults); + await HandleUserDeletionsAsync(validationResults.ValidResults); + await LogDeletedOrganizationUsersAsync(validationResults.ValidResults); + + return validationResults; + } + + private List CreateRequests( + Guid organizationId, + Guid? deletingUserId, + IEnumerable orgUserIds, + ICollection orgUsers, + IEnumerable users, + IDictionary managementStatuses) + { + var requests = new List(); foreach (var orgUserId in orgUserIds) { - var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId); - if (orgUser == null || orgUser.OrganizationId != organizationId) + var orgUser = orgUsers.FirstOrDefault(orgUser => orgUser.Id == orgUserId); + var user = users.FirstOrDefault(user => user.Id == orgUser?.UserId); + managementStatuses.TryGetValue(orgUserId, out var isManaged); + + requests.Add(new DeleteUserValidationRequest { - userDeletionRequests.Add((orgUserId, new BadRequestFailure("Member not found."), null, null)); - continue; - } - - var user = users.FirstOrDefault(u => u.Id == orgUser.UserId); - - if (user == null) - { - userDeletionRequests.Add((orgUserId, new BadRequestFailure("Member not found."), orgUser, null)); - continue; - } - - var result = await ValidateAsync(organizationId, orgUser, user, deletingUserId, managementStatus); - if (result is not Success) - { - userDeletionRequests.Add((orgUserId, result, orgUser, user)); - continue; - } - - await CancelPremiumAsync(user); - - userDeletionRequests.Add((orgUserId, new Success(), orgUser, user)); + User = user, + OrganizationUser = orgUser, + IsManaged = isManaged, + OrganizationId = organizationId, + DeletingUserId = deletingUserId, + }); } - await HandleUserDeletionsAsync(userDeletionRequests); - - await LogDeletedOrganizationUsersAsync(userDeletionRequests); - - return userDeletionRequests - .Select(request => (request.OrganizationUserId, request.result)) - .ToList(); + return requests; } private async Task> GetUsersAsync(ICollection orgUsers) @@ -120,112 +125,12 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz return await _userRepository.GetManyAsync(userIds); } - private async Task ValidateAsync(Guid organizationId, OrganizationUser orgUser, User user, Guid? deletingUserId, IDictionary managementStatus) - { - var validators = new[] - { - () => EnsureUserStatusIsNotInvited(orgUser), - () => PreventSelfDeletion(orgUser, deletingUserId), - () => EnsureUserIsManagedByOrganization(orgUser, managementStatus), - }; - var result = CommandResultValidator.ExecuteValidators(validators); - - if (result is not Success) - { - return result; - } - - var asyncValidators = new[] - { - async () => await EnsureOnlyOwnersCanDeleteOwnersAsync(organizationId, orgUser, deletingUserId), - async () => await EnsureUserIsNotSoleOrganizationOwnerAsync(user), - async () => await EnsureUserIsNotSoleProviderOwnerAsync(user) - }; - var asyncResult = await CommandResultValidator.ExecuteValidatorAsync(asyncValidators); - - if (asyncResult is not Success) - { - return asyncResult; - } - - return new Success(); - } - private static CommandResult EnsureUserStatusIsNotInvited(OrganizationUser orgUser) - { - if (!orgUser.UserId.HasValue || orgUser.Status == OrganizationUserStatusType.Invited) - { - return new BadRequestFailure("You cannot delete a member with Invited status."); - } - - return new Success(); - } - private static CommandResult PreventSelfDeletion(OrganizationUser orgUser, Guid? deletingUserId) - { - if (!orgUser.UserId.HasValue || !deletingUserId.HasValue) - { - return new Success(); - } - if (orgUser.UserId.Value == deletingUserId.Value) - { - return new BadRequestFailure("You cannot delete yourself."); - } - - return new Success(); - } - - private async Task EnsureOnlyOwnersCanDeleteOwnersAsync(Guid organizationId, OrganizationUser orgUser, Guid? deletingUserId) - { - if (orgUser.Type != OrganizationUserType.Owner) - { - return new Success(); - } - - if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(organizationId)) - { - return new BadRequestFailure("Only owners can delete other owners."); - } - - return new Success(); - } - - private static CommandResult EnsureUserIsManagedByOrganization(OrganizationUser orgUser, IDictionary managementStatus) - { - if (!managementStatus.TryGetValue(orgUser.Id, out var isManaged) || !isManaged) - { - return new BadRequestFailure("Member is not managed by the organization."); - } - return new Success(); - } - - private async Task EnsureUserIsNotSoleOrganizationOwnerAsync(User user) - { - var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id); - if (onlyOwnerCount > 0) - { - return new BadRequestFailure("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user."); - } - return new Success(); - } - - private async Task EnsureUserIsNotSoleProviderOwnerAsync(User user) - { - var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id); - if (onlyOwnerProviderCount > 0) - { - return new BadRequestFailure("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user."); - } - return new Success(); - } - - private async Task LogDeletedOrganizationUsersAsync( - List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)> userDeletionRequests) + private async Task LogDeletedOrganizationUsersAsync(List> requests) { var eventDate = DateTime.UtcNow; - var events = userDeletionRequests - .Where(request => - request.result is Success) - .Select(request => (request.orgUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate)) + var events = requests + .Select(request => (request.Value.OrganizationUser!, (EventType)EventType.OrganizationUser_Deleted, (DateTime?)eventDate)) .ToList(); if (events.Any()) @@ -234,16 +139,15 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz } } - private async Task HandleUserDeletionsAsync(List<(Guid OrganizationUserId, CommandResult result, OrganizationUser? orgUser, User? user)> userDeletionRequests) - { - var usersToDelete = userDeletionRequests - .Where(request => - request.result is Success) - .Select(request => request.user!); - if (usersToDelete.Any()) + private async Task HandleUserDeletionsAsync(List> requests) + { + var users = requests + .Select(request => request.Value.User!); + + if (users.Any()) { - await DeleteManyAsync(usersToDelete); + await DeleteManyAsync(users); } } @@ -259,16 +163,23 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz } - private async Task CancelPremiumAsync(User user) + private async Task CancelPremiumsAsync(List> requests) { - if (string.IsNullOrWhiteSpace(user.GatewaySubscriptionId)) + var users = requests + .Select(request => request.Value.User!); + + foreach (var user in users) { - return; + try + { + await _userService.CancelPremiumAsync(user); + } + catch (GatewayException) + { + + } } - try - { - await _userService.CancelPremiumAsync(user); - } - catch (GatewayException) { } } + } + diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteUserValidationRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteUserValidationRequest.cs new file mode 100644 index 0000000000..0861cb2fbd --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteUserValidationRequest.cs @@ -0,0 +1,15 @@ +using Bit.Core.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +#nullable enable + +public class DeleteUserValidationRequest +{ + public Guid OrganizationId { get; init; } + public OrganizationUser? OrganizationUser { get; init; } + public User? User { get; init; } + public Guid? DeletingUserId { get; init; } + + public IDictionary? ManagementStatus { get; init; } + public bool? IsManaged { get; init; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteManagedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteManagedOrganizationUserAccountCommand.cs index 713ef13e66..8afd208ab5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteManagedOrganizationUserAccountCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteManagedOrganizationUserAccountCommand.cs @@ -9,7 +9,8 @@ public interface IDeleteManagedOrganizationUserAccountCommand /// /// Removes a user from an organization and deletes all of their associated user data. /// - Task<(Guid OrganizationUserId, CommandResult result)> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); + /// Jimmy temporary comment: consider removing the nullable from deletingUserId. + Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); /// /// Removes multiple users from an organization and deletes all of their associated user data. @@ -17,5 +18,6 @@ public interface IDeleteManagedOrganizationUserAccountCommand /// /// An error message for each user that could not be removed, otherwise null. /// + /// Jimmy temporary comment: consider removing the nullable from deletingUserId. Task> DeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid? deletingUserId); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteManagedOrganizationUserAccountValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteManagedOrganizationUserAccountValidator.cs new file mode 100644 index 0000000000..4eb03c7629 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteManagedOrganizationUserAccountValidator.cs @@ -0,0 +1,8 @@ +using Bit.Core.AdminConsole.Shared.Validation; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IDeleteManagedOrganizationUserAccountValidator +{ + Task> ValidateAsync(List requests); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/DeleteManagedOrganizationUserAccountValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/DeleteManagedOrganizationUserAccountValidator.cs new file mode 100644 index 0000000000..f635b1c2ee --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/DeleteManagedOrganizationUserAccountValidator.cs @@ -0,0 +1,160 @@ +using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser; + +public class DeleteManagedOrganizationUserAccountValidator( + ICurrentContext currentContext, + IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository) : IDeleteManagedOrganizationUserAccountValidator +{ + public async Task> ValidateAsync(List requests) + { + var invalidResults = new List>(); + var validResults = new List>(); + foreach (var request in requests) + { + // The order of the validators matters. + // Earlier validators assert nullable properties so that later validators don’t have to. + var validators = new[] + { + EnsureUserBelongsToOrganization, + EnsureUserStatusIsNotInvited, + PreventSelfDeletion, + EnsureUserIsManagedByOrganization + }; + + var asyncValidators = new[] + { + EnsureOnlyOwnersCanDeleteOwnersAsync, + EnsureUserIsNotSoleOrganizationOwnerAsync, + EnsureUserIsNotSoleProviderOwnerAsync + }; + + var result = await ExecuteValidatorsAsync(validators, asyncValidators, request); + + if (result is Valid valid) + { + validResults.Add(valid); + } + else + { + invalidResults.Add((Invalid)result); + } + } + + return new PartialValidationResult() + { + InvalidResults = invalidResults, + ValidResults = validResults + }; + } + + private static async Task> ExecuteValidatorsAsync( + Func>[] validators, + Func>>[] asyncValidators, + DeleteUserValidationRequest request) + { + foreach (var validator in validators) + { + var result = validator(request); + + if (result is Invalid) + { + return result; + } + } + + foreach (var asyncValidator in asyncValidators) + { + var result = await asyncValidator(request); + + if (result is Invalid) + { + return result; + } + } + + return new Valid(request); + } + + private static ValidationResult EnsureUserBelongsToOrganization(DeleteUserValidationRequest request) + { + if (request.User == null || request.OrganizationUser == null) + { + return new Invalid(new BadRequestError("You cannot delete a member with Invited status.", request)); + } + + return new Valid(); + } + + private static ValidationResult EnsureUserIsManagedByOrganization(DeleteUserValidationRequest request) + { + if (request.IsManaged == true) + { + return new Valid(); + } + return new Invalid(new BadRequestError("Member is not managed by the organization.", request)); + } + + private static ValidationResult EnsureUserStatusIsNotInvited(DeleteUserValidationRequest request) + { + if (request.OrganizationUser!.Status == OrganizationUserStatusType.Invited) + { + return new Invalid(new BadRequestError("You cannot delete a member with Invited status.", request)); + } + + return new Valid(); + } + + private static ValidationResult PreventSelfDeletion(DeleteUserValidationRequest request) + { + if (request.OrganizationUser?.UserId == request.DeletingUserId) + { + return new Invalid(new BadRequestError("You cannot delete yourself.", request)); + } + + return new Valid(request); + } + + private async Task> EnsureOnlyOwnersCanDeleteOwnersAsync(DeleteUserValidationRequest request) + { + if (request.OrganizationUser?.Type != OrganizationUserType.Owner) + { + return new Valid(request); + } + + if (request.DeletingUserId.HasValue && !await currentContext.OrganizationOwner(request.OrganizationId)) + { + return new Invalid(new BadRequestError("Only owners can delete other owners.", request)); + } + + return new Valid(request); + } + + private async Task> EnsureUserIsNotSoleOrganizationOwnerAsync(DeleteUserValidationRequest request) + { + var onlyOwnerCount = await organizationUserRepository.GetCountByOnlyOwnerAsync(request.User!.Id); + if (onlyOwnerCount > 0) + { + return new Invalid(new BadRequestError("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.", request)); + } + return new Valid(request); + } + + private async Task> EnsureUserIsNotSoleProviderOwnerAsync(DeleteUserValidationRequest request) + { + var onlyOwnerProviderCount = await providerUserRepository.GetCountByOnlyOwnerAsync(request.User!.Id); + if (onlyOwnerProviderCount > 0) + { + return new Invalid(new BadRequestError("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.", request)); + } + + return new Valid(request); + } +} diff --git a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs b/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs index e25103e701..f00ae82892 100644 --- a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs +++ b/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs @@ -6,10 +6,35 @@ public abstract record ValidationResult; public record Valid : ValidationResult { + public Valid() { } + + public Valid(T Value) + { + this.Value = Value; + } + public T Value { get; init; } } +public record PartialValidationResult +{ + public List> InvalidResults { get; init; } + + public List> ValidResults { get; init; } +} + public record Invalid : ValidationResult { - public IEnumerable> Errors { get; init; } + public IEnumerable> Errors { get; init; } = []; + + public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message)); + + public Invalid() { } + + public Invalid(Error error) : this([error]) { } + + public Invalid(IEnumerable> errors) + { + Errors = errors; + } } diff --git a/src/Core/AdminConsole/Shared/Validation/ValidationResultMappers.cs b/src/Core/AdminConsole/Shared/Validation/ValidationResultMappers.cs new file mode 100644 index 0000000000..6214bc9b43 --- /dev/null +++ b/src/Core/AdminConsole/Shared/Validation/ValidationResultMappers.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.AdminConsole.Shared.Validation; + +public static class ValidationResultMappers +{ + // public static Failure MapToFailure(this Error error) + // { + // + // return error switch + // { + // BadRequestError badRequestError => new Failure(badRequestError.Message), + // _ => throw new InvalidOperationException($"Unhandled commandResult type: {error.GetType().Name}") + // } + // // return commandResult switch + // // { + // // NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, + // // BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + // // Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, + // // Success success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK }, + // // _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") + // // }; + // } +}