Generator Changes at 9/27/2025 11:07:17 PM

This commit is contained in:
MeysamMoghaddam
2025-09-27 23:48:41 +03:30
parent 447e580a8a
commit a1b6e28d35
45 changed files with 1320 additions and 18 deletions

View File

@@ -10,6 +10,7 @@
<PackageReference Include="Mapster" Version="7.3.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,38 @@
using System.Security.Cryptography;
namespace CMSMicroservice.Application.Common.Extensions;
public static class UtilExtensions
{
public static string NormalizeIranMobile(this string input)
{
if (string.IsNullOrWhiteSpace(input)) throw new ArgumentException("mobile is empty");
var m = new string(input.Where(char.IsDigit).ToArray());
if (m.StartsWith("0098")) m = m[4..];
else if (m.StartsWith("098")) m = m[3..];
else if (m.StartsWith("98")) m = m[2..];
if (!m.StartsWith("0")) m = "0" + m;
if (m.Length != 11 || !m.StartsWith("09"))
throw new ArgumentException("شماره موبایل نامعتبر است.");
return m;
}
public static string Generate(int digits = 10, bool firstDigitNonZero = false)
{
if (digits <= 0) throw new ArgumentOutOfRangeException(nameof(digits));
var chars = new char[digits];
// رقم اول
if (firstDigitNonZero)
chars[0] = (char)('0' + RandomNumberGenerator.GetInt32(1, 10)); // 1..9
else
chars[0] = (char)('0' + RandomNumberGenerator.GetInt32(10)); // 0..9
// بقیه ارقام
for (int i = 1; i < digits; i++)
chars[i] = (char)('0' + RandomNumberGenerator.GetInt32(10));
return new string(chars);
}
}

View File

@@ -2,11 +2,12 @@ namespace CMSMicroservice.Application.Common.Interfaces;
public interface IApplicationDbContext
{
DbSet<User> Users { get; }
DbSet<UserAddress> UserAddresss { get; }
DbSet<Package> Packages { get; }
DbSet<UserOrder> UserOrders { get; }
DbSet<Role> Roles { get; }
DbSet<UserRole> UserRoles { get; }
DbSet<User> Users { get; }
DbSet<OtpToken> OtpTokens { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,10 @@
namespace CMSMicroservice.Application.Common.Mappings;
public class OtpTokenProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
//config.NewConfig<Source,Destination>()
// .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}");
}
}

View File

@@ -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; }
}

View File

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

View File

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

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
namespace CMSMicroservice.Application.OtpTokenCQ.Commands.VerifyOtpToken;
public class VerifyOtpTokenResponseDto
{
//موفق؟
public bool Success { get; set; }
//پیام
public string Message { get; set; }
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

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

View File

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

View File

@@ -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; }
}

View File

@@ -1,4 +1,5 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Domain.Events;
using System.Security.Cryptography;
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewUser;
public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand, CreateNewUserResponseDto>
{
@@ -13,6 +14,8 @@ public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand,
CancellationToken cancellationToken)
{
var entity = request.Adapt<User>();
entity.ReferralCode = UtilExtensions.Generate(digits: 10, firstDigitNonZero: true);
await _context.Users.AddAsync(entity, cancellationToken);
entity.AddDomainEvent(new CreateNewUserEvent(entity));
await _context.SaveChangesAsync(cancellationToken);

View File

@@ -7,13 +7,9 @@ public record UpdateUserCommand : IRequest<Unit>
public string? FirstName { get; init; }
//نام خانوادگی
public string? LastName { get; init; }
//شماره موبایل
public string Mobile { get; init; }
//کد ملی
public string? NationalCode { get; init; }
//آدرس آواتار
public string? AvatarPath { get; init; }
//شناسه والد
public long? ParentId { get; init; }
}

View File

@@ -5,8 +5,6 @@ public class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>
{
RuleFor(model => model.Id)
.NotNull();
RuleFor(model => model.Mobile)
.NotEmpty();
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{

View File

@@ -24,4 +24,10 @@ public record GetAllUserByFilterQuery : IRequest<GetAllUserByFilterResponseDto>
public string? AvatarPath { get; set; }
//شناسه والد
public long? ParentId { get; set; }
//کد ارجاع
public string? ReferralCode { get; set; }
//موبایل فعال شده؟
public bool? IsMobileVerified { get; set; }
//تاریخ فعال سازی موبایل
public DateTime? MobileVerifiedAt { get; set; }
}

View File

@@ -23,6 +23,8 @@ public class GetAllUserByFilterQueryHandler : IRequestHandler<GetAllUserByFilter
.Where(x => request.Filter.Mobile == null || x.Mobile.Contains(request.Filter.Mobile))
.Where(x => request.Filter.NationalCode == null || x.NationalCode.Contains(request.Filter.NationalCode))
.Where(x => request.Filter.AvatarPath == null || x.AvatarPath.Contains(request.Filter.AvatarPath))
.Where(x => request.Filter.ReferralCode == null || x.ReferralCode == request.Filter.ReferralCode)
.Where(x => request.Filter.IsMobileVerified == null || x.IsMobileVerified == request.Filter.IsMobileVerified)
.Where(x => request.Filter.ParentId == null || x.ParentId == request.Filter.ParentId)
;
}

View File

@@ -22,4 +22,10 @@ public class GetAllUserByFilterResponseDto
public string? AvatarPath { get; set; }
//شناسه والد
public long? ParentId { get; set; }
//کد ارجاع
public string ReferralCode { get; set; }
//موبایل فعال شده؟
public bool IsMobileVerified { get; set; }
//تاریخ فعال سازی موبایل
public DateTime? MobileVerifiedAt { get; set; }
}

View File

@@ -15,5 +15,11 @@ public class GetUserResponseDto
public string? AvatarPath { get; set; }
//شناسه والد
public long? ParentId { get; set; }
//کد ارجاع
public string ReferralCode { get; set; }
//موبایل فعال شده؟
public bool IsMobileVerified { get; set; }
//تاریخ فعال سازی موبایل
public DateTime? MobileVerifiedAt { get; set; }
}