Files
TakeoutSaaS.AdminApi/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs

101 lines
4.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using MediatR;
using System.Text.Json;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Commands;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Events;
using TakeoutSaaS.Domain.Identity.Enums;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
/// <summary>
/// 生成用户重置密码链接处理器。
/// </summary>
public sealed class ResetIdentityUserPasswordCommandHandler(
IAdminPasswordResetTokenStore tokenStore,
IIdentityUserRepository identityUserRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService,
IIdentityOperationLogPublisher operationLogPublisher)
: IRequestHandler<ResetIdentityUserPasswordCommand, ResetIdentityUserPasswordResult>
{
/// <inheritdoc />
public async Task<ResetIdentityUserPasswordResult> Handle(ResetIdentityUserPasswordCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. (空行后) 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
}
// 3. (空行后) 查询用户实体
var user = isSuperAdmin
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
if (user == null)
{
throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
}
if (!isSuperAdmin && user.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
}
// 4. (空行后) 签发重置令牌1 小时有效)
var expiresAt = DateTime.UtcNow.AddHours(1);
var token = await tokenStore.IssueAsync(user.Id, expiresAt, cancellationToken);
// 5. (空行后) 标记用户需重置密码
user.MustChangePassword = true;
user.FailedLoginCount = 0;
user.LockedUntil = null;
if (user.Status == IdentityUserStatus.Locked)
{
user.Status = IdentityUserStatus.Active;
}
// 6. (空行后) 构建操作日志消息
var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName)
? operatorProfile.Account
: operatorProfile.DisplayName;
if (string.IsNullOrWhiteSpace(operatorName))
{
operatorName = $"user:{currentUserAccessor.UserId}";
}
var logMessage = new IdentityUserOperationLogMessage
{
OperationType = "identity-user:password-reset",
TargetType = "identity_user",
TargetIds = JsonSerializer.Serialize(new[] { user.Id }),
OperatorId = currentUserAccessor.UserId.ToString(),
OperatorName = operatorName,
Parameters = JsonSerializer.Serialize(new { userId = user.Id, tenantId = user.TenantId }),
Result = JsonSerializer.Serialize(new { userId = user.Id, expiresAt }),
Success = true
};
// 7. (空行后) 写入 Outbox 并保存变更
await operationLogPublisher.PublishAsync(logMessage, cancellationToken);
await identityUserRepository.SaveChangesAsync(cancellationToken);
return new ResetIdentityUserPasswordResult
{
Token = token,
ExpiresAt = expiresAt
};
}
}