Repository: OrchardCMS/OrchardCore
Pull request #17756: Don't call IEnumerable<IUserEventHandler> from UserDisplayDriver
Author: login: hishamco
Created: 2025-04-07T07:43:24Z | Merged: 2025-04-26T00:22:17Z

Fixes #17072

Linked issue #17072: IEnumerable<IUserEventHandler> should not be called by UserDisplayDriver
I noticed that we resolve `IEnumerable<IUserEventHandler>` in [UserDisplayDriver](https://github.com/OrchardCMS/OrchardCore/blob/1cc5fdd7a63a4d62b0f2c437a478c67a0ab2caae/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs#L36). I don't think this handler should by called by another hander like a display driver.

https://github.com/OrchardCMS/OrchardCore/blob/1cc5fdd7a63a4d62b0f2c437a478c67a0ab2caae/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs#L36

We should be able to identify a user in Enable/Disabled from a controller or the user store instead of a driver.
Linked issue comments: user: login: hishamco
created_at: 2024-12-04T23:59:31Z
body: @MikeAlhayek I'm working on a PR right now, I'm not sure why we introduced `IEnumerable<IUserEventHandler>` here
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2024-12-12T18:47:03Z
body: We triaged this issue and set the milestone according to the priority we think is appropriate (see  [the docs](https://docs.orchardcore.net/en/latest/docs/guides/contributing/managing-issues/) on how we  triage and prioritize issues).

This indicates when the core team may start working on it. However, if you'd like to contribute, we'd  warmly welcome you to do that anytime. See our guide on contributions  [here](https://docs.orchardcore.net/en/latest/docs/guides/contributing/).

author_association: CONTRIBUTOR

Diff:
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
index 9bdd66c3bb8..ed7b7917b7e 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
@@ -258,16 +258,14 @@ public async Task<ActionResult> IndexPOST(UserIndexOptions options, IEnumerable<
                     case UsersBulkAction.Disable:
                         if (!isSameUser && canEditUser)
                         {
-                            user.IsEnabled = false;
-                            await _userManager.UpdateAsync(user);
+                            await _userService.DisableAsync(user);
                             await _notifier.SuccessAsync(H["User {0} successfully disabled.", user.UserName]);
                         }
                         break;
                     case UsersBulkAction.Enable:
                         if (!isSameUser && canEditUser)
                         {
-                            user.IsEnabled = true;
-                            await _userManager.UpdateAsync(user);
+                            await _userService.EnableAsync(user);
                             await _notifier.SuccessAsync(H["User {0} successfully enabled.", user.UserName]);
                         }
                         break;
@@ -561,6 +559,56 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User account was successfully enabled."]);
+        }
+        else
+        {
+            await _notifier.ErrorAsync(H["Could not enable the user."]);
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> Disable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.DisableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User account was successfully disabled."]);
+        }
+        else
+        {
+            await _notifier.ErrorAsync(H["Could not disable the user."]);
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
     [HttpPost]
     public async Task<IActionResult> Unlock(string id)
     {
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs
index 1ac4f1f120b..68e338106b9 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs
@@ -1,16 +1,12 @@
-using System.Security.Claims;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.AspNetCore.Mvc.Localization;
 using Microsoft.Extensions.Localization;
-using Microsoft.Extensions.Logging;
 using OrchardCore.DisplayManagement.Handlers;
 using OrchardCore.DisplayManagement.Notify;
 using OrchardCore.DisplayManagement.Views;
-using OrchardCore.Modules;
 using OrchardCore.Mvc.ModelBinding;
-using OrchardCore.Users.Handlers;
 using OrchardCore.Users.Models;
 using OrchardCore.Users.ViewModels;
 
@@ -18,32 +14,20 @@ namespace OrchardCore.Users.Drivers;
 
 public sealed class UserDisplayDriver : DisplayDriver<User>
 {
-    private readonly UserManager<IUser> _userManager;
     private readonly IHttpContextAccessor _httpContextAccessor;
-    private readonly INotifier _notifier;
     private readonly IAuthorizationService _authorizationService;
-    private readonly IEnumerable<IUserEventHandler> _userEventHandlers;
-    private readonly ILogger _logger;
 
     internal readonly IHtmlLocalizer H;
     internal readonly IStringLocalizer S;
 
     public UserDisplayDriver(
-        UserManager<IUser> userManager,
         IHttpContextAccessor httpContextAccessor,
-        INotifier notifier,
-        ILogger<UserDisplayDriver> logger,
-        IEnumerable<IUserEventHandler> userEventHandlers,
         IAuthorizationService authorizationService,
         IHtmlLocalizer<UserDisplayDriver> htmlLocalizer,
         IStringLocalizer<UserDisplayDriver> stringLocalizer)
     {
-        _userManager = userManager;
         _httpContextAccessor = httpContextAccessor;
-        _notifier = notifier;
         _authorizationService = authorizationService;
-        _logger = logger;
-        _userEventHandlers = userEventHandlers;
         H = htmlLocalizer;
         S = stringLocalizer;
     }
@@ -67,10 +51,7 @@ public override async Task<IDisplayResult> EditAsync(User user, BuildEditorConte
 
         return Initialize<EditUserViewModel>("UserFields_Edit", model =>
         {
-            model.IsEnabled = user.IsEnabled;
             model.IsNewRequest = context.IsNew;
-            // The current user cannot disable themselves, nor can a user without permission to manage this user disable them.
-            model.IsEditingDisabled = IsCurrentUser(user);
         })
        .Location("Content:1.5");
     }
@@ -106,42 +87,6 @@ public override async Task<IDisplayResult> UpdateAsync(User user, UpdateEditorCo
             return await EditAsync(user, context);
         }
 
-        var isEditingDisabled = IsCurrentUser(user);
-
-        if (!isEditingDisabled && !model.IsEnabled && user.IsEnabled)
-        {
-            var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
-                .Cast<User>()
-                .Where(user => user.IsEnabled)
-                .ToList();
-
-            if (enabledUsersOfAdminRole.Count == 1 && user.UserId == enabledUsersOfAdminRole.First().UserId)
-            {
-                await _notifier.WarningAsync(H["Cannot disable the only enabled administrator."]);
-            }
-            else
-            {
-                user.IsEnabled = model.IsEnabled;
-                var userContext = new UserContext(user);
-                // TODO This handler should be invoked through the create or update methods.
-                // otherwise it will not be invoked when a workflow, or other operation, changes this value.
-                await _userEventHandlers.InvokeAsync((handler, context) => handler.DisabledAsync(context), userContext, _logger);
-            }
-        }
-        else if (!isEditingDisabled && model.IsEnabled && !user.IsEnabled)
-        {
-            user.IsEnabled = model.IsEnabled;
-            var userContext = new UserContext(user);
-            // TODO This handler should be invoked through the create or update methods.
-            // otherwise it will not be invoked when a workflow, or other operation, changes this value.
-            await _userEventHandlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
-        }
-
         return await EditAsync(user, context);
     }
-
-    private bool IsCurrentUser(User user)
-    {
-        return _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) == user.UserId;
-    }
 }
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserViewModel.cs
index 1d9400a01fb..9d987f9e4de 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserViewModel.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserViewModel.cs
@@ -4,18 +4,10 @@ namespace OrchardCore.Users.ViewModels;
 
 public class EditUserViewModel
 {
-    public bool IsEnabled { get; set; }
-
     public string Password { get; set; }
 
     public string PasswordConfirmation { get; set; }
 
-    /// <summary>
-    /// When a user only has rights to view they cannot update this model.
-    /// </summary>
-    [BindNever]
-    public bool IsEditingDisabled { get; set; }
-
     [BindNever]
     public bool IsNewRequest { get; set; }
 }
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserActionsMenu.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserActionsMenu.cshtml
index f25b5a869c8..6d65083ccce 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserActionsMenu.cshtml
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserActionsMenu.cshtml
@@ -16,6 +16,20 @@
     <li>
         <a asp-action="EditPassword" asp-route-id="@Model.User.UserId" class="dropdown-item">@T["Change password"]</a>
     </li>
+    
+    if (Model.User.IsEnabled)
+    {
+        <li>
+            <a asp-action="Disable" asp-route-id="@Model.User.UserId" class="dropdown-item" data-url-af="RemoveUrl UnsafeUrl" data-title="@T["Disable user"]" data-message="@T["Are you sure you want to disable this user?"]">@T["Disable"]</a>
+        </li>
+    }
+    else
+    {
+        <li>
+            <a asp-action="Enable" asp-route-id="@Model.User.UserId" class="dropdown-item" data-url-af="RemoveUrl UnsafeUrl" data-title="@T["Enable user"]" data-message="@T["Are you sure you want to enable this user?"]">@T["Enable"]</a>
+        </li>
+    }
+    
     if (isLockedOut)
     {
         <li>
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserFields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserFields.Edit.cshtml
index 958bfa10d7e..a0ee5ce2380 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserFields.Edit.cshtml
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserFields.Edit.cshtml
@@ -7,14 +7,6 @@
 
 @inject IOptions<IdentityOptions> IdentityOptions
 
-<div class="mb-3">
-    <div class="form-check form-switch">
-        <input asp-for="IsEnabled" type="checkbox" class="form-check-input" checked="@Model.IsEnabled" asp-is-disabled="@Model.IsEditingDisabled" />
-        <label asp-for="IsEnabled" class="form-check-label cursor-pointer">@T["Is enabled?"]</label>
-        <span class="hint dashed">@T["Uncheck to disable this user account."]</span>
-    </div>
-</div>
-
 @if (Model.IsNewRequest)
 {
     <div class="password-generator-wrapper">
diff --git a/src/OrchardCore/OrchardCore.Users.Abstractions/Services/IUserService.cs b/src/OrchardCore/OrchardCore.Users.Abstractions/Services/IUserService.cs
index f7a04760163..8c7403e58ab 100644
--- a/src/OrchardCore/OrchardCore.Users.Abstractions/Services/IUserService.cs
+++ b/src/OrchardCore/OrchardCore.Users.Abstractions/Services/IUserService.cs
@@ -97,4 +97,16 @@ public interface IUserService
     /// <param name="reportError">The error reported in case failure happened during the create process.</param>
     /// <returns></returns>
     Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, string> reportError);
+
+    /// <summary>
+    /// Enabled the given user.
+    /// </summary>
+    /// <param name="user">The user to enable.</param>
+    Task<bool> EnableAsync(IUser user);
+
+    /// <summary>
+    /// Disables the given user.
+    /// </summary>
+    /// <param name="user">The user to disable.</param>
+    Task<bool> DisableAsync(IUser user);
 }
diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
index 49c97ff5b2b..2db720f86f3 100644
--- a/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
+++ b/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
@@ -6,6 +6,7 @@
 using OrchardCore.Modules;
 using OrchardCore.Settings;
 using OrchardCore.Users.Events;
+using OrchardCore.Users.Handlers;
 using OrchardCore.Users.Models;
 
 namespace OrchardCore.Users.Services;
@@ -22,6 +23,7 @@ public sealed class UserService : IUserService
     private readonly IEnumerable<IRegistrationFormEvents> _registrationFormEvents;
     private readonly RegistrationOptions _registrationOptions;
     private readonly ISiteService _siteService;
+    private readonly IEnumerable<IUserEventHandler> _handlers;
     private readonly ILogger _logger;
 
     internal readonly IStringLocalizer S;
@@ -34,6 +36,7 @@ public UserService(
         IEnumerable<IRegistrationFormEvents> registrationFormEvents,
         IOptions<RegistrationOptions> registrationOptions,
         ISiteService siteService,
+        IEnumerable<IUserEventHandler> handlers,
         ILogger<UserService> logger,
         IStringLocalizer<UserService> stringLocalizer)
     {
@@ -44,6 +47,7 @@ public UserService(
         _registrationFormEvents = registrationFormEvents;
         _registrationOptions = registrationOptions.Value;
         _siteService = siteService;
+        _handlers = handlers;
         _logger = logger;
         S = stringLocalizer;
     }
@@ -370,4 +374,57 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        if (user is User u)
+        {
+            u.IsEnabled = true;
+        }
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
+            .Cast<User>()
+            .Where(user => user.IsEnabled)
+            .Take(2)
+            .ToList();
+
+        if (enabledUsersOfAdminRole.Count == 1 && user.UserName == enabledUsersOfAdminRole.First().UserName)
+        {
+            return false;
+        }
+
+        if (user is User u)
+        {
+            u.IsEnabled = false;
+        }
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.DisabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
 }
diff --git a/src/docs/releases/3.0.0.md b/src/docs/releases/3.0.0.md
index 7052943dc7a..e88dfd3cb1e 100644
--- a/src/docs/releases/3.0.0.md
+++ b/src/docs/releases/3.0.0.md
@@ -212,7 +212,11 @@ To simplify the `LoginForm.Edit.cshtml` view, the following code has been moved
 <hr />
 ```
 
-If you are overriding the `Views/Account/Login.cshtml` view, you may want to add the above code to your custom version for consistency.
+#### Enabling & Disabling Users
+
+To centralize user activation and deactivation, the **"Is enabled?"** checkbox has been removed from the Edit User form. Instead, you can now enable or disable a user directly from the **Users List** by clicking the **Actions** button next to the user and selecting **"Enable"** or **"Disable"**.
+
+To support this change, two new methods—`EnableAsync()` and `DisableAsync()`—have been added to `IUserService`, providing a more standardized approach to managing user status.
 
 ### ReCaptcha
 


Reviews:
user: login: sebastienros
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-24T18:00:01Z
user: login: MikeAlhayek
body: Also, please remove this

https://github.com/OrchardCMS/OrchardCore/blob/909f4b688c2ab03f3aec1a350f2c8fdbba6906ff/src/OrchardCore.Modules/OrchardCore.Users/Views/UserFields.Edit.cshtml#L10-L16

and 

https://github.com/OrchardCMS/OrchardCore/blob/909f4b688c2ab03f3aec1a350f2c8fdbba6906ff/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserViewModel.cs#L7

Lastly, please add 3.0 documentation how to removal of "Is Enabled?" option from the user form and how to enable/disable the user moving forward. 

state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-25T21:05:43Z
user: login: hishamco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T21:39:57Z
user: login: hishamco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T21:43:16Z
user: login: hishamco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T21:43:40Z
user: login: hishamco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T21:44:16Z
user: login: MikeAlhayek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T22:17:26Z
user: login: MikeAlhayek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T22:19:57Z
user: login: MikeAlhayek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T22:59:44Z
user: login: MikeAlhayek
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-25T23:40:46Z
user: login: hishamco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T23:42:47Z
user: login: MikeAlhayek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T23:51:38Z
user: login: hishamco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T23:53:11Z
user: login: hishamco
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T23:57:01Z
user: login: MikeAlhayek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-26T00:02:57Z
user: login: MikeAlhayek
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-26T00:12:47Z

Review comments:
diff_hunk: @@ -97,4 +97,16 @@ public interface IUserService
     /// <param name="reportError">The error reported in case failure happened during the create process.</param>
     /// <returns></returns>
     Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, string> reportError);
+
+    /// <summary>
+    /// Enables a user with a specified username.
+    /// </summary>
+    /// <param name="username">The username for the user to be enabled.</param>
+    Task<bool> EnableAsync(string username);
path: src/OrchardCore/OrchardCore.Users.Abstractions/Services/IUserService.cs
user: login: sebastienros
body: Take `IUser` instead
created_at: 2025-04-24T17:54:37Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,42 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(string username)
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: sebastienros
body: It's fine to keep the method because we want it to be an action in the UI and not some state to update. Therefore could you please update the UI to add these Enable/Disable as action buttons on the users, maybe in bulk too, and then remove the property assignments from the driver too.
created_at: 2025-04-24T17:59:10Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,42 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(string username)
+        => await ToggleAsync(username, isEnable: true);
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(string username)
+        => await ToggleAsync(username, isEnable: false);
+
+    private async Task<bool> ToggleAsync(string username, bool isEnable)
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: sebastienros
body: This can be removed when it's taking an IUser argument. The code doesn't need to be shared in a function just to check a bool in the switch/case.
created_at: 2025-04-24T17:59:58Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,56 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User enabled successfully."]);
+        }
+        else
+        {
+            await _notifier.ErrorAsync(H["Could not enable the user."]);
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> Disable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.DisableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User disabled successfully."]);
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: MikeAlhayek
body: ```suggestion
            await _notifier.SuccessAsync(H["User account was successfully disabled"]);
```
created_at: 2025-04-25T21:01:04Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,56 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User enabled successfully."]);
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: MikeAlhayek
body: ```suggestion
            await _notifier.SuccessAsync(H["User account was successfully enabled"]);
```
created_at: 2025-04-25T21:01:20Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,40 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: MikeAlhayek
body: ```suggestion
        ArgumentNullException.ThrowIfNull(user);
```
created_at: 2025-04-25T21:01:51Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,40 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
+
+        (user as User).IsEnabled = true;
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: MikeAlhayek
body: ```suggestion
        if (user is User u)
        {
            u.IsEnabled = true;
        }
```
created_at: 2025-04-25T21:02:41Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,40 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
+
+        (user as User).IsEnabled = true;
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
+
+        (user as User).IsEnabled = false;
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: MikeAlhayek
body: ```suggestion
if (user is User u)
        {
            u.IsEnabled = false;
        }
```
created_at: 2025-04-25T21:03:07Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,40 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
+
+        (user as User).IsEnabled = true;
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: MikeAlhayek
body: ```suggestion
        ArgumentNullException.ThrowIfNull(user);
```
created_at: 2025-04-25T21:03:20Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,56 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User enabled successfully."]);
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: hishamco
body: I'm using the same message for the unlock action
created_at: 2025-04-25T21:39:57Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,40 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: hishamco
body: What's wrong with this?
created_at: 2025-04-25T21:43:16Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,40 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
+
+        (user as User).IsEnabled = true;
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: hishamco
body: What's wrong with this?
created_at: 2025-04-25T21:43:40Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,56 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User enabled successfully."]);
+        }
+        else
+        {
+            await _notifier.ErrorAsync(H["Could not enable the user."]);
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> Disable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.DisableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User disabled successfully."]);
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: hishamco
body: Same as above
created_at: 2025-04-25T21:44:16Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,40 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
+
+        (user as User).IsEnabled = true;
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user, nameof(user));
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: MikeAlhayek
body: without the second paramters, it'll have the same result. We don't use the second parameter anywhere else 
created_at: 2025-04-25T22:17:26Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,56 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User enabled successfully."]);
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: MikeAlhayek
body: I understand. It's grammatically incorrect. Please take the suggested verbiage instead. 
created_at: 2025-04-25T22:19:57Z
author_association: MEMBER
diff_hunk: @@ -106,42 +89,6 @@ public override async Task<IDisplayResult> UpdateAsync(User user, UpdateEditorCo
             return await EditAsync(user, context);
         }
 
-        var isEditingDisabled = IsCurrentUser(user);
-
-        if (!isEditingDisabled && !model.IsEnabled && user.IsEnabled)
-        {
-            var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
-                .Cast<User>()
-                .Where(user => user.IsEnabled)
-                .ToList();
-
-            if (enabledUsersOfAdminRole.Count == 1 && user.UserId == enabledUsersOfAdminRole.First().UserId)
path: src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs
user: login: MikeAlhayek
body: You should handle this case as well to make sure we don't allow disabling the only admin.
created_at: 2025-04-25T22:59:44Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,68 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User account was successfully enabled."]);
+        }
+        else
+        {
+            await _notifier.ErrorAsync(H["Could not enable the user."]);
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> Disable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
+            .Cast<User>()
+            .Where(user => user.IsEnabled)
+            .ToList();
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: MikeAlhayek
body: This will give us a better query
```suggestion
            .Take(2)
            .ToList();
```
created_at: 2025-04-25T23:32:45Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,68 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User account was successfully enabled."]);
+        }
+        else
+        {
+            await _notifier.ErrorAsync(H["Could not enable the user."]);
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> Disable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: MikeAlhayek
body: This logic should be part of the service. Otherwise we would have to duplicate the same logic in the bulk action and everywhere we use the Display function. 
created_at: 2025-04-25T23:33:49Z
author_association: MEMBER
diff_hunk: @@ -212,7 +212,11 @@ To simplify the `LoginForm.Edit.cshtml` view, the following code has been moved
 <hr />
 ```
 
-If you are overriding the `Views/Account/Login.cshtml` view, you may want to add the above code to your custom version for consistency.
+#### Enabling & Disabling Users
+
+To centeralize the user enabling and disabling functionality, the `IsEnabled` property has been removed from the `EditUserViewModel`. Instead, you can now use the `Enable` and `Disable` buttons in the **Users List** to manage user status, which utilizes both `EnableAsync()` and `DisableAsync()` that have been added into `IUserService`.
+
+This change streamlines user management by consolidating these actions into a single interface.
path: src/docs/releases/3.0.0.md
user: login: MikeAlhayek
body: ```suggestion
To centralize user activation and deactivation, the **"Is enabled?"** checkbox has been removed from the Edit User form. Instead, you can now enable or disable a user directly from the **Users List** by clicking the **Actions** button next to the user and selecting **"Enable"** or **"Disable"**.

To support this change, two new methods—`EnableAsync()` and `DisableAsync()`—have been added to `IUserService`, providing a more standardized approach to managing user status.
```
created_at: 2025-04-25T23:39:15Z
author_association: MEMBER
diff_hunk: @@ -561,6 +559,68 @@ public async Task<IActionResult> EditPassword(EditPasswordViewModel model, strin
         return View(model);
     }
 
+    [HttpPost]
+    public async Task<IActionResult> Enable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        if (await _userService.EnableAsync(user))
+        {
+            await _notifier.SuccessAsync(H["User account was successfully enabled."]);
+        }
+        else
+        {
+            await _notifier.ErrorAsync(H["Could not enable the user."]);
+        }
+
+        return RedirectToAction(nameof(Index));
+    }
+
+    [HttpPost]
+    public async Task<IActionResult> Disable(string id)
+    {
+        if (await _userManager.FindByIdAsync(id) is not User user)
+        {
+            return NotFound();
+        }
+
+        if (!await _authorizationService.AuthorizeAsync(User, UsersPermissions.EditUsers, user))
+        {
+            return Forbid();
+        }
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
+            .Cast<User>()
+            .Where(user => user.IsEnabled)
+            .ToList();
path: src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs
user: login: hishamco
body: I just copy the previous code to avoid anything could happen
created_at: 2025-04-25T23:42:47Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,56 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        if (user is User u)
+        {
+            u.IsEnabled = true;
+        }
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
+            .Cast<User>()
+            .Where(user => user.IsEnabled)
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: MikeAlhayek
body: ```suggestion
            .Where(user => user.IsEnabled)
            .Take(2)
```
created_at: 2025-04-25T23:51:37Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,56 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        if (user is User u)
+        {
+            u.IsEnabled = true;
+        }
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
+            .Cast<User>()
+            .Where(user => user.IsEnabled)
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: hishamco
body: Why do you need to take 2?
created_at: 2025-04-25T23:53:11Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,56 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        if (user is User u)
+        {
+            u.IsEnabled = true;
+        }
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
+            .Cast<User>()
+            .Where(user => user.IsEnabled)
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: hishamco
body: Please let us keep things as it's. This PR takes much time
created_at: 2025-04-25T23:57:01Z
author_association: MEMBER
diff_hunk: @@ -370,4 +374,56 @@ public async Task<IUser> RegisterAsync(RegisterUserForm model, Action<string, st
 
         return user;
     }
+
+    /// <inheritdoc/>
+    public async Task<bool> EnableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        if (user is User u)
+        {
+            u.IsEnabled = true;
+        }
+
+        var result = await _userManager.UpdateAsync(user);
+
+        if (result.Succeeded)
+        {
+            var userContext = new UserContext(user);
+            await _handlers.InvokeAsync((handler, context) => handler.EnabledAsync(context), userContext, _logger);
+        }
+
+        return result.Succeeded;
+    }
+
+    /// <inheritdoc/>
+    public async Task<bool> DisableAsync(IUser user)
+    {
+        ArgumentNullException.ThrowIfNull(user);
+
+        var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(OrchardCoreConstants.Roles.Administrator))
+            .Cast<User>()
+            .Where(user => user.IsEnabled)
path: src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs
user: login: MikeAlhayek
body: this makes the query faster, We are doing this logic just to check if we have one or more admins. so checking for 2 will make the query run faster and gives us the same result. Take the change and I'll merge it.
created_at: 2025-04-26T00:02:56Z
author_association: MEMBER

PR comments:
user: login: MikeAlhayek
created_at: 2025-04-07T23:36:52Z
body: I don't think this is a valid approach and should be tested. 

Currently users are able to enable or disable users via the handler. So you can implement the handler, the enable or disable the user. With the proposed approach these events are fired after the user is updated which ignores the handlers logic.

What I think should happen here is that we should move the function to enable/disable user outside the Form edit request similar to the approach I did for confirming email in PR #17684

Moving the disable/enable process as part of the Actions button will allow us to properly call the handlers outside the process of updating the data form. 
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-08T00:51:47Z
body: > With the proposed approach these events are fired after the user is updated which ignores the handlers logic.

What's the wrong with that while enabling / disabling the user should trigger updating event
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-08T01:00:08Z
body: The ToggleStatus action in the controller will update the User object and call the UserStore.Update "which will trigger the updating and Updated events. This will also normalize the process
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-08T01:02:37Z
body: The current approach will open the possibility to listen to the event outside the OC without accessing the control
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-08T02:56:49Z
body: Yes but the proposed will not work. Did you test it out or you assumed the code is good? 


author_association: MEMBER
user: login: hishamco
created_at: 2025-04-08T04:48:23Z
body: It works fine after disabling `UserDisabledEventHandler` which makes the app enter an infinite loop.  I'm not sure why you are regenerating the user stamp here.
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-20T00:37:44Z
body: @MikeAlhayek could you please have a look at my last comment? I need to merge this PR ASAP because it takes 2 weeks

@sebastienros do you know why `UserDisabledEventHandler` has been added?
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-20T01:24:32Z
body: > The ToggleStatus action in the controller will update the User object and call the UserStore.Update "which will trigger the updating and Updated events. This will also normalize the process

@hishamco I still think the above recommendation is the way to go. 
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-20T02:20:18Z
body: I think it's the wrong way because the store is the right place to trigger such an event. Your suggestion is to do the same thing but in a direct way, right?
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-20T03:30:04Z
body: I suggested removing the IsEnabled checkbox from the form and make it part of the controller as its own action.
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-20T03:50:06Z
body: Why?
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-20T04:06:24Z
body: Please review the above comments. 
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-20T04:16:58Z
body: I looked into your related PR The disadvantage of this approach if someone calls the `UserService`, then tries to enable or disable the user, the event will not fire at all coz you are relying on the controller. This is fine for UI, but not execute the code directly

Hope you got my point
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-20T04:41:42Z
body: If you are not careful enough, you could cause a circular reference as you encountered the other day. IMHO, the store should not be responsible if figuring out if the user is enabled or not and trigger events. Another process that should be responsible of that. If you move this into a controller, you'll have a good practice and a better separation of concerns. 
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-20T04:48:11Z
body: I mentioned above why it goes in infinite loop, but still I didn't get an answer why you updated the stamp on that handler

Regarding the separation of concern that you are talking about usually the event system trigger the even in the place where certain action is happened, that's why we use mangers to handle the events in many places
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-20T05:10:08Z
body: The manager does not the store. So add a controller action that would Toggle the user state and can the manager 
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-20T05:13:36Z
body: Right, but what about handle such events outside the UI?
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-20T05:47:07Z
body: You can add a Toggle or Enable and Disable methods in the service and call it from the controller 
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-21T08:13:58Z
body: That makes sense now, by adding `Enable()` & `Disable()` in the `UserService`
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-21T08:36:48Z
body: @MikeAlhayek if the last commits looks good for you, I will do a last test then we could merge
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-21T13:22:05Z
body: Almost there.

1. On the Edit user form remove the "Enabled?" Checkbox
2. On the Actions button next to each user add an option called "Disable user" or "Enable user" based on the user's current status.
3. The service should only need the ToggleAsync method. And you may need to pass IUser instead of username because you'll do the lookup in the controller and return 404 if the userId is not found.


author_association: MEMBER
user: login: hishamco
created_at: 2025-04-21T14:41:32Z
body: ` ToggleAsync()` is a weird name, especially for a public API for a user

> And you may need to pass IUser instead of username because you'll do the lookup in the controller and return 404 if the userId is not found.

Also with `usernameOrEmail` we could return 404, I don't think we need to pass `IUser`
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-21T16:05:35Z
body: You can call it SetStatusAsync.

Once the implementation is done, i'll review again. 
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-21T16:35:16Z
body: `SetStatusAsync` is not clear, let us stay with `EnableAsync()` & `DisableAsync()`  and complete the last change
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-25T21:51:32Z
body: I don't think there's a need to remove `IsEnabled`; it's still useful when editing the user. Again, your feedback is welcome, but it's better to have it early, just to save time. Coz  last time, after a long discussion and updates, you unsubscribed from the PR because maybe we did not agree on all of your feedback 
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-25T22:18:49Z
body: > I don't think there's a need to remove IsEnabled; it's still useful when editing the user. Again, your feedback is welcome, but it's better to have it early, just to save time. 

You have to remove it otherwise you can risk not firing the events when someone enable/disable the user. This is now a more centralized process and should be removed from the form.
author_association: MEMBER
user: login: hishamco
created_at: 2025-04-25T22:26:03Z
body: I need to recheck, but even if we remove it, I don't think we need to document removing such field in a module
author_association: MEMBER
user: login: MikeAlhayek
created_at: 2025-04-25T23:00:51Z
body: >  I don't think we need to document removing such field in a module

We do. It's important to document these changes so people are aware of the change. Some may have to update their own internal instructions.
author_association: MEMBER