84 lines
3.2 KiB
C#
84 lines
3.2 KiB
C#
|
|
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; // 4-6 مناسب است
|
|||
|
|
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
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- 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);
|
|||
|
|
}
|
|||
|
|
}
|