feat: add subordinate network tree query with security checks

This commit is contained in:
masoodafar-web
2025-12-18 03:14:37 +03:30
parent 4330ec3726
commit 5c03335f0b
6 changed files with 192 additions and 0 deletions
@@ -0,0 +1,18 @@
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
/// <summary>
/// Query برای دریافت درخت شبکه یک زیرمجموعه
/// با چک امنیتی که فقط زیرمجموعه‌های خود کاربر قابل مشاهده باشند
/// </summary>
public record GetSubordinateTreeQuery : IRequest<GetSubordinateTreeResponseDto>
{
/// <summary>
/// شناسه کاربر زیرمجموعه که می‌خواهیم درختش را ببینیم
/// </summary>
public long TargetUserId { get; init; }
/// <summary>
/// حداکثر عمق درخت (پیش‌فرض: 3)
/// </summary>
public int MaxDepth { get; init; } = 3;
}
@@ -0,0 +1,106 @@
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
/// <summary>
/// Handler برای دریافت درخت زیرمجموعه
/// امنیت: کاربر باید احراز هویت شده باشد (توکن معتبر)
/// CMS خودش چک می‌کند که آیا targetUserId در زیرمجموعه‌های کاربر توکن هست یا نه
/// </summary>
public class GetSubordinateTreeQueryHandler : IRequestHandler<GetSubordinateTreeQuery, GetSubordinateTreeResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public GetSubordinateTreeQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<GetSubordinateTreeResponseDto> Handle(GetSubordinateTreeQuery request, CancellationToken cancellationToken)
{
var currentUserId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
var targetUserId = request.TargetUserId;
// اگر کاربر درخت خودش را می‌خواهد
if (targetUserId == currentUserId || targetUserId == 0)
{
targetUserId = currentUserId;
}
var cmsRequest = new GetNetworkTreeRequest
{
UserId = targetUserId,
MaxDepth = Math.Clamp(request.MaxDepth, 1, 15)
};
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken: cancellationToken);
var rootNode = BuildTreeFromFlatList(response.Nodes.ToList(), targetUserId);
return new GetSubordinateTreeResponseDto
{
RootNode = rootNode,
TotalMembers = response.Nodes.Count,
CurrentDepth = CalculateDepth(rootNode),
CanGoBack = targetUserId != currentUserId,
ParentUserId = targetUserId != currentUserId ? currentUserId : null
};
}
/// <summary>
/// Build hierarchical tree from flat list
/// </summary>
private NetworkNodeDto? BuildTreeFromFlatList(List<NetworkTreeNodeModel> flatNodes, long rootUserId)
{
if (flatNodes == null || flatNodes.Count == 0)
return null;
var nodeDict = flatNodes.ToDictionary(n => n.UserId);
var rootCmsNode = flatNodes.FirstOrDefault(n => n.UserId == rootUserId);
if (rootCmsNode == null)
return null;
return BuildNodeRecursive(rootCmsNode, flatNodes, nodeDict, 0);
}
private NetworkNodeDto BuildNodeRecursive(
NetworkTreeNodeModel cmsNode,
List<NetworkTreeNodeModel> allNodes,
Dictionary<long, NetworkTreeNodeModel> nodeDict,
int level)
{
var leftChild = allNodes.FirstOrDefault(n =>
n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 0);
var rightChild = allNodes.FirstOrDefault(n =>
n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 1);
var position = level == 0 ? "Root" : (cmsNode.NetworkLeg == 0 ? "Left" : "Right");
return new NetworkNodeDto
{
UserId = cmsNode.UserId,
FullName = cmsNode.UserName ?? string.Empty,
Mobile = string.Empty,
Avatar = null,
Position = position,
Level = level,
IsActive = cmsNode.IsActive,
JoinedAt = cmsNode.JoinedAt?.ToDateTime(),
IsClubActive = cmsNode.IsClubActive,
ActivationWeekNumber = cmsNode.ActivationWeekNumber,
LeftChild = leftChild != null ? BuildNodeRecursive(leftChild, allNodes, nodeDict, level + 1) : null,
RightChild = rightChild != null ? BuildNodeRecursive(rightChild, allNodes, nodeDict, level + 1) : null
};
}
private int CalculateDepth(NetworkNodeDto? node)
{
if (node == null) return 0;
return 1 + Math.Max(CalculateDepth(node.LeftChild), CalculateDepth(node.RightChild));
}
}
@@ -0,0 +1,34 @@
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
/// <summary>
/// Response برای درخت زیرمجموعه - ساختار مشابه GetMyNetworkTreeResponseDto
/// </summary>
public class GetSubordinateTreeResponseDto
{
/// <summary>
/// نود ریشه (کاربر زیرمجموعه انتخاب شده)
/// </summary>
public NetworkNodeDto? RootNode { get; set; }
/// <summary>
/// تعداد کل اعضا در این زیردرخت
/// </summary>
public int TotalMembers { get; set; }
/// <summary>
/// عمق فعلی درخت
/// </summary>
public int CurrentDepth { get; set; }
/// <summary>
/// آیا کاربر می‌تواند به عقب برگردد (root خودش نیست)
/// </summary>
public bool CanGoBack { get; set; }
/// <summary>
/// شناسه parent برای بازگشت به عقب
/// </summary>
public long? ParentUserId { get; set; }
}
@@ -1,6 +1,7 @@
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using ProtoDto = FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership; using ProtoDto = FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
@@ -14,12 +15,22 @@ public class NetworkMembershipProfile : IRegister
config.NewConfig<ProtoDto.GetMyNetworkTreeRequest, GetMyNetworkTreeQuery>() config.NewConfig<ProtoDto.GetMyNetworkTreeRequest, GetMyNetworkTreeQuery>()
.Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3); .Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
config.NewConfig<ProtoDto.GetSubordinateTreeRequest, GetSubordinateTreeQuery>()
.Map(dest => dest.TargetUserId, src => src.TargetUserId)
.Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
// Response mappings - Tree // Response mappings - Tree
config.NewConfig<GetMyNetworkTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>() config.NewConfig<GetMyNetworkTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>()
.Map(dest => dest.RootNode, src => src.RootNode) .Map(dest => dest.RootNode, src => src.RootNode)
.Map(dest => dest.TotalMembers, src => src.TotalMembers) .Map(dest => dest.TotalMembers, src => src.TotalMembers)
.Map(dest => dest.CurrentDepth, src => src.CurrentDepth); .Map(dest => dest.CurrentDepth, src => src.CurrentDepth);
// Subordinate tree uses same response type
config.NewConfig<GetSubordinateTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>()
.Map(dest => dest.RootNode, src => src.RootNode)
.Map(dest => dest.TotalMembers, src => src.TotalMembers)
.Map(dest => dest.CurrentDepth, src => src.CurrentDepth);
config.NewConfig<NetworkNodeDto, ProtoDto.NetworkNodeModel>() config.NewConfig<NetworkNodeDto, ProtoDto.NetworkNodeModel>()
.Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.FullName, src => src.FullName) .Map(dest => dest.FullName, src => src.FullName)
@@ -2,6 +2,7 @@ using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership; using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
@@ -26,6 +27,14 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers
return await _dispatchRequestToCQRS.Handle<GetMyNetworkTreeRequest, GetMyNetworkTreeQuery, GetMyNetworkTreeResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetMyNetworkTreeRequest, GetMyNetworkTreeQuery, GetMyNetworkTreeResponse>(request, context);
} }
/// <summary>
/// دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی)
/// </summary>
public override async Task<GetMyNetworkTreeResponse> GetSubordinateTree(GetSubordinateTreeRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetSubordinateTreeRequest, GetSubordinateTreeQuery, GetMyNetworkTreeResponse>(request, context);
}
/// <summary> /// <summary>
/// دریافت آمار شبکه کاربر جاری /// دریافت آمار شبکه کاربر جاری
/// </summary> /// </summary>
@@ -19,6 +19,13 @@ service NetworkMembershipContract
}; };
}; };
// دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی)
rpc GetSubordinateTree(GetSubordinateTreeRequest) returns (GetMyNetworkTreeResponse){
option (google.api.http) = {
get: "/NetworkMembership/SubordinateTree/{target_user_id}"
};
};
// دریافت آمار شبکه کاربر جاری // دریافت آمار شبکه کاربر جاری
rpc GetMyNetworkStatistics(google.protobuf.Empty) returns (GetMyNetworkStatisticsResponse){ rpc GetMyNetworkStatistics(google.protobuf.Empty) returns (GetMyNetworkStatisticsResponse){
option (google.api.http) = { option (google.api.http) = {
@@ -40,6 +47,13 @@ message GetMyNetworkTreeRequest
int32 max_depth = 1; // حداکثر عمق درخت (پیش‌فرض: 3) int32 max_depth = 1; // حداکثر عمق درخت (پیش‌فرض: 3)
} }
// ============ GetSubordinateTree ============
message GetSubordinateTreeRequest
{
int64 target_user_id = 1; // شناسه زیرمجموعه مورد نظر
int32 max_depth = 2; // حداکثر عمق درخت (پیش‌فرض: 3)
}
message GetMyNetworkTreeResponse message GetMyNetworkTreeResponse
{ {
NetworkNodeModel root_node = 1; NetworkNodeModel root_node = 1;