Files
CMS/src/CMSMicroservice.Application/OtpTokenCQ/Commands/CreateNewOtpToken/CreateNewOtpTokenCommandHandler.cs

88 lines
3.3 KiB
C#
Raw Normal View History

using CMSMicroservice.Domain.Events;
using Microsoft.Extensions.Configuration;
using System.Security.Cryptography;
using System.Text;
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.CreateNewOtpToken;
public class CreateNewOtpTokenCommandHandler : IRequestHandler<CreateNewOtpTokenCommand, CreateNewOtpTokenResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly IConfiguration _cfg;
public CreateNewOtpTokenCommandHandler(IApplicationDbContext context, IConfiguration cfg)
{
_context = context;
_cfg = cfg;
}
const int CodeLength = 6;
const int MaxAttempts = 5; // محدودیت تلاش
static readonly TimeSpan Ttl = TimeSpan.FromMinutes(2);
static readonly TimeSpan Cooldown = TimeSpan.FromSeconds(60); // فاصله ارسال مجدد
public async Task<CreateNewOtpTokenResponseDto> Handle(CreateNewOtpTokenCommand request,
CancellationToken cancellationToken)
{
var mobile = request.Mobile.NormalizeIranMobile();
var purpose = request.Purpose?.ToLowerInvariant() ?? "signup";
// ریت‌لیمیت ساده: اگر هنوز کدی فعال و تازه داریم، اجازه نده
var now = DateTime.Now;
var lastActive = await _context.OtpTokens
.Where(o => o.Mobile == mobile && o.Purpose == purpose && !o.IsUsed && o.ExpiresAt > now)
.OrderByDescending(o => o.Created)
.FirstOrDefaultAsync(cancellationToken);
if (lastActive is not null && (now - lastActive.Created) < Cooldown)
return new CreateNewOtpTokenResponseDto()
{
Success = false,
Message = "لطفاً کمی بعد دوباره تلاش کنید."
};
// تولید کد
var code = GenerateNumericCode(CodeLength);
var codeHash = Hash(code);
var entity = new OtpToken
{
Mobile = mobile,
Purpose = purpose,
CodeHash = codeHash,
ExpiresAt = now.Add(Ttl),
Attempts = 0,
IsUsed = false
};
await _context.OtpTokens.AddAsync(entity, cancellationToken);
entity.AddDomainEvent(new CreateNewOtpTokenEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return new CreateNewOtpTokenResponseDto()
{
Success = true,
Message = "کد ارسال شد.",
Code = code,
RemainingAttempts = MaxAttempts,
RemainingSeconds = Ttl.Seconds
};
}
// --- utilها ---
private string GenerateNumericCode(int len)
{
// امن‌تر از Random(): تولید ارقام با RNG
var bytes = new byte[len];
RandomNumberGenerator.Fill(bytes);
var sb = new StringBuilder(len);
foreach (var b in bytes) sb.Append((b % 10).ToString());
return sb.ToString();
}
private string Hash(string code)
{
// HMAC با secret اپ (نیازی به ذخیره salt جدا نیست)
var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = h.ComputeHash(Encoding.UTF8.GetBytes(code));
return Convert.ToHexString(hash);
}
}