2502cbbda2
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m44s
Payment Gateway: - Add PYMSPaymentService: IPaymentGatewayService via gRPC to PYMS microservice - Add ZarinPalPaymentService: direct ZarinPal integration (backup) - Register 'pyms' payment provider in DI ConfigureServices - Add PYMS proto files (pyms_transaction.proto, pyms_public_messages.proto) - Fix VerifyDiscountWalletCharge: pass 'OK' as status instead of Authority - Update appsettings: PaymentProvider=pyms, sandbox mode, merchant ID Blog System: - Add BlogCategory, BlogPost, BlogPostImage entities and CQRS - Add proto files and gRPC services for blog management - Add Mapster profiles for blog responses Content Management: - Add SitePage entity and CQRS for static pages - Add proto and gRPC service for site pages Image/File Management: - Add LocalFileManager with disk storage + base64 serving + FMS fallback - Add ImagePathResolverInterceptor for gRPC responses - Add ImageResolverService for explicit image resolution - Add UploadsController for public file serving with FMS fallback - Add PaymentCallbackController for discount order payment callbacks Database: - Add blog and content entity migrations - Remove ImagePath MaxLength constraints - Remove old FileManagementService (replaced by LocalFileManager)
83 lines
3.2 KiB
C#
83 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;
|
|
private readonly IHashService _hashService;
|
|
|
|
public CreateNewOtpTokenCommandHandler(IApplicationDbContext context, IConfiguration cfg, IHashService hashService)
|
|
{
|
|
_context = context;
|
|
_cfg = cfg;
|
|
_hashService = hashService;
|
|
}
|
|
|
|
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 secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not set");
|
|
var codeHash = _hashService.ComputeHmacSha256Hex(code, secret);
|
|
|
|
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, code, request.SignGuid));
|
|
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();
|
|
}
|
|
}
|