88 lines
3.3 KiB
C#
88 lines
3.3 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;
|
||
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);
|
||
}
|
||
}
|