feat: restructure ProgramManager area and integrate Shared.Contracts

This commit is contained in:
2025-12-13 16:41:27 +03:30
parent b12b3b9eb8
commit f2293934d4
52 changed files with 134 additions and 735 deletions

View File

@@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\Shared.Contracts\Shared.Contracts.csproj" />
<ProjectReference Include="..\..\Domain\GozareshgirProgramManager.Domain\GozareshgirProgramManager.Domain.csproj" />
</ItemGroup>

View File

@@ -15,6 +15,7 @@ using MediatR;
using PersianTools.Core;
using System.Runtime.InteropServices;
using Microsoft.EntityFrameworkCore;
using Shared.Contracts.Holidays;
namespace GozareshgirProgramManager.Application.Modules.Checkouts.Commands.CreateCheckout;
@@ -25,16 +26,18 @@ public class CreateOrEditCheckoutCommandHandler : IBaseCommandHandler<CreateOrEd
private readonly ISalaryPaymentSettingRepository _salaryPaymentSettingRepository;
private readonly ITaskSectionActivityRepository _taskSectionActivityRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly IGozareshgirDbContext _gozareshgirDbContext;
private readonly IHolidayQueryService _holidayQueryService;
public CreateOrEditCheckoutCommandHandler(ICheckoutRepository checkoutRepository, IUnitOfWork unitOfWork, ISalaryPaymentSettingRepository salaryPaymentSettingRepository, ITaskSectionActivityRepository taskSectionActivityRepository, IGozareshgirDbContext gozareshgirDbContext)
public CreateOrEditCheckoutCommandHandler(ICheckoutRepository checkoutRepository, IUnitOfWork unitOfWork,
ISalaryPaymentSettingRepository salaryPaymentSettingRepository,
ITaskSectionActivityRepository taskSectionActivityRepository, IHolidayQueryService holidayQueryService)
{
_checkoutRepository = checkoutRepository;
_unitOfWork = unitOfWork;
_salaryPaymentSettingRepository = salaryPaymentSettingRepository;
_taskSectionActivityRepository = taskSectionActivityRepository;
_gozareshgirDbContext = gozareshgirDbContext;
_holidayQueryService = holidayQueryService;
}
public async Task<OperationResult> Handle(CreateOrEditCheckoutCommand request, CancellationToken cancellationToken)
@@ -182,9 +185,7 @@ public class CreateOrEditCheckoutCommandHandler : IBaseCommandHandler<CreateOrEd
var endMonth = Convert.ToInt32(endDate.Substring(5, 2));
var endDay = Convert.ToInt32(endDate.Substring(8, 2));
var persianEnd = new PersianDateTime(endYear, endMonth, endDay);
var holidays = await _gozareshgirDbContext.HolidayItems.Where(x=>x.Holidaydate >= start && x.Holidaydate <= end).ToListAsync();
var holidays = await _holidayQueryService.GetHolidaysInDates(start, end) ;
int mandatoryHours = 0;
for (var currentDay = persianStart; currentDay <= persianEnd; currentDay = currentDay.AddDays(1))
{

View File

@@ -1,11 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.LoginUser;
/// <summary>
/// دستور ورود کاربر به سیستم
/// </summary>
public record LoginUserCommand(long UserId) : IBaseCommand<LoginResponse>;

View File

@@ -1,98 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.LoginUser;
/// <summary>
/// Handler برای ورود کاربر به سیستم
/// </summary>
public class LoginUserCommandHandler : IRequestHandler<LoginUserCommand, OperationResult<LoginResponse>>
{
private readonly IUserRepository _userRepository;
private readonly IUserRefreshTokenRepository _refreshTokenRepository;
private readonly IAuthHelper _authHelper;
private readonly IUnitOfWork _unitOfWork;
public LoginUserCommandHandler(
IUserRepository userRepository,
IAuthHelper authHelper,
IUnitOfWork unitOfWork, IUserRefreshTokenRepository refreshTokenRepository)
{
_userRepository = userRepository;
_authHelper = authHelper;
_unitOfWork = unitOfWork;
_refreshTokenRepository = refreshTokenRepository;
}
public async Task<OperationResult<LoginResponse>> Handle(LoginUserCommand request, CancellationToken cancellationToken)
{
// اعتبارسنجی
if (request.UserId <= 0)
{
return OperationResult<LoginResponse>.Failure("شناسه کاربری معتبر نیست", ErrorType.BadRequest);
}
// یافتن کاربر
var user = await _userRepository.GetUserWithRolesByIdAsync(request.UserId, cancellationToken);
if (user == null)
{
return OperationResult<LoginResponse>.Failure("کاربر یافت نشد", ErrorType.NotFound);
}
// بررسی فعال بودن کاربر
if (!user.IsActive)
{
return OperationResult<LoginResponse>.Failure("حساب کاربری غیرفعال است", ErrorType.Unauthorized);
}
// تولید توکن‌ها با استفاده از AuthHelper
var roles = user.RoleUser
.Select(r => r.RoleId.ToString()).ToList();
var session = _authHelper.SignIn(
user.Id,
user.UserName,
user.FullName,
user.AccountId??0,
roles);
// دریافت اطلاعات درخواست با استفاده از AuthHelper
var ipAddress = _authHelper.GetClientIpAddress();
var userAgent = _authHelper.GetUserAgent();
// ذخیره Refresh Token در دیتابیس
//user.AddRefreshToken(refreshToken, refreshTokenExpiration, ipAddress, userAgent);
var refreshTokenEntity = new UserRefreshToken(
user.Id,
session.RefreshToken,
session.RefreshTokenExpiration,
ipAddress,
userAgent);
await _refreshTokenRepository.CreateAsync(refreshTokenEntity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
// ساخت پاسخ (RefreshToken به فرانت داده نمی‌شود)
var response = new LoginResponse
{
AccessToken = session.AccessToken,
ExpiresAt = session.AccessTokenExpiration,
UserId = user.Id,
FullName = user.FullName,
UserName = user.UserName,
Roles = user.RoleUser.Select(r => r.RoleId).ToList()
};
return OperationResult<LoginResponse>.Success(response);
}
}

View File

@@ -1,11 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.RefreshUserToken;
/// <summary>
/// دستور تازه‌سازی توکن دسترسی کاربر
/// </summary>
public record RefreshUserTokenCommand() : IBaseCommand;

View File

@@ -1,86 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using Microsoft.EntityFrameworkCore;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.RefreshUserToken;
/// <summary>
/// Handler برای تازه‌سازی توکن دسترسی
/// </summary>
public class RefreshUserTokenCommandHandler : IBaseCommandHandler<RefreshUserTokenCommand>
{
private readonly IAuthHelper _authHelper;
private readonly IProgramManagerDbContext _context;
public RefreshUserTokenCommandHandler(
IAuthHelper authHelper,
IProgramManagerDbContext context)
{
_authHelper = authHelper;
_context = context;
}
public async Task<OperationResult> Handle(RefreshUserTokenCommand request, CancellationToken cancellationToken)
{
var refreshToken = _authHelper.GetRefreshTokenFromCookie();
// یافتن کاربر و Refresh Token فعال از دیتابیس
var user = await _context.Users
.Include(u => u.RefreshTokens)
.Include(u => u.RoleUser)
.FirstOrDefaultAsync(u => u.RefreshTokens.Any(r=>r.Token ==refreshToken), cancellationToken);
if (user == null)
{
return OperationResult<AccessTokenResponse>.Failure("کاربر یافت نشد", ErrorType.NotFound);
}
// بررسی فعال بودن کاربر
if (!user.IsActive)
{
return OperationResult<AccessTokenResponse>.Failure("حساب کاربری غیرفعال است", ErrorType.Unauthorized);
}
// پیدا کردن Refresh Token فعال
var activeRefreshToken = user.RefreshTokens
.FirstOrDefault(rt => rt.Token == refreshToken && rt.IsActive);
if (activeRefreshToken == null)
{
return OperationResult<AccessTokenResponse>.Failure(
"نشست شما منقضی شده است. لطفاً دوباره وارد شوید",
ErrorType.Unauthorized);
}
if (!activeRefreshToken.IsActive|| activeRefreshToken.IsRevoked||activeRefreshToken.IsExpired)
{
return OperationResult<AccessTokenResponse>.Failure(
"نشست شما منقضی شده است. لطفاً دوباره وارد شوید",
ErrorType.Unauthorized);
}
// تولید Access Token جدید با استفاده از AuthHelper
var roles = user.RoleUser.Select(r => r.RoleId.ToString()).ToList();
var newAccessToken = _authHelper.GenerateAccessToken(
user.Id,
user.UserName,
user.FullName,
user.AccountId,
roles);
var response = new AccessTokenResponse
{
AccessToken = newAccessToken,
ExpiresAt = DateTime.UtcNow.AddMinutes(30),
UserId = user.Id,
FullName = user.FullName,
UserName = user.UserName
};
return OperationResult<AccessTokenResponse>.Success(response);
}
}

View File

@@ -1,11 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.SignOutUser;
/// <summary>
/// دستور خروج کاربر از سیستم
/// </summary>
public record SignOutUserCommand(string RefreshToken) : IBaseCommand;

View File

@@ -1,68 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.SignOutUser;
/// <summary>
/// Handler برای خروج کاربر از سیستم
/// </summary>
public class SignOutUserCommandHandler : IBaseCommandHandler<SignOutUserCommand>
{
private readonly IAuthHelper _authHelper;
private readonly IProgramManagerDbContext _context;
private readonly IUnitOfWork _unitOfWork;
public SignOutUserCommandHandler(
IAuthHelper _authHelper,
IProgramManagerDbContext context,
IUnitOfWork unitOfWork)
{
this._authHelper = _authHelper;
_context = context;
_unitOfWork = unitOfWork;
}
public async Task<OperationResult> Handle(SignOutUserCommand request, CancellationToken cancellationToken)
{
// دریافت UserId از Claims با استفاده از AuthHelper
var userId = _authHelper.GetCurrentUserId();
if (!userId.HasValue)
{
return OperationResult.Failure("کاربر احراز هویت نشده است", ErrorType.Unauthorized);
}
if (string.IsNullOrEmpty(request.RefreshToken))
{
return OperationResult.Failure("توکن تازه‌سازی یافت نشد", ErrorType.BadRequest);
}
// یافتن کاربر
var user = await _context.Users
.Include(u => u.RefreshTokens)
.FirstOrDefaultAsync(u => u.Id == userId.Value, cancellationToken);
if (user == null)
{
return OperationResult.Failure("کاربر یافت نشد", ErrorType.NotFound);
}
try
{
// لغو Refresh Token
user.RevokeRefreshToken(request.RefreshToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
_authHelper.SignOut();
return OperationResult.Success();
}
catch (InvalidOperationException ex)
{
return OperationResult.Failure(ex.Message, ErrorType.BadRequest);
}
}
}

View File

@@ -1,10 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.SsoLogin;
/// <summary>
/// دستور ورود از طریق SSO با استفاده از توکن JWT
/// </summary>
public record SsoLoginCommand(string Token) : IBaseCommand<LoginResponse>;

View File

@@ -1,115 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Application._Common.Models;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
using GozareshgirProgramManager.Domain.UserAgg.Repositories;
using MediatR;
namespace GozareshgirProgramManager.Application.Modules.Users.Commands.SsoLogin;
/// <summary>
/// Handler برای ورود از طریق SSO با استفاده از JWT Token
/// </summary>
public class SsoLoginCommandHandler : IRequestHandler<SsoLoginCommand, OperationResult<LoginResponse>>
{
private readonly IUserRepository _userRepository;
private readonly IUserRefreshTokenRepository _refreshTokenRepository;
private readonly IAuthHelper _authHelper;
private readonly IUnitOfWork _unitOfWork;
public SsoLoginCommandHandler(
IUserRepository userRepository,
IAuthHelper authHelper,
IUnitOfWork unitOfWork,
IUserRefreshTokenRepository refreshTokenRepository)
{
_userRepository = userRepository;
_authHelper = authHelper;
_unitOfWork = unitOfWork;
_refreshTokenRepository = refreshTokenRepository;
}
public async Task<OperationResult<LoginResponse>> Handle(SsoLoginCommand request, CancellationToken cancellationToken)
{
// اعتبارسنجی
if (string.IsNullOrWhiteSpace(request.Token))
{
return OperationResult<LoginResponse>.Failure("توکن SSO معتبر نیست", ErrorType.BadRequest);
}
// اعتبارسنجی توکن و استخراج Claims
var principal = _authHelper.ValidateToken(request.Token);
if (principal == null)
{
return OperationResult<LoginResponse>.Failure("توکن SSO نامعتبر یا منقضی شده است", ErrorType.Unauthorized);
}
// استخراج AccountId از Claims
var accountIdClaim = principal.FindFirst("AccountId")?.Value;
if (string.IsNullOrEmpty(accountIdClaim) || !long.TryParse(accountIdClaim, out var accountId))
{
return OperationResult<LoginResponse>.Failure("AccountId در توکن یافت نشد", ErrorType.BadRequest);
}
// یافتن کاربر بر اساس AccountId
var user = await _userRepository.GetByGozareshgirAccountId(accountId);
if (user == null)
{
return OperationResult<LoginResponse>.Failure("کاربر با AccountId مشخص شده یافت نشد", ErrorType.NotFound);
}
// بررسی فعال بودن کاربر
if (!user.IsActive)
{
return OperationResult<LoginResponse>.Failure("حساب کاربری غیرفعال است", ErrorType.Unauthorized);
}
// بارگذاری نقش‌های کاربر
user = await _userRepository.GetUserWithRolesByIdAsync(user.Id, cancellationToken);
if (user == null)
{
return OperationResult<LoginResponse>.Failure("خطا در بارگذاری اطلاعات کاربر", ErrorType.InternalServerError);
}
// تولید توکن‌های جدید برای کاربر
var roles = user.RoleUser
.Select(r => r.RoleId.ToString()).ToList();
var session = _authHelper.SignIn(
user.Id,
user.UserName,
user.FullName,
user.AccountId ?? 0,
roles);
// دریافت اطلاعات درخواست
var ipAddress = _authHelper.GetClientIpAddress();
var userAgent = _authHelper.GetUserAgent();
// ذخیره Refresh Token در دیتابیس
var refreshTokenEntity = new UserRefreshToken(
user.Id,
session.RefreshToken,
session.RefreshTokenExpiration,
ipAddress,
userAgent);
await _refreshTokenRepository.CreateAsync(refreshTokenEntity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
// ساخت پاسخ
var response = new LoginResponse
{
AccessToken = session.AccessToken,
ExpiresAt = session.AccessTokenExpiration,
UserId = user.Id,
FullName = user.FullName,
UserName = user.UserName,
Roles = user.RoleUser.Select(r => r.RoleId).ToList()
};
return OperationResult<LoginResponse>.Success(response);
}
}

View File

@@ -9,38 +9,38 @@ namespace GozareshgirProgramManager.Application._Common.Interfaces;
public interface IAuthHelper
{
// ==================== Token Generation ====================
LoginSession SignIn(long userId, string userName, string fullName, long accountId, List<string> roles);
/// <summary>
/// تولید Access Token
/// </summary>
string GenerateAccessToken(long userId, string userName, string fullName, long? accountId, List<string> roles);
// LoginSession SignIn(long userId, string userName, string fullName, long accountId, List<string> roles);
// /// <summary>
// /// تولید Access Token
// /// </summary>
// string GenerateAccessToken(long userId, string userName, string fullName, long? accountId, List<string> roles);
/// <summary>
/// تولید Refresh Token
/// </summary>
string GenerateRefreshToken();
// /// <summary>
// /// تولید Refresh Token
// /// </summary>
// string GenerateRefreshToken();
/// <summary>
/// دریافت تاریخ انقضای Refresh Token
/// </summary>
DateTime GetRefreshTokenExpiration();
// /// <summary>
// /// دریافت تاریخ انقضای Refresh Token
// /// </summary>
// DateTime GetRefreshTokenExpiration();
// ==================== Token Validation ====================
/// <summary>
/// اعتبارسنجی توکن و استخراج Claims
/// </summary>
ClaimsPrincipal? ValidateToken(string token);
//
// /// <summary>
// /// اعتبارسنجی توکن و استخراج Claims
// /// </summary>
// ClaimsPrincipal? ValidateToken(string token);
/// <summary>
/// اعتبارسنجی توکن منقضی شده (بدون چک زمان انقضا)
/// </summary>
ClaimsPrincipal? ValidateExpiredToken(string token);
// /// <summary>
// /// اعتبارسنجی توکن منقضی شده (بدون چک زمان انقضا)
// /// </summary>
// ClaimsPrincipal? ValidateExpiredToken(string token);
/// <summary>
/// استخراج UserId از توکن (حتی اگر منقضی شده باشد)
/// </summary>
long? GetUserIdFromToken(string token);
// /// <summary>
// /// استخراج UserId از توکن (حتی اگر منقضی شده باشد)
// /// </summary>
// long? GetUserIdFromToken(string token);
// ==================== HttpContext Helpers ====================
@@ -49,15 +49,11 @@ public interface IAuthHelper
/// </summary>
string? GetClientIpAddress();
/// <summary>
/// دریافت User Agent کاربر جاری
/// </summary>
string? GetUserAgent();
/// <summary>
/// دریافت Refresh Token از Cookie
/// </summary>
string? GetRefreshTokenFromCookie();
// /// <summary>
// /// دریافت Refresh Token از Cookie
// /// </summary>
// string? GetRefreshTokenFromCookie();
// ==================== Current User Claims ====================
@@ -71,39 +67,12 @@ public interface IAuthHelper
/// </summary>
long? GetCurrentUserId();
/// <summary>
/// دریافت نام کاربری جاری از Claims
/// </summary>
string? GetCurrentUserName();
/// <summary>
/// دریافت نام کامل کاربر جاری از Claims
/// </summary>
string? GetCurrentFullName();
/// <summary>
/// دریافت AccountId کاربر جاری از Claims
/// </summary>
long? GetCurrentAccountId();
/// <summary>
/// دریافت نقش‌های کاربر جاری از Claims
/// </summary>
List<string> GetCurrentUserRoles();
// ==================== Role Checking ====================
/// <summary>
/// بررسی دسترسی کاربر به نقش خاص
/// </summary>
bool HasRole(string roleName);
/// <summary>
/// بررسی دسترسی کاربر به یکی از نقش‌ها
/// </summary>
bool HasAnyRole(params string[] roleNames);
void SignOut();
}
public class LoginSession
{

View File

@@ -1,12 +0,0 @@
using GozareshgirProgramManager.Domain.HolidayAgg;
using GozareshgirProgramManager.Domain.HolidayItemAgg;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Application._Common.Interfaces;
public interface IGozareshgirDbContext
{
DbSet<HolidayItem> HolidayItems { get; set; }
DbSet<Holiday> Holidays { get; set; }
}

View File

@@ -42,16 +42,7 @@ public static class DependencyInjection
// Register IAppDbContext
services.AddScoped<IProgramManagerDbContext>(provider =>
provider.GetRequiredService<ProgramManagerDbContext>());
#region GozareshgirDbContext
services.AddDbContext<GozareshgirDbContext>(x => x.UseSqlServer(configuration.GetConnectionString("GozareshgirDb")));
services.AddScoped<IGozareshgirDbContext>(provider =>
provider.GetRequiredService<GozareshgirDbContext>());
#endregion
// Unit of Work
services.AddScoped<IUnitOfWork, UnitOfWork>();

View File

@@ -1,22 +0,0 @@
using GozareshgirProgramManager.Application._Common.Interfaces;
using GozareshgirProgramManager.Domain.HolidayAgg;
using GozareshgirProgramManager.Domain.HolidayItemAgg;
using Microsoft.EntityFrameworkCore;
namespace GozareshgirProgramManager.Infrastructure.Persistence.Context;
public class GozareshgirDbContext : DbContext, IGozareshgirDbContext
{
public GozareshgirDbContext(DbContextOptions<GozareshgirDbContext> options) : base(options)
{
}
public DbSet<HolidayItem> HolidayItems { get; set; } = null!;
public DbSet<Holiday> Holidays { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(GozareshgirDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
}

View File

@@ -149,7 +149,7 @@ public class AuthHelper : IAuthHelper
/// </summary>
public long? GetCurrentUserId()
{
var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst("pm.userId")?.Value;
return long.TryParse(userIdClaim, out var userId) ? userId : null;
}