88 lines
3.4 KiB
C#
88 lines
3.4 KiB
C#
|
|
using CMSMicroservice.Domain.Events;
|
|||
|
|
using Microsoft.Extensions.Configuration;
|
|||
|
|
using System.Security.Cryptography;
|
|||
|
|
using System.Text;
|
|||
|
|
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.VerifyOtpToken;
|
|||
|
|
public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenCommand, VerifyOtpTokenResponseDto>
|
|||
|
|
{
|
|||
|
|
private readonly IApplicationDbContext _context;
|
|||
|
|
private readonly IConfiguration _cfg;
|
|||
|
|
|
|||
|
|
public VerifyOtpTokenCommandHandler(IApplicationDbContext context, IConfiguration cfg)
|
|||
|
|
{
|
|||
|
|
_context = context;
|
|||
|
|
_cfg = cfg;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const int MaxAttempts = 5; // محدودیت تلاش
|
|||
|
|
public async Task<VerifyOtpTokenResponseDto> Handle(VerifyOtpTokenCommand request, CancellationToken cancellationToken)
|
|||
|
|
{
|
|||
|
|
var mobile = request.Mobile.NormalizeIranMobile();
|
|||
|
|
var purpose = request.Purpose?.ToLowerInvariant() ?? "signup";
|
|||
|
|
var now = DateTime.Now;
|
|||
|
|
|
|||
|
|
|
|||
|
|
var otp = await _context.OtpTokens
|
|||
|
|
.Where(o => o.Mobile == mobile && o.Purpose == purpose && !o.IsUsed && o.ExpiresAt > now)
|
|||
|
|
.OrderByDescending(o => o.Created)
|
|||
|
|
.FirstOrDefaultAsync(cancellationToken);
|
|||
|
|
|
|||
|
|
if (otp is null) return new VerifyOtpTokenResponseDto() { Success = false, Message = "کد پیدا نشد یا منقضی شده است." };
|
|||
|
|
|
|||
|
|
if (otp.Attempts >= MaxAttempts) return new VerifyOtpTokenResponseDto() { Success = false, Message = "تعداد تلاشها زیاد است. دوباره کد بگیرید." };
|
|||
|
|
|
|||
|
|
otp.Attempts++;
|
|||
|
|
|
|||
|
|
if (!VerifyHash(request.Code, otp.CodeHash))
|
|||
|
|
{
|
|||
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|||
|
|
return new VerifyOtpTokenResponseDto() { Success = false, Message = "کد نادرست است." };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// موفق
|
|||
|
|
otp.IsUsed = true;
|
|||
|
|
|
|||
|
|
// کاربر را بساز/بهروزرسانی کن
|
|||
|
|
var user = await _context.Users.FirstOrDefaultAsync(u => u.Mobile == mobile, cancellationToken);
|
|||
|
|
if (user is null)
|
|||
|
|
{
|
|||
|
|
user = new User
|
|||
|
|
{
|
|||
|
|
Mobile = mobile,
|
|||
|
|
ReferralCode = UtilExtensions.Generate(digits: 10, firstDigitNonZero: true),
|
|||
|
|
IsMobileVerified = true,
|
|||
|
|
MobileVerifiedAt = now
|
|||
|
|
};
|
|||
|
|
await _context.Users.AddAsync(user, cancellationToken);
|
|||
|
|
user.AddDomainEvent(new CreateNewUserEvent(user));
|
|||
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
user.IsMobileVerified = true;
|
|||
|
|
user.MobileVerifiedAt ??= now;
|
|||
|
|
|
|||
|
|
_context.Users.Update(user);
|
|||
|
|
user.AddDomainEvent(new UpdateUserEvent(user));
|
|||
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new VerifyOtpTokenResponseDto()
|
|||
|
|
{
|
|||
|
|
Success = true,
|
|||
|
|
Message = "اعتبارسنجی موفق."
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
private bool VerifyHash(string code, string hexHash) => Hash(code).Equals(hexHash, StringComparison.OrdinalIgnoreCase);
|
|||
|
|
|
|||
|
|
}
|