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 { 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 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); } }