101 lines
4.1 KiB
C#
101 lines
4.1 KiB
C#
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
|
||
};
|
||
}
|
||
}
|