init
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Common\ValidationBase\**" />
|
||||
<EmbeddedResource Remove="Common\ValidationBase\**" />
|
||||
<None Remove="Common\ValidationBase\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CleanArc.Domain\CleanArc.Domain.csproj" />
|
||||
<PackageReference Include="Mediator.SourceGenerator">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Mediator.Abstractions" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CleanArc.Application.Common;
|
||||
|
||||
public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
|
||||
: IPipelineBehavior<TRequest, TResponse>
|
||||
where TResponse : class
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
|
||||
public async ValueTask<TResponse> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await next(message,cancellationToken);
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
|
||||
if (typeof(TResponse).GetGenericTypeDefinition() == typeof(OperationResult<>))
|
||||
{
|
||||
var response = new OperationResult<TResponse> { IsException = true };
|
||||
|
||||
return response as TResponse;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Common;
|
||||
|
||||
public class MetricsBehaviour<TRequest, TResponse> :
|
||||
IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly Histogram<long> _requestResponseDurationHistogram;
|
||||
|
||||
public MetricsBehaviour(IMeterFactory meterFactory)
|
||||
{
|
||||
var meter = meterFactory.Create("mediator_meter");
|
||||
_requestResponseDurationHistogram = meter.CreateHistogram<long>(
|
||||
"Request_Response_Duration", "ms"
|
||||
, "Determines the total request response durations");
|
||||
}
|
||||
|
||||
|
||||
public async ValueTask<TResponse> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
var stopWatch = Stopwatch.StartNew();
|
||||
|
||||
var response = await next(message,cancellationToken);
|
||||
|
||||
stopWatch.Stop();
|
||||
|
||||
_requestResponseDurationHistogram.Record(stopWatch.ElapsedMilliseconds,new []{new KeyValuePair<string, object>("Request",message.GetType().Name)});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Common;
|
||||
|
||||
public class ValidateCommandBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
|
||||
: IPipelineBehavior<TRequest, TResponse>
|
||||
where TResponse : IOperationResult, new()
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
|
||||
public async ValueTask<TResponse> Handle(TRequest message, MessageHandlerDelegate<TRequest, TResponse> next, CancellationToken cancellationToken)
|
||||
{
|
||||
var errors = new List<ValidationFailure>();
|
||||
|
||||
|
||||
foreach (var validator in validators)
|
||||
{
|
||||
var validationResult =
|
||||
await validator.ValidateAsync(new ValidationContext<TRequest>(message), cancellationToken);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
errors.AddRange(validationResult.Errors);
|
||||
}
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
return new TResponse()
|
||||
{
|
||||
ErrorMessages = errors.Select(c => new KeyValuePair<string, string>(c.PropertyName, c.ErrorMessage))
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return await next(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Security.Claims;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using CleanArc.Domain.Entities.User;
|
||||
|
||||
namespace CleanArc.Application.Contracts;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
Task<AccessToken> GenerateAsync(User user);
|
||||
Task<ClaimsPrincipal> GetPrincipalFromExpiredToken(string token);
|
||||
Task<AccessToken> GenerateByPhoneNumberAsync(string phoneNumber);
|
||||
Task<AccessToken> RefreshToken(Guid refreshTokenId);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using CleanArc.Domain.Entities.User;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Application.Contracts.Identity;
|
||||
|
||||
public interface IAppUserManager
|
||||
{
|
||||
Task<IdentityResult> CreateUser(User user);
|
||||
Task<IdentityResult> CreateUser(User user,string password);
|
||||
Task<bool> IsExistUser(string phoneNumber);
|
||||
Task<bool> IsExistUserName(string userName);
|
||||
Task<string> GeneratePhoneNumberConfirmationToken(User user, string phoneNumber);
|
||||
Task<User> GetUserByCode(string code);
|
||||
Task<IdentityResult> ChangePhoneNumber(User user, string phoneNumber, string code);
|
||||
Task<IdentityResult> VerifyUserCode(User user,string code);
|
||||
Task<string> GenerateOtpCode(User user);
|
||||
Task<User> GetUserByPhoneNumber(string phoneNumber);
|
||||
Task<User> GetByUserName(string userName);
|
||||
Task<User> GetUserByIdAsync(int userId);
|
||||
Task<List<User>> GetAllUsersAsync();
|
||||
Task<IdentityResult> CreateUserWithPasswordAsync(User user,string password);
|
||||
Task<IdentityResult> AddUserToRoleAsync(User user, Role role);
|
||||
Task<IdentityResult> IncrementAccessFailedCountAsync(User user);
|
||||
Task<bool> IsUserLockedOutAsync(User user);
|
||||
Task ResetUserLockoutAsync(User user);
|
||||
Task UpdateUserAsync(User user);
|
||||
Task UpdateSecurityStampAsync(User user);
|
||||
|
||||
Task<bool> IsPasswordValidAsync(User user, string password);
|
||||
Task<string[]> GetRoleAsync(User user);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CleanArc.Application.Models.Identity;
|
||||
using CleanArc.Domain.Entities.User;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Application.Contracts.Identity;
|
||||
|
||||
public interface IRoleManagerService
|
||||
{
|
||||
Task<List<GetRolesDto>> GetRolesAsync();
|
||||
Task<IdentityResult> CreateRoleAsync(CreateRoleDto model);
|
||||
Task<bool> DeleteRoleAsync(int roleId);
|
||||
Task<List<ActionDescriptionDto>> GetPermissionActionsAsync();
|
||||
Task<RolePermissionDto> GetRolePermissionsAsync(int roleId);
|
||||
Task<bool> ChangeRolePermissionsAsync(EditRolePermissionsDto model);
|
||||
Task<Role> GetRoleByIdAsync(int roleId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using CleanArc.Domain.Entities.Order;
|
||||
|
||||
namespace CleanArc.Application.Contracts.Persistence;
|
||||
|
||||
public interface IOrderRepository
|
||||
{
|
||||
Task AddOrderAsync(Order order);
|
||||
Task<List<Order>> GetAllUserOrdersAsync(int userId);
|
||||
Task<List<Order>> GetAllOrdersWithRelatedUserAsync();
|
||||
Task<Order> GetUserOrderByIdAndUserIdAsync(int userId,int orderId,bool trackEntity);
|
||||
Task DeleteUserOrdersAsync(int userId);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace CleanArc.Application.Contracts.Persistence;
|
||||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
public IUserRefreshTokenRepository UserRefreshTokenRepository { get; }
|
||||
public IOrderRepository OrderRepository { get; }
|
||||
Task CommitAsync();
|
||||
ValueTask RollBackAsync();
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using CleanArc.Domain.Entities.User;
|
||||
|
||||
namespace CleanArc.Application.Contracts.Persistence;
|
||||
|
||||
public interface IUserRefreshTokenRepository
|
||||
{
|
||||
Task<Guid> CreateToken(int userId);
|
||||
Task<UserRefreshToken> GetTokenWithInvalidation(Guid id);
|
||||
Task<User> GetUserByRefreshToken(Guid tokenId);
|
||||
Task RemoveUserOldTokens(int userId, CancellationToken cancellationToken);
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Domain.Entities.User;
|
||||
using CleanArc.SharedKernel.Extensions;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Admin.Commands.AddAdminCommand
|
||||
{
|
||||
internal class AddAdminCommandHandler:IRequestHandler<AddAdminCommand,OperationResult<bool>>
|
||||
{
|
||||
private readonly IAppUserManager _userManager;
|
||||
private readonly IRoleManagerService _roleManagerService;
|
||||
|
||||
public AddAdminCommandHandler(IAppUserManager userManager, IRoleManagerService roleManagerService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_roleManagerService = roleManagerService;
|
||||
}
|
||||
|
||||
public async ValueTask<OperationResult<bool>> Handle(AddAdminCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var role = await _roleManagerService.GetRoleByIdAsync(request.RoleId);
|
||||
|
||||
if(role is null)
|
||||
return OperationResult<bool>.NotFoundResult("Specified role not found");
|
||||
|
||||
var newAdmin = new User { UserName = request.UserName, Email = request.Email };
|
||||
|
||||
var adminCreateResult =
|
||||
await _userManager.CreateUserWithPasswordAsync(
|
||||
newAdmin, request.Password);
|
||||
|
||||
if(!adminCreateResult.Succeeded)
|
||||
return OperationResult<bool>.FailureResult(adminCreateResult.Errors.StringifyIdentityResultErrors());
|
||||
|
||||
var addAdminToRoleResult = await _userManager.AddUserToRoleAsync(newAdmin, role);
|
||||
|
||||
if(addAdminToRoleResult.Succeeded)
|
||||
return OperationResult<bool>.SuccessResult(true);
|
||||
|
||||
return OperationResult<bool>.FailureResult(addAdminToRoleResult.Errors.StringifyIdentityResultErrors());
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Admin.Commands.AddAdminCommand;
|
||||
|
||||
public record AddAdminCommand
|
||||
(string UserName, string Email, string Password, int RoleId) : IRequest<OperationResult<bool>>,
|
||||
IValidatableModel<AddAdminCommand>
|
||||
{
|
||||
public IValidator<AddAdminCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AddAdminCommand> validator)
|
||||
{
|
||||
validator.RuleFor(c => c.Email)
|
||||
.EmailAddress()
|
||||
.WithMessage("Please enter an valid email");
|
||||
|
||||
validator.RuleFor(c => c.UserName)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Please specify a valid username");
|
||||
|
||||
validator
|
||||
.RuleFor(c => c.RoleId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("Please select a valid role");
|
||||
|
||||
return validator;
|
||||
}
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using CleanArc.Application.Contracts;
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Admin.Queries.GetToken;
|
||||
|
||||
public class AdminGetTokenQueryHandler:IRequestHandler<AdminGetTokenQuery,OperationResult<AdminGetTokenQueryResult>>
|
||||
{
|
||||
private readonly IAppUserManager _userManager;
|
||||
private readonly IJwtService _jwtService;
|
||||
public AdminGetTokenQueryHandler(IAppUserManager userManager, IJwtService jwtService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_jwtService = jwtService;
|
||||
}
|
||||
|
||||
public async ValueTask<OperationResult<AdminGetTokenQueryResult>> Handle(AdminGetTokenQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userManager.GetByUserName(request.UserName);
|
||||
|
||||
if(user is null)
|
||||
return OperationResult<AdminGetTokenQueryResult>.FailureResult("User not found");
|
||||
|
||||
var isUserLockedOut = await _userManager.IsUserLockedOutAsync(user);
|
||||
|
||||
if(isUserLockedOut)
|
||||
if (user.LockoutEnd != null)
|
||||
return OperationResult<AdminGetTokenQueryResult>.FailureResult(
|
||||
$"User is locked out. Try in {(user.LockoutEnd-DateTimeOffset.Now).Value.Minutes} Minutes");
|
||||
|
||||
var userRoles = await _userManager.GetRoleAsync(user);
|
||||
|
||||
|
||||
if(!userRoles.Any())
|
||||
return OperationResult<AdminGetTokenQueryResult>.FailureResult("This user does not have any role assigned");
|
||||
|
||||
if(!await _userManager.IsPasswordValidAsync(user, request.Password))
|
||||
return OperationResult<AdminGetTokenQueryResult>.NotFoundResult("User not found");
|
||||
|
||||
var token= await _jwtService.GenerateAsync(user);
|
||||
|
||||
|
||||
return OperationResult<AdminGetTokenQueryResult>.SuccessResult(new(token,userRoles));
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
|
||||
namespace CleanArc.Application.Features.Admin.Queries.GetToken;
|
||||
|
||||
public record AdminGetTokenQueryResult(AccessToken Token,string[] Roles);
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Admin.Queries.GetToken;
|
||||
|
||||
public record AdminGetTokenQuery(string UserName, string Password) : IRequest<OperationResult<AdminGetTokenQueryResult>>,
|
||||
IValidatableModel<AdminGetTokenQuery>
|
||||
{
|
||||
public IValidator<AdminGetTokenQuery> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AdminGetTokenQuery> validator)
|
||||
{
|
||||
validator.RuleFor(c => c.UserName)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Please enter admin username");
|
||||
|
||||
validator.RuleFor(c => c.Password)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Please enter admin password");
|
||||
|
||||
return validator;
|
||||
}
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Contracts.Persistence;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Commands;
|
||||
|
||||
internal class AddOrderCommandHandler(IUnitOfWork unitOfWork, IAppUserManager userManager)
|
||||
: IRequestHandler<AddOrderCommand, OperationResult<bool>>
|
||||
{
|
||||
public async ValueTask<OperationResult<bool>> Handle(AddOrderCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.GetUserByIdAsync(request.UserId);
|
||||
|
||||
if(user==null)
|
||||
return OperationResult<bool>.FailureResult("User Not Found");
|
||||
|
||||
await unitOfWork.OrderRepository.AddOrderAsync(new Domain.Entities.Order.Order()
|
||||
{ UserId = user.Id, OrderName = request.OrderName });
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
|
||||
return OperationResult<bool>.SuccessResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Commands;
|
||||
|
||||
public record AddOrderCommand( string OrderName) : IRequest<OperationResult<bool>>,
|
||||
IValidatableModel<AddOrderCommand>
|
||||
{
|
||||
[JsonIgnore]
|
||||
public int UserId { get; set; }
|
||||
|
||||
public IValidator<AddOrderCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AddOrderCommand> validator)
|
||||
{
|
||||
validator.RuleFor(c => c.OrderName)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Please enter your role name");
|
||||
|
||||
return validator;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
using CleanArc.Application.Contracts.Persistence;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Commands;
|
||||
|
||||
public class DeleteUserOrdersCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler<DeleteUserOrdersCommand,OperationResult<bool>>
|
||||
{
|
||||
public async ValueTask<OperationResult<bool>> Handle(DeleteUserOrdersCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await unitOfWork.OrderRepository.DeleteUserOrdersAsync(request.UserId);
|
||||
|
||||
return OperationResult<bool>.SuccessResult(true);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Commands;
|
||||
|
||||
public record DeleteUserOrdersCommand(int UserId):IRequest<OperationResult<bool>>;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CleanArc.Application.Contracts.Persistence;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Commands;
|
||||
|
||||
public class UpdateUserOrderCommandHandler(IUnitOfWork unitOfWork) : IRequestHandler<UpdateUserOrderCommand,OperationResult<bool>>
|
||||
{
|
||||
|
||||
|
||||
public async ValueTask<OperationResult<bool>> Handle(UpdateUserOrderCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var order = await unitOfWork.OrderRepository.GetUserOrderByIdAndUserIdAsync(request.UserId, request.OrderId,
|
||||
true);
|
||||
|
||||
if(order is null)
|
||||
return OperationResult<bool>.NotFoundResult("Specified Order not found");
|
||||
|
||||
order.OrderName=request.OrderName;
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
|
||||
return OperationResult<bool>.SuccessResult(true);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Commands;
|
||||
|
||||
public record UpdateUserOrderCommand
|
||||
(int OrderId, string OrderName) : IRequest<OperationResult<bool>>,IValidatableModel<UpdateUserOrderCommand>
|
||||
{
|
||||
[JsonIgnore]
|
||||
public int UserId { get; set; }
|
||||
|
||||
public IValidator<UpdateUserOrderCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<UpdateUserOrderCommand> validator)
|
||||
{
|
||||
validator.RuleFor(c => c.OrderId).NotEmpty().GreaterThan(0);
|
||||
validator.RuleFor(c => c.OrderName).NotEmpty().NotNull();
|
||||
|
||||
return validator;
|
||||
}
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Queries.GetAllOrders;
|
||||
|
||||
public record GetAllOrdersQuery():IRequest<OperationResult<List<GetAllOrdersQueryResult>>>;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using CleanArc.Application.Contracts.Persistence;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using MapsterMapper;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Queries.GetAllOrders
|
||||
{
|
||||
internal class GetAllOrdersQueryHandler(IUnitOfWork unitOfWork, IMapper mapper)
|
||||
: IRequestHandler<GetAllOrdersQuery, OperationResult<List<GetAllOrdersQueryResult>>>
|
||||
{
|
||||
public async ValueTask<OperationResult<List<GetAllOrdersQueryResult>>> Handle(GetAllOrdersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var orders = await unitOfWork.OrderRepository.GetAllOrdersWithRelatedUserAsync();
|
||||
|
||||
var result = orders.Select(mapper.Map<Domain.Entities.Order.Order,GetAllOrdersQueryResult>).ToList();
|
||||
|
||||
return OperationResult<List<GetAllOrdersQueryResult>>.SuccessResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
using Mapster;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Queries.GetAllOrders;
|
||||
|
||||
public record GetAllOrdersQueryResult(int OrderId, string OrderName, int OrderOwnerId, string OrderOwnerUserName);
|
||||
|
||||
class GetAllOrdersQueryResultMapping : IRegister
|
||||
{
|
||||
public void Register(TypeAdapterConfig config)
|
||||
{
|
||||
config.NewConfig<Domain.Entities.Order.Order, GetAllOrdersQueryResult>()
|
||||
.Map(dest => dest.OrderId, src => src.Id)
|
||||
.Map(dest => dest.OrderName, src => src.OrderName)
|
||||
.Map(dest => dest.OrderOwnerId, src => src.User.Id)
|
||||
.Map(dest => dest.OrderOwnerUserName, src => src.User.UserName);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using CleanArc.Application.Contracts.Persistence;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Queries.GetUserOrders;
|
||||
|
||||
internal class GetUserOrdersQueryHandler(IUnitOfWork unitOfWork)
|
||||
: IRequestHandler<GetUserOrdersQueryModel, OperationResult<List<GetUsersQueryResultModel>>>
|
||||
{
|
||||
public async ValueTask<OperationResult<List<GetUsersQueryResultModel>>> Handle(GetUserOrdersQueryModel request, CancellationToken cancellationToken)
|
||||
{
|
||||
var orders = await unitOfWork.OrderRepository.GetAllUserOrdersAsync(request.UserId);
|
||||
|
||||
if(!orders.Any())
|
||||
return OperationResult<List<GetUsersQueryResultModel>>.NotFoundResult("You Don't Have Any Orders");
|
||||
|
||||
var result = orders.Select(c => new GetUsersQueryResultModel(c.Id, c.OrderName));
|
||||
|
||||
return OperationResult<List<GetUsersQueryResultModel>>.SuccessResult(result.ToList());
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Order.Queries.GetUserOrders;
|
||||
|
||||
public record GetUserOrdersQueryModel(int UserId) : IRequest<OperationResult<List<GetUsersQueryResultModel>>>;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace CleanArc.Application.Features.Order.Queries.GetUserOrders;
|
||||
|
||||
public record GetUsersQueryResultModel(int OrderId, string OrderName);
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Identity;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Commands.AddRoleCommand
|
||||
{
|
||||
internal class AddRoleCommandHandler(IRoleManagerService roleManagerService)
|
||||
: IRequestHandler<AddRoleCommand, OperationResult<bool>>
|
||||
{
|
||||
public async ValueTask<OperationResult<bool>> Handle(AddRoleCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var addRoleResult =
|
||||
await roleManagerService.CreateRoleAsync(new CreateRoleDto() { RoleName = request.RoleName });
|
||||
|
||||
if (addRoleResult.Succeeded)
|
||||
return OperationResult<bool>.SuccessResult(true);
|
||||
|
||||
var errors = string.Join("\n", addRoleResult.Errors.Select(c => c.Description));
|
||||
|
||||
return OperationResult<bool>.FailureResult(errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Commands.AddRoleCommand;
|
||||
|
||||
public record AddRoleCommand(string RoleName) : IRequest<OperationResult<bool>>,
|
||||
IValidatableModel<AddRoleCommand>
|
||||
{
|
||||
public IValidator<AddRoleCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<AddRoleCommand> validator)
|
||||
{
|
||||
validator
|
||||
.RuleFor(c => c.RoleName)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Please enter role name");
|
||||
|
||||
return validator;
|
||||
}
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Identity;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand
|
||||
{
|
||||
internal class UpdateRoleClaimsCommandHandler(IRoleManagerService roleManagerService)
|
||||
: IRequestHandler<UpdateRoleClaimsCommand, OperationResult<bool>>
|
||||
{
|
||||
public async ValueTask<OperationResult<bool>> Handle(UpdateRoleClaimsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var updateRoleResult = await roleManagerService.ChangeRolePermissionsAsync(new EditRolePermissionsDto()
|
||||
{ RoleId = request.RoleId, Permissions = request.RoleClaimValue });
|
||||
|
||||
return updateRoleResult
|
||||
? OperationResult<bool>.SuccessResult(true)
|
||||
: OperationResult<bool>.FailureResult("Could Not Update Claims for given Role");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Commands.UpdateRoleClaimsCommand;
|
||||
|
||||
public record UpdateRoleClaimsCommand( int RoleId, List<string> RoleClaimValue):IRequest<OperationResult<bool>>;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery
|
||||
{
|
||||
internal class GetAllRolesQueryHandler(IRoleManagerService roleManagerService)
|
||||
: IRequestHandler<GetAllRolesQuery, OperationResult<List<GetAllRolesQueryResponse>>>
|
||||
{
|
||||
public async ValueTask<OperationResult<List<GetAllRolesQueryResponse>>> Handle(GetAllRolesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var roles = await roleManagerService.GetRolesAsync();
|
||||
|
||||
if(!roles.Any())
|
||||
return OperationResult<List<GetAllRolesQueryResponse>>.NotFoundResult("No Roles Found");
|
||||
|
||||
var result = roles.Select(c => new GetAllRolesQueryResponse(int.Parse(c.Id), c.Name)).ToList();
|
||||
|
||||
return OperationResult<List<GetAllRolesQueryResponse>>.SuccessResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery;
|
||||
|
||||
public record GetAllRolesQueryResponse(int RoleId,string RoleName);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAllRolesQuery;
|
||||
|
||||
public record GetAllRolesQuery():IRequest<OperationResult<List<GetAllRolesQueryResponse>>>;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery
|
||||
{
|
||||
internal class GetAuthorizableRoutesQueryHandler(IRoleManagerService roleManagerService)
|
||||
: IRequestHandler<GetAuthorizableRoutesQuery, OperationResult<List<GetAuthorizableRoutesQueryResponse>>>
|
||||
{
|
||||
public async ValueTask<OperationResult<List<GetAuthorizableRoutesQueryResponse>>> Handle(GetAuthorizableRoutesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var authRoutes = await roleManagerService.GetPermissionActionsAsync();
|
||||
|
||||
if(!authRoutes.Any())
|
||||
return OperationResult<List<GetAuthorizableRoutesQueryResponse>>.NotFoundResult("No Special auth route found");
|
||||
|
||||
var result = authRoutes.Select(c =>
|
||||
new GetAuthorizableRoutesQueryResponse(c.Key, c.AreaName, c.ControllerName, c.ActionName,c.ControllerDescription))
|
||||
.ToList();
|
||||
|
||||
return OperationResult<List<GetAuthorizableRoutesQueryResponse>>.SuccessResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery;
|
||||
|
||||
public record GetAuthorizableRoutesQueryResponse(string RouteKey,string AreaName,string ControllerName,string ActionName,string ControllerDescription);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Role.Queries.GetAuthorizableRoutesQuery;
|
||||
|
||||
public record GetAuthorizableRoutesQuery():IRequest<OperationResult<List<GetAuthorizableRoutesQueryResponse>>>;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Domain.Entities.User;
|
||||
using MapsterMapper;
|
||||
using Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Commands.Create;
|
||||
|
||||
internal class UserCreateCommandHandler(
|
||||
IAppUserManager userManager,
|
||||
ILogger<UserCreateCommandHandler> logger,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<UserCreateCommand, OperationResult<UserCreateCommandResult>>
|
||||
{
|
||||
public async ValueTask<OperationResult<UserCreateCommandResult>> Handle(UserCreateCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userNameExist = await userManager.IsExistUser(request.PhoneNumber);
|
||||
|
||||
if (userNameExist)
|
||||
return OperationResult<UserCreateCommandResult>.FailureResult("Phone number already exists");
|
||||
|
||||
var phoneNumberExist = await userManager.IsExistUserName(request.UserName);
|
||||
|
||||
if (phoneNumberExist)
|
||||
return OperationResult<UserCreateCommandResult>.FailureResult("Username already exists");
|
||||
|
||||
//var user = new User { UserName = request.UserName, Name = request.FirstName, FamilyName = request.LastName, PhoneNumber = request.PhoneNumber };
|
||||
|
||||
var user = mapper.Map<User>(request);
|
||||
|
||||
|
||||
var createResult =string.IsNullOrEmpty(request.Password)?
|
||||
await userManager.CreateUser(user)
|
||||
:await userManager.CreateUser(user, request.Password);
|
||||
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
return OperationResult<UserCreateCommandResult>.FailureResult(string.Join(",",
|
||||
createResult.Errors.Select(c => c.Description)));
|
||||
}
|
||||
|
||||
var code = await userManager.GeneratePhoneNumberConfirmationToken(user, user.PhoneNumber);
|
||||
|
||||
|
||||
logger.LogWarning($"Generated Code for User ID {user.Id} is {code}");
|
||||
|
||||
//TODO Send Code Via Sms Provider
|
||||
|
||||
return OperationResult<UserCreateCommandResult>.SuccessResult(new UserCreateCommandResult
|
||||
{ UserGeneratedKey = user.GeneratedCode });
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace CleanArc.Application.Features.Users.Commands.Create;
|
||||
|
||||
public class UserCreateCommandResult
|
||||
{
|
||||
public string UserGeneratedKey { get; set; }
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Domain.Entities.User;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Commands.Create;
|
||||
|
||||
public record UserCreateCommand
|
||||
(string UserName, string Name, string FamilyName, string PhoneNumber,string Password,string RepeatPassword)
|
||||
: IRequest<OperationResult<UserCreateCommandResult>>
|
||||
,IValidatableModel<UserCreateCommand>
|
||||
{
|
||||
|
||||
public IValidator<UserCreateCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<UserCreateCommand> validator)
|
||||
{
|
||||
|
||||
validator
|
||||
.RuleFor(c => c.Name)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("User must have first name");
|
||||
|
||||
validator.RuleFor(c => c.UserName)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Please enter your username");
|
||||
|
||||
validator
|
||||
.RuleFor(c => c.FamilyName)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("User must have last name");
|
||||
|
||||
|
||||
validator.RuleFor(c => c.PhoneNumber).NotEmpty()
|
||||
.NotNull().WithMessage("Phone Number is required.")
|
||||
.MinimumLength(10).WithMessage("PhoneNumber must not be less than 10 characters.")
|
||||
.MaximumLength(20).WithMessage("PhoneNumber must not exceed 50 characters.")
|
||||
.Matches(new Regex(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")).WithMessage("Phone number is not valid");
|
||||
|
||||
validator.RuleFor(c => c.Password)
|
||||
.Matches(c => c.RepeatPassword)
|
||||
.WithMessage("passwords do not match");
|
||||
|
||||
return validator;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using CleanArc.Application.Contracts;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand
|
||||
{
|
||||
internal class RefreshUserTokenCommandHandler(IJwtService jwtService)
|
||||
: IRequestHandler<RefreshUserTokenCommand, OperationResult<AccessToken>>
|
||||
{
|
||||
public async ValueTask<OperationResult<AccessToken>> Handle(RefreshUserTokenCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var newToken = await jwtService.RefreshToken(request.RefreshToken);
|
||||
|
||||
if(newToken is null)
|
||||
return OperationResult<AccessToken>.FailureResult("Invalid refresh token");
|
||||
|
||||
return OperationResult<AccessToken>.SuccessResult(newToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Commands.RefreshUserTokenCommand;
|
||||
|
||||
public record RefreshUserTokenCommand(Guid RefreshToken) : IRequest<OperationResult<AccessToken>>,
|
||||
IValidatableModel<RefreshUserTokenCommand>
|
||||
{
|
||||
public IValidator<RefreshUserTokenCommand> ValidateApplicationModel(ApplicationBaseValidationModelProvider<RefreshUserTokenCommand> validator)
|
||||
{
|
||||
validator.RuleFor(c => c.RefreshToken)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Please enter valid user refresh token");
|
||||
|
||||
return validator;
|
||||
}
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Commands.RequestLogout
|
||||
{
|
||||
internal class RequestLogoutCommandHandler(IAppUserManager userManager)
|
||||
: IRequestHandler<RequestLogoutCommand, OperationResult<bool>>
|
||||
{
|
||||
public async ValueTask<OperationResult<bool>> Handle(RequestLogoutCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.GetUserByIdAsync(request.UserId);
|
||||
|
||||
if (user == null)
|
||||
return OperationResult<bool>.FailureResult("User not found");
|
||||
|
||||
await userManager.UpdateSecurityStampAsync(user);
|
||||
|
||||
return OperationResult<bool>.SuccessResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Commands.RequestLogout;
|
||||
|
||||
public record RequestLogoutCommand(int UserId):IRequest<OperationResult<bool>>;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using CleanArc.Application.Contracts;
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using CleanArc.SharedKernel.Extensions;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.GenerateUserToken;
|
||||
|
||||
internal class GenerateUserTokenQueryHandler(IJwtService jwtService, IAppUserManager userManager)
|
||||
: IRequestHandler<GenerateUserTokenQuery, OperationResult<AccessToken>>
|
||||
{
|
||||
public async ValueTask<OperationResult<AccessToken>> Handle(GenerateUserTokenQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.GetUserByCode(request.UserKey);
|
||||
|
||||
if (user is null)
|
||||
return OperationResult<AccessToken>.FailureResult("User Not found");
|
||||
|
||||
var result = user.PhoneNumberConfirmed? await userManager.VerifyUserCode(
|
||||
user, request.Code):await userManager.ChangePhoneNumber(user,user.PhoneNumber,request.Code);
|
||||
|
||||
|
||||
if (!result.Succeeded)
|
||||
return OperationResult<AccessToken>.FailureResult(result.Errors.StringifyIdentityResultErrors());
|
||||
|
||||
await userManager.UpdateUserAsync(user);
|
||||
|
||||
var token = await jwtService.GenerateAsync(user);
|
||||
|
||||
return OperationResult<AccessToken>.SuccessResult(token);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.GenerateUserToken;
|
||||
|
||||
public record GenerateUserTokenQuery(string UserKey, string Code) : IRequest<OperationResult<AccessToken>>,
|
||||
IValidatableModel<GenerateUserTokenQuery>
|
||||
{
|
||||
public IValidator<GenerateUserTokenQuery> ValidateApplicationModel(ApplicationBaseValidationModelProvider<GenerateUserTokenQuery> validator)
|
||||
{
|
||||
validator.RuleFor(c => c.Code)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.Length(6)
|
||||
.WithMessage("User code is not valid");
|
||||
|
||||
validator.RuleFor(c => c.UserKey)
|
||||
.NotEmpty()
|
||||
.NotNull()
|
||||
.WithMessage("Invalid user key");
|
||||
|
||||
return validator;
|
||||
}
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Domain.Entities.User;
|
||||
using MapsterMapper;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.GetUsers;
|
||||
|
||||
internal class GetUsersQueryHandler(IAppUserManager userManager, IMapper mapper)
|
||||
: IRequestHandler<GetUsersQuery, OperationResult<List<GetUsersQueryResponse>>>
|
||||
{
|
||||
public async ValueTask<OperationResult<List<GetUsersQueryResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var usersModel =
|
||||
(await userManager.GetAllUsersAsync()).Select(mapper.Map<User, GetUsersQueryResponse>).ToList();
|
||||
|
||||
if(!usersModel.Any())
|
||||
return OperationResult<List<GetUsersQueryResponse>>.NotFoundResult("No Users Found!");
|
||||
|
||||
return OperationResult<List<GetUsersQueryResponse>>.SuccessResult(usersModel);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
|
||||
using CleanArc.Domain.Entities.User;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.GetUsers;
|
||||
|
||||
public record GetUsersQueryResponse
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public int UserId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.GetUsers;
|
||||
|
||||
public record GetUsersQuery : IRequest<OperationResult<List<GetUsersQueryResponse>>>;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using CleanArc.Application.Contracts;
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
|
||||
|
||||
public class PasswordUserTokenRequestQueryResult
|
||||
(IAppUserManager userManager,IJwtService jwtService)
|
||||
:IRequestHandler<PasswordUserTokenRequestQuery,OperationResult<AccessToken>>
|
||||
{
|
||||
public async ValueTask<OperationResult<AccessToken>> Handle(PasswordUserTokenRequestQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.GetByUserName(request.UserName);
|
||||
|
||||
if(user is null)
|
||||
return OperationResult<AccessToken>.NotFoundResult("User not found");
|
||||
|
||||
if(!await userManager.IsPasswordValidAsync(user,request.Password))
|
||||
return OperationResult<AccessToken>.NotFoundResult("User not found");
|
||||
|
||||
var token = await jwtService.GenerateAsync(user);
|
||||
|
||||
return OperationResult<AccessToken>.SuccessResult(token);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.Application.Models.Jwt;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
|
||||
|
||||
public record PasswordUserTokenRequestQuery
|
||||
(string UserName,string Password)
|
||||
:IValidatableModel<PasswordUserTokenRequestQuery>,IRequest<OperationResult<AccessToken>>
|
||||
{
|
||||
public IValidator<PasswordUserTokenRequestQuery> ValidateApplicationModel(ApplicationBaseValidationModelProvider<PasswordUserTokenRequestQuery> validator)
|
||||
{
|
||||
validator.RuleFor(c => c.UserName)
|
||||
.NotEmpty();
|
||||
|
||||
validator.RuleFor(c => c.Password)
|
||||
.NotEmpty();
|
||||
|
||||
return validator;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using CleanArc.Application.Contracts.Identity;
|
||||
using CleanArc.Application.Models.Common;
|
||||
using Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
|
||||
|
||||
public class UserTokenRequestQueryHandler(
|
||||
IAppUserManager userManager,
|
||||
ILogger<UserTokenRequestQueryHandler> logger)
|
||||
: IRequestHandler<UserTokenRequestQuery, OperationResult<UserTokenRequestQueryResponse>>
|
||||
{
|
||||
|
||||
|
||||
public async ValueTask<OperationResult<UserTokenRequestQueryResponse>> Handle(UserTokenRequestQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await userManager.GetUserByPhoneNumber(request.UserPhoneNumber);
|
||||
|
||||
if(user is null)
|
||||
return OperationResult<UserTokenRequestQueryResponse>.NotFoundResult("User Not found");
|
||||
|
||||
var code = user.PhoneNumberConfirmed? await userManager.GenerateOtpCode(user) : await userManager.GeneratePhoneNumberConfirmationToken(user,user.PhoneNumber);
|
||||
|
||||
logger.LogWarning($"Generated Code for user Id {user.Id} is {code}");
|
||||
|
||||
//TODO Send Code Via Sms Provider
|
||||
|
||||
return OperationResult<UserTokenRequestQueryResponse>.SuccessResult(new UserTokenRequestQueryResponse {UserKey = user.GeneratedCode});
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
|
||||
|
||||
public class UserTokenRequestQueryResponse
|
||||
{
|
||||
public string UserKey { get; set; }
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CleanArc.Application.Models.Common;
|
||||
using CleanArc.SharedKernel.ValidationBase;
|
||||
using CleanArc.SharedKernel.ValidationBase.Contracts;
|
||||
using FluentValidation;
|
||||
using Mediator;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CleanArc.Application.Features.Users.Queries.TokenRequest;
|
||||
|
||||
public record UserTokenRequestQuery(string UserPhoneNumber) : IRequest<OperationResult<UserTokenRequestQueryResponse>>,
|
||||
IValidatableModel<UserTokenRequestQuery>
|
||||
{
|
||||
public IValidator<UserTokenRequestQuery> ValidateApplicationModel(ApplicationBaseValidationModelProvider<UserTokenRequestQuery> validator)
|
||||
{
|
||||
|
||||
validator.RuleFor(c => c.UserPhoneNumber).NotEmpty()
|
||||
.NotNull().WithMessage("Phone Number is required.")
|
||||
.MinimumLength(10).WithMessage("PhoneNumber must not be less than 10 characters.")
|
||||
.MaximumLength(20).WithMessage("PhoneNumber must not exceed 50 characters.")
|
||||
.Matches(new Regex(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")).WithMessage("Phone number is not valid");
|
||||
|
||||
|
||||
return validator;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Diagnostics;
|
||||
using CleanArc.SharedKernel.Extensions;
|
||||
|
||||
namespace CleanArc.Application.Models.ApiResult;
|
||||
|
||||
public class ApiResult
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public ApiResultStatusCode StatusCode { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
public string RequestId { get; }
|
||||
|
||||
public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, string message = null)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
StatusCode = statusCode;
|
||||
Message = message ?? statusCode.ToDisplay();
|
||||
RequestId = Activity.Current?.TraceId.ToHexString() ?? string.Empty;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class ApiResult<TData> : ApiResult
|
||||
|
||||
{
|
||||
public TData Data { get; set; }
|
||||
|
||||
public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, TData data, string message = null)
|
||||
: base(isSuccess, statusCode, message)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace CleanArc.Application.Models.ApiResult;
|
||||
|
||||
public enum ApiResultStatusCode
|
||||
{
|
||||
[Display(Name = "Success")]
|
||||
Success = 200,
|
||||
|
||||
[Display(Name = "Server Error")]
|
||||
ServerError = 500,
|
||||
|
||||
[Display(Name = "Bad Request Error")]
|
||||
BadRequest = 400,
|
||||
|
||||
[Display(Name = "Not Found")]
|
||||
NotFound = 404,
|
||||
|
||||
|
||||
[Display(Name = "Request Process Error")]
|
||||
EntityProcessError = 422,
|
||||
|
||||
[Display(Name = "Authentication Error")]
|
||||
UnAuthorized = 401,
|
||||
|
||||
[Display(Name = "Authorization Error")]
|
||||
Forbidden = 403,
|
||||
|
||||
[Display(Name = "Not Acceptable")]
|
||||
NotAcceptable = 406,
|
||||
|
||||
[Display(Name = "Failed Dependency")]
|
||||
FailedDependency = 424
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace CleanArc.Application.Models.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Marker Interface To Mark Operation Result Response In Validation Pipeline
|
||||
/// </summary>
|
||||
public interface IOperationResult
|
||||
{
|
||||
bool IsSuccess { get; set; }
|
||||
List<KeyValuePair<string,string>> ErrorMessages { get; set; }
|
||||
bool IsException { get; set; }
|
||||
bool IsNotFound { get; set; }
|
||||
}
|
||||
|
||||
public class OperationResult<TResult> : IOperationResult
|
||||
{
|
||||
public TResult Result { get; set; }
|
||||
|
||||
public bool IsSuccess { get; set; }
|
||||
public List<KeyValuePair<string,string>> ErrorMessages { get; set; } = new();
|
||||
public bool IsException { get; set; }
|
||||
public bool IsNotFound { get; set; }
|
||||
|
||||
public static OperationResult<TResult> SuccessResult(TResult result)
|
||||
{
|
||||
return new OperationResult<TResult> { Result = result, IsSuccess = true };
|
||||
}
|
||||
|
||||
public static OperationResult<TResult> FailureResult(string propertyName, string message, TResult result = default)
|
||||
{
|
||||
var operationResult = new OperationResult<TResult> { Result = result, IsSuccess = false };
|
||||
|
||||
operationResult.ErrorMessages.Add(new(propertyName,message));
|
||||
|
||||
return operationResult;
|
||||
}
|
||||
public static OperationResult<TResult> FailureResult(string message, TResult result = default)
|
||||
{
|
||||
var operationResult = new OperationResult<TResult> { Result = result, IsSuccess = false };
|
||||
|
||||
operationResult.ErrorMessages.Add(new("GeneralError",message));
|
||||
|
||||
return operationResult;
|
||||
}
|
||||
public static OperationResult<TResult> NotFoundResult(string message)
|
||||
{
|
||||
var operationResult = new OperationResult<TResult> { IsSuccess = false, IsNotFound = true };
|
||||
|
||||
operationResult.ErrorMessages.Add(new("GeneralError", message));
|
||||
|
||||
return operationResult;
|
||||
}
|
||||
|
||||
public void AddError(string propertyName, string message)
|
||||
{
|
||||
IsSuccess = false;
|
||||
|
||||
ErrorMessages.Add(new(propertyName,message));
|
||||
}
|
||||
|
||||
public string GetErrorMessage()
|
||||
=> string.Join(Environment.NewLine, ErrorMessages.Select(x => $"{x.Key}: {x.Value}"));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace CleanArc.Application.Models.Identity;
|
||||
|
||||
public class ActionDescriptionDto
|
||||
{
|
||||
public string Key => $"{AreaName}:{ControllerName}:{ActionName}";
|
||||
|
||||
public string AreaName { get; set; }
|
||||
|
||||
public string ControllerName { get; set; }
|
||||
public string ControllerDisplayName { get; set; }
|
||||
|
||||
public string ActionName { get; set; }
|
||||
|
||||
public string ActionDisplayName { get; set; }
|
||||
public string ControllerDescription { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace CleanArc.Application.Models.Identity;
|
||||
|
||||
public class CreateRoleDto
|
||||
{
|
||||
public string RoleName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace CleanArc.Application.Models.Identity;
|
||||
|
||||
public class EditRolePermissionsDto
|
||||
{
|
||||
public int RoleId { get; set; }
|
||||
public List<string> Permissions { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using CleanArc.Domain.Entities.User;
|
||||
|
||||
namespace CleanArc.Application.Models.Identity;
|
||||
|
||||
public class GetRolesDto
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using CleanArc.Domain.Entities.User;
|
||||
|
||||
namespace CleanArc.Application.Models.Identity;
|
||||
|
||||
public class RolePermissionDto
|
||||
{
|
||||
public List<string> Keys { get; set; } = new List<string>();
|
||||
|
||||
public Role Role { get; set; }
|
||||
|
||||
public int RoleId { get; set; }
|
||||
|
||||
public List<ActionDescriptionDto> Actions { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
namespace CleanArc.Application.Models.Jwt;
|
||||
|
||||
public class AccessToken
|
||||
{
|
||||
public string access_token { get; set; }
|
||||
public string refresh_token { get; set; }
|
||||
public string token_type { get; set; }
|
||||
public int expires_in { get; set; }
|
||||
|
||||
public AccessToken(JwtSecurityToken securityToken,string refreshToken="")
|
||||
{
|
||||
access_token = new JwtSecurityTokenHandler().WriteToken(securityToken);
|
||||
token_type = "Bearer";
|
||||
expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds;
|
||||
refresh_token = refreshToken;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace CleanArc.Application.Models.Jwt;
|
||||
|
||||
public class TokenRequest
|
||||
{
|
||||
[Required]
|
||||
public string grant_type { get; set; }
|
||||
public string username { get; set; }
|
||||
public string password { get; set; }
|
||||
public string refresh_token { get; set; }
|
||||
public string scope { get; set; }
|
||||
|
||||
public string client_id { get; set; }
|
||||
public string client_secret { get; set; }
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using System.Reflection;
|
||||
using CleanArc.Application.Common;
|
||||
using Mapster;
|
||||
using Mediator;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CleanArc.Application.ServiceConfiguration;
|
||||
|
||||
public static class ServiceCollectionExtension
|
||||
{
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddMediator(options =>
|
||||
{
|
||||
options.ServiceLifetime = ServiceLifetime.Scoped;
|
||||
options.Namespace = "CleanArc.Application.Mediator";
|
||||
});
|
||||
|
||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MetricsBehaviour<,>));
|
||||
|
||||
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidateCommandBehavior<,>));
|
||||
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Shared\CleanArc.SharedKernel\CleanArc.SharedKernel.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace CleanArc.Domain.Common;
|
||||
|
||||
public interface IEntity
|
||||
{
|
||||
}
|
||||
|
||||
public interface ITimeModification
|
||||
{
|
||||
DateTime CreatedTime { get; set; }
|
||||
DateTime? ModifiedDate { get; set; }
|
||||
}
|
||||
|
||||
public abstract class BaseEntity<TKey> : IEntity, ITimeModification
|
||||
{
|
||||
public TKey Id { get; protected set; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is BaseEntity<TKey> other))
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
if (GetType() != other.GetType())
|
||||
return false;
|
||||
|
||||
return Id.Equals(other.Id);
|
||||
}
|
||||
|
||||
public static bool operator ==(BaseEntity<TKey> a, BaseEntity<TKey> b)
|
||||
{
|
||||
if (a is null && b is null)
|
||||
return true;
|
||||
|
||||
if (a is null || b is null)
|
||||
return false;
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
public static bool operator !=(BaseEntity<TKey> a, BaseEntity<TKey> b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (GetType().ToString() + Id).GetHashCode();
|
||||
}
|
||||
|
||||
public DateTime CreatedTime { get; set; }
|
||||
public DateTime? ModifiedDate { get; set; }
|
||||
}
|
||||
|
||||
public abstract class BaseEntity : BaseEntity<int>
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CleanArc.Domain.Common;
|
||||
|
||||
namespace CleanArc.Domain.Entities.Order;
|
||||
|
||||
public class Order:BaseEntity
|
||||
{
|
||||
public string OrderName { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
#region Navigation Properties
|
||||
|
||||
public User.User User { get; set; }
|
||||
public int UserId { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using CleanArc.Domain.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class Role:IdentityRole<int>,IEntity
|
||||
{
|
||||
public Role()
|
||||
{
|
||||
CreatedDate=DateTime.Now;
|
||||
}
|
||||
|
||||
public string DisplayName { get; set; }
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public ICollection<RoleClaim> Claims { get; set; }
|
||||
public ICollection<UserRole> Users { get; set; }
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CleanArc.Domain.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class RoleClaim:IdentityRoleClaim<int>,IEntity
|
||||
{
|
||||
public RoleClaim()
|
||||
{
|
||||
CreatedClaim=DateTime.Now;
|
||||
}
|
||||
|
||||
public DateTime CreatedClaim { get; set; }
|
||||
public Role Role { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using CleanArc.Domain.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class User:IdentityUser<int>,IEntity
|
||||
{
|
||||
public User()
|
||||
{
|
||||
this.GeneratedCode = Guid.NewGuid().ToString().Substring(0, 8);
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string FamilyName { get; set; }
|
||||
public string GeneratedCode { get; set; }
|
||||
|
||||
public ICollection<UserRole> UserRoles { get; set; }
|
||||
public ICollection<UserLogin> Logins { get; set; }
|
||||
public ICollection<UserClaim> Claims { get; set; }
|
||||
public ICollection<UserToken> Tokens { get; set; }
|
||||
public ICollection<UserRefreshToken> UserRefreshTokens { get; set; }
|
||||
|
||||
#region Navigation Properties
|
||||
|
||||
public IList<Order.Order> Orders { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using CleanArc.Domain.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class UserClaim:IdentityUserClaim<int>,IEntity
|
||||
{
|
||||
public User User { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using CleanArc.Domain.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class UserLogin:IdentityUserLogin<int>,IEntity
|
||||
{
|
||||
public UserLogin()
|
||||
{
|
||||
LoggedOn=DateTime.Now;
|
||||
}
|
||||
|
||||
public User User { get; set; }
|
||||
public DateTime LoggedOn { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CleanArc.Domain.Common;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class UserRefreshToken:BaseEntity<Guid>
|
||||
{
|
||||
public UserRefreshToken()
|
||||
{
|
||||
CreatedAt=DateTime.Now;
|
||||
}
|
||||
|
||||
public int UserId { get; set; }
|
||||
public User User { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public bool IsValid { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using CleanArc.Domain.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class UserRole : IdentityUserRole<int>,IEntity
|
||||
{
|
||||
public User User { get; set; }
|
||||
public Role Role { get; set; }
|
||||
public DateTime CreatedUserRoleDate { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using CleanArc.Domain.Common;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace CleanArc.Domain.Entities.User;
|
||||
|
||||
public class UserToken:IdentityUserToken<int>,IEntity
|
||||
{
|
||||
public UserToken()
|
||||
{
|
||||
GeneratedTime=DateTime.Now;
|
||||
}
|
||||
|
||||
public User User { get; set; }
|
||||
public DateTime GeneratedTime { get; set; }
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user