Generator Changes at 9/27/2025 11:07:17 PM
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.CreateNewOtpToken;
|
||||
public record CreateNewOtpTokenCommand : IRequest<CreateNewOtpTokenResponseDto>
|
||||
{
|
||||
//موبایل مقصد
|
||||
public string Mobile { get; init; }
|
||||
//مقصود
|
||||
public string Purpose { get; init; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.CreateNewOtpToken;
|
||||
public class CreateNewOtpTokenCommandValidator : AbstractValidator<CreateNewOtpTokenCommand>
|
||||
{
|
||||
public CreateNewOtpTokenCommandValidator()
|
||||
{
|
||||
RuleFor(model => model.Mobile)
|
||||
.NotEmpty();
|
||||
RuleFor(model => model.Purpose)
|
||||
.NotEmpty();
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<CreateNewOtpTokenCommand>.CreateWithOptions((CreateNewOtpTokenCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.CreateNewOtpToken;
|
||||
public class CreateNewOtpTokenResponseDto
|
||||
{
|
||||
//موفق؟
|
||||
public bool Success { get; set; }
|
||||
//پیام
|
||||
public string Message { get; set; }
|
||||
//کد
|
||||
public string? Code { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.VerifyOtpToken;
|
||||
public record VerifyOtpTokenCommand : IRequest<VerifyOtpTokenResponseDto>
|
||||
{
|
||||
//موبایل مقصد
|
||||
public string Mobile { get; init; }
|
||||
//مقصود
|
||||
public string Purpose { get; init; }
|
||||
//کد
|
||||
public string Code { get; init; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.VerifyOtpToken;
|
||||
public class VerifyOtpTokenCommandValidator : AbstractValidator<VerifyOtpTokenCommand>
|
||||
{
|
||||
public VerifyOtpTokenCommandValidator()
|
||||
{
|
||||
RuleFor(model => model.Mobile)
|
||||
.NotEmpty();
|
||||
RuleFor(model => model.Purpose)
|
||||
.NotEmpty();
|
||||
RuleFor(model => model.Code)
|
||||
.NotEmpty();
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<VerifyOtpTokenCommand>.CreateWithOptions((VerifyOtpTokenCommand)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.VerifyOtpToken;
|
||||
public class VerifyOtpTokenResponseDto
|
||||
{
|
||||
//موفق؟
|
||||
public bool Success { get; set; }
|
||||
//پیام
|
||||
public string Message { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.EventHandlers;
|
||||
|
||||
public class CreateNewOtpTokenEventHandler : INotificationHandler<CreateNewOtpTokenEvent>
|
||||
{
|
||||
private readonly ILogger<CreateNewOtpTokenEventHandler> _logger;
|
||||
|
||||
public CreateNewOtpTokenEventHandler(ILogger<CreateNewOtpTokenEventHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Handle(CreateNewOtpTokenEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using CMSMicroservice.Domain.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.EventHandlers;
|
||||
|
||||
public class VerifyOtpTokenEventHandler : INotificationHandler<VerifyOtpTokenEvent>
|
||||
{
|
||||
private readonly ILogger<VerifyOtpTokenEventHandler> _logger;
|
||||
|
||||
public VerifyOtpTokenEventHandler(ILogger<VerifyOtpTokenEventHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task Handle(VerifyOtpTokenEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().Name);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Queries.GetAllOtpTokenByFilter;
|
||||
public record GetAllOtpTokenByFilterQuery : IRequest<GetAllOtpTokenByFilterResponseDto>
|
||||
{
|
||||
//موقعیت صفحه بندی
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
//مرتب سازی بر اساس
|
||||
public string? SortBy { get; init; }
|
||||
//فیلتر
|
||||
public GetAllOtpTokenByFilterFilter? Filter { get; init; }
|
||||
|
||||
}public class GetAllOtpTokenByFilterFilter
|
||||
{
|
||||
//شناسه
|
||||
public long? Id { get; set; }
|
||||
//موبایل مقصد
|
||||
public string? Mobile { get; set; }
|
||||
//مقصود
|
||||
public string? Purpose { get; set; }
|
||||
//کد هش شده
|
||||
public string? CodeHash { get; set; }
|
||||
//زمان انقضا
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
//شمارش تلاشها
|
||||
public int? Attempts { get; set; }
|
||||
//موفق بود؟
|
||||
public bool? IsUsed { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Queries.GetAllOtpTokenByFilter;
|
||||
public class GetAllOtpTokenByFilterQueryHandler : IRequestHandler<GetAllOtpTokenByFilterQuery, GetAllOtpTokenByFilterResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllOtpTokenByFilterQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllOtpTokenByFilterResponseDto> Handle(GetAllOtpTokenByFilterQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.OtpTokens
|
||||
.ApplyOrder(sortBy: request.SortBy)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
if (request.Filter is not null)
|
||||
{
|
||||
query = query
|
||||
.Where(x => request.Filter.Id == null || x.Id == request.Filter.Id)
|
||||
.Where(x => request.Filter.Mobile == null || x.Mobile.Contains(request.Filter.Mobile))
|
||||
.Where(x => request.Filter.Purpose == null || x.Purpose.Contains(request.Filter.Purpose))
|
||||
.Where(x => request.Filter.CodeHash == null || x.CodeHash.Contains(request.Filter.CodeHash))
|
||||
.Where(x => request.Filter.ExpiresAt == null || x.ExpiresAt == request.Filter.ExpiresAt)
|
||||
.Where(x => request.Filter.Attempts == null || x.Attempts == request.Filter.Attempts)
|
||||
.Where(x => request.Filter.IsUsed == null || x.IsUsed == request.Filter.IsUsed)
|
||||
;
|
||||
}
|
||||
return new GetAllOtpTokenByFilterResponseDto
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ProjectToType<GetAllOtpTokenByFilterResponseModel>().ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Queries.GetAllOtpTokenByFilter;
|
||||
public class GetAllOtpTokenByFilterQueryValidator : AbstractValidator<GetAllOtpTokenByFilterQuery>
|
||||
{
|
||||
public GetAllOtpTokenByFilterQueryValidator()
|
||||
{
|
||||
}
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
{
|
||||
var result = await ValidateAsync(ValidationContext<GetAllOtpTokenByFilterQuery>.CreateWithOptions((GetAllOtpTokenByFilterQuery)model, x => x.IncludeProperties(propertyName)));
|
||||
if (result.IsValid)
|
||||
return Array.Empty<string>();
|
||||
return result.Errors.Select(e => e.ErrorMessage);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace CMSMicroservice.Application.OtpTokenCQ.Queries.GetAllOtpTokenByFilter;
|
||||
public class GetAllOtpTokenByFilterResponseDto
|
||||
{
|
||||
//متادیتا
|
||||
public MetaData MetaData { get; set; }
|
||||
//مدل خروجی
|
||||
public List<GetAllOtpTokenByFilterResponseModel>? Models { get; set; }
|
||||
|
||||
}public class GetAllOtpTokenByFilterResponseModel
|
||||
{
|
||||
//شناسه
|
||||
public long Id { get; set; }
|
||||
//موبایل مقصد
|
||||
public string Mobile { get; set; }
|
||||
//مقصود
|
||||
public string Purpose { get; set; }
|
||||
//کد هش شده
|
||||
public string CodeHash { get; set; }
|
||||
//زمان انقضا
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
//شمارش تلاشها
|
||||
public int Attempts { get; set; }
|
||||
//موفق بود؟
|
||||
public bool IsUsed { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user