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:
@@ -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>;
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user