From b537cea29080d3ce7f1f44b1018c945c6e242b32 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Mon, 30 Mar 2026 10:32:39 +0700 Subject: [PATCH] feat(iam): add admin reset password endpoint for staff management - Create AdminResetPasswordCommand + Handler using Identity's GeneratePasswordResetTokenAsync + ResetPasswordAsync (no current password required, admin-only action) - Add POST /api/v1/users/{id}/reset-password endpoint in UsersController with OwnerOrAdmin authorization policy - Fix BFF staff/reset-password to send correct payload Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Controllers/StaffController.cs | 2 +- .../Auth/AdminResetPasswordCommand.cs | 11 +++++ .../Auth/AdminResetPasswordCommandHandler.cs | 48 +++++++++++++++++++ .../Controllers/UsersController.cs | 31 ++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommand.cs create mode 100644 services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs index e57695d9..af3bbeb4 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs @@ -136,7 +136,7 @@ public class StaffController : ControllerBase if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(newPassword)) return BadRequest(new { success = false, message = "userId và newPassword là bắt buộc." }); - var payload = new { userId, newPassword }; + var payload = new { newPassword }; return await _iam.PostAsJsonAsync($"/api/v1/users/{userId}/reset-password", payload).ProxyAsync(); } diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommand.cs new file mode 100644 index 00000000..ad1ba8b6 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommand.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace IamService.API.Application.Commands.Auth; + +/// +/// EN: Admin command to force-reset a user's password (no current password required). +/// VI: Command admin để reset mật khẩu user (không cần mật khẩu hiện tại). +/// +public record AdminResetPasswordCommand( + Guid UserId, + string NewPassword) : IRequest; diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs new file mode 100644 index 00000000..8972ce7c --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/AdminResetPasswordCommandHandler.cs @@ -0,0 +1,48 @@ +using MediatR; +using Microsoft.AspNetCore.Identity; +using IamService.Domain.AggregatesModel.UserAggregate; +using IamService.Domain.Exceptions; + +namespace IamService.API.Application.Commands.Auth; + +/// +/// EN: Handler for AdminResetPasswordCommand — uses RemovePassword + AddPassword (no current password needed). +/// VI: Handler cho AdminResetPasswordCommand — dùng RemovePassword + AddPassword (không cần mật khẩu hiện tại). +/// +public class AdminResetPasswordCommandHandler : IRequestHandler +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public AdminResetPasswordCommandHandler( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task Handle(AdminResetPasswordCommand request, CancellationToken cancellationToken) + { + _logger.LogInformation("EN: Admin resetting password for user {UserId} / VI: Admin reset mật khẩu cho user {UserId}", request.UserId); + + var user = await _userManager.FindByIdAsync(request.UserId.ToString()); + if (user == null) + throw new DomainException($"User with ID {request.UserId} not found."); + + // EN: Generate reset token and reset password (no current password needed) + // VI: Tạo reset token và reset mật khẩu (không cần mật khẩu hiện tại) + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var result = await _userManager.ResetPasswordAsync(user, token, request.NewPassword); + + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + _logger.LogWarning("EN: Failed to reset password for user {UserId}: {Errors}", request.UserId, errors); + return new ChangePasswordCommandResult(false, $"Đổi mật khẩu thất bại: {errors}"); + } + + _logger.LogInformation("EN: Password reset successfully for user {UserId}", request.UserId); + return new ChangePasswordCommandResult(true, "Đổi mật khẩu thành công."); + } +} diff --git a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs index 2eebbbcd..52d3715c 100644 --- a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs +++ b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using IamService.API.Application.Common; +using IamService.API.Application.Commands.Auth; using IamService.API.Application.Commands.Users; using IamService.API.Application.Queries.Users; using IamService.Domain.AggregatesModel.UserAggregate; @@ -184,6 +185,27 @@ public class UsersController : ControllerBase } } + /// + /// EN: Admin reset password for a user (no current password required). + /// VI: Admin reset mật khẩu cho user (không cần mật khẩu hiện tại). + /// + [HttpPost("{id:guid}/reset-password")] + [Authorize(Policy = "OwnerOrAdmin")] + [SwaggerOperation(Summary = "Admin reset password", Description = "Resets a user's password without requiring current password. Requires Owner or Admin role.")] + [ProducesResponseType(typeof(ChangePasswordCommandResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task AdminResetPassword( + [FromRoute] Guid id, + [FromBody] AdminResetPasswordRequest request, + CancellationToken cancellationToken = default) + { + var command = new AdminResetPasswordCommand(id, request.NewPassword); + var result = await _mediator.Send(command, cancellationToken); + if (!result.Success) + return BadRequest(new { success = false, message = result.Message }); + return Ok(new { success = true, message = result.Message }); + } + /// /// EN: Delete (deactivate) a user. /// VI: Xóa (vô hiệu hóa) user. @@ -423,3 +445,12 @@ public class UserPermissionsDto /// public IEnumerable Permissions { get; set; } = Enumerable.Empty(); } + +/// +/// EN: Request body for admin password reset. +/// VI: Request body cho admin reset mật khẩu. +/// +public class AdminResetPasswordRequest +{ + public string NewPassword { get; set; } = string.Empty; +}