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) <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-03-30 10:32:39 +07:00
parent ccb7716ba1
commit b537cea290
4 changed files with 91 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
using MediatR;
namespace IamService.API.Application.Commands.Auth;
/// <summary>
/// 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).
/// </summary>
public record AdminResetPasswordCommand(
Guid UserId,
string NewPassword) : IRequest<ChangePasswordCommandResult>;

View File

@@ -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;
/// <summary>
/// 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).
/// </summary>
public class AdminResetPasswordCommandHandler : IRequestHandler<AdminResetPasswordCommand, ChangePasswordCommandResult>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<AdminResetPasswordCommandHandler> _logger;
public AdminResetPasswordCommandHandler(
UserManager<ApplicationUser> userManager,
ILogger<AdminResetPasswordCommandHandler> logger)
{
_userManager = userManager;
_logger = logger;
}
public async Task<ChangePasswordCommandResult> 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.");
}
}

View File

@@ -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
}
}
/// <summary>
/// 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).
/// </summary>
[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<IActionResult> 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 });
}
/// <summary>
/// EN: Delete (deactivate) a user.
/// VI: Xóa (vô hiệu hóa) user.
@@ -423,3 +445,12 @@ public class UserPermissionsDto
/// </summary>
public IEnumerable<string> Permissions { get; set; } = Enumerable.Empty<string>();
}
/// <summary>
/// EN: Request body for admin password reset.
/// VI: Request body cho admin reset mật khẩu.
/// </summary>
public class AdminResetPasswordRequest
{
public string NewPassword { get; set; } = string.Empty;
}