feat: add subordinate network tree query with security checks
This commit is contained in:
+18
@@ -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;
|
||||||
|
}
|
||||||
+106
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -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>
|
||||||
|
|||||||
+14
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user