From 247a89935ea95f300108c852809d5cb20dc3ee36 Mon Sep 17 00:00:00 2001 From: Thomasr Date: Fri, 20 Jun 2025 18:22:31 -0400 Subject: [PATCH] Get users who do not belong to this group among the members of the organization. Removed "/{orgId}/{searchMemberName}/{searchGroupId}/members" endpoint. --- .../user/repository/UserRepository.java | 9 +++ .../domain/user/service/UserService.java | 10 +-- .../domain/user/service/UserServiceImpl.java | 10 +++ .../api/usermanagement/GroupApiService.java | 3 + .../usermanagement/GroupApiServiceImpl.java | 77 ++++++++++++++++--- .../api/usermanagement/GroupController.java | 17 +++- .../api/usermanagement/GroupEndpoints.java | 15 ++++ .../api/usermanagement/OrgApiService.java | 2 - .../api/usermanagement/OrgApiServiceImpl.java | 72 ----------------- .../OrganizationController.java | 10 --- .../usermanagement/OrganizationEndpoints.java | 7 -- 11 files changed, 121 insertions(+), 111 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/repository/UserRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/repository/UserRepository.java index 9536f52e78..896c278cd7 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/repository/UserRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/repository/UserRepository.java @@ -1,13 +1,16 @@ package org.lowcoder.domain.user.repository; import java.util.Collection; +import java.util.List; import org.lowcoder.domain.user.model.User; +import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.data.mongodb.repository.Query; @Repository public interface UserRepository extends ReactiveMongoRepository { @@ -23,4 +26,10 @@ public interface UserRepository extends ReactiveMongoRepository { //email1 and email2 should be equal Flux findByEmailOrConnections_Email(String email1, String email2); + + @Query("{ '_id': { $in: ?0 }, 'state': ?1, 'isEnabled': ?2, $or: [ { 'name': { $regex: ?3, $options: 'i' } }, { '_id': { $regex: ?3, $options: 'i' } } ] }") + Flux findUsersByIdsAndSearchNameForPagination(Collection ids, String state, boolean isEnabled, String searchRegex, Pageable pageable); + + @Query(value = "{ '_id': { $in: ?0 }, 'state': ?1, 'isEnabled': ?2, $or: [ { 'name': { $regex: ?3, $options: 'i' } }, { '_id': { $regex: ?3, $options: 'i' } } ] }", count = true) + Mono countUsersByIdsAndSearchName(Collection ids, String state, boolean isEnabled, String searchRegex); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java index 52a1ba05c5..e5057ab51a 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserService.java @@ -3,12 +3,10 @@ import java.util.Collection; import java.util.Map; -import org.lowcoder.domain.user.model.AuthUser; -import org.lowcoder.domain.user.model.Connection; -import org.lowcoder.domain.user.model.User; -import org.lowcoder.domain.user.model.UserDetail; +import org.lowcoder.domain.user.model.*; import org.lowcoder.infra.annotation.NonEmptyMono; import org.lowcoder.infra.mongo.MongoUpsertHelper.PartialResourceWithId; +import org.springframework.data.domain.Pageable; import org.springframework.http.codec.multipart.Part; import org.springframework.web.server.ServerWebExchange; @@ -68,5 +66,7 @@ public interface UserService { Flux findBySourceAndIds(String connectionSource, Collection connectionSourceUuids); -} + Flux findUsersByIdsAndSearchNameForPagination(Collection ids, String state, boolean isEnabled, String searchRegex, Pageable pageable); + Mono countUsersByIdsAndSearchName(Collection ids, String state, boolean isEnabled, String searchRegex); +} \ No newline at end of file diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 1a8dcf566e..1df76dfccf 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -36,6 +36,7 @@ import org.lowcoder.sdk.util.HashUtils; import org.lowcoder.sdk.util.LocaleUtils; import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.domain.Pageable; import org.springframework.http.codec.multipart.Part; import org.springframework.stereotype.Service; import org.springframework.web.server.ServerWebExchange; @@ -473,4 +474,13 @@ public Flux findBySourceAndIds(String connectionSource, Collection return repository.findByConnections_SourceAndConnections_RawIdIn(connectionSource, connectionSourceUuids); } + @Override + public Flux findUsersByIdsAndSearchNameForPagination(Collection ids, String state, boolean isEnabled, String searchRegex, Pageable pageable) { + return repository.findUsersByIdsAndSearchNameForPagination(ids, state, isEnabled, searchRegex, pageable); + } + + @Override + public Mono countUsersByIdsAndSearchName(Collection ids, String state, boolean isEnabled, String searchRegex) { + return repository.countUsersByIdsAndSearchName(ids, state, isEnabled, searchRegex); + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java index 549d2105e7..e227e5d117 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiService.java @@ -1,6 +1,7 @@ package org.lowcoder.api.usermanagement; import org.lowcoder.api.usermanagement.view.*; +import org.lowcoder.api.usermanagement.view.OrgMemberListView; import org.lowcoder.domain.group.model.Group; import reactor.core.publisher.Mono; @@ -24,4 +25,6 @@ public interface GroupApiService { Mono update(String groupId, UpdateGroupRequest updateGroupRequest); Mono removeUser(String groupId, String userId); + + Mono getPotentialGroupMembers(String groupId, String searchName, Integer pageNum, Integer pageSize); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java index 55b5f2adb1..3dbd8b6dd1 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupApiServiceImpl.java @@ -9,9 +9,8 @@ import static org.lowcoder.sdk.util.StreamUtils.collectList; import static org.lowcoder.sdk.util.StreamUtils.collectMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; import com.github.f4b6a3.uuid.UuidCreator; @@ -19,24 +18,22 @@ import org.apache.commons.lang3.tuple.Pair; import org.lowcoder.api.bizthreshold.AbstractBizThresholdChecker; import org.lowcoder.api.home.SessionUserService; -import org.lowcoder.api.usermanagement.view.CreateGroupRequest; -import org.lowcoder.api.usermanagement.view.GroupMemberAggregateView; -import org.lowcoder.api.usermanagement.view.GroupMemberView; -import org.lowcoder.api.usermanagement.view.GroupView; -import org.lowcoder.api.usermanagement.view.UpdateGroupRequest; -import org.lowcoder.api.usermanagement.view.UpdateRoleRequest; +import org.lowcoder.api.usermanagement.view.*; import org.lowcoder.domain.group.model.Group; import org.lowcoder.domain.group.model.GroupMember; +import org.lowcoder.domain.user.model.UserState; +import org.lowcoder.api.usermanagement.view.OrgMemberListView; import org.lowcoder.domain.group.service.GroupMemberService; import org.lowcoder.domain.group.service.GroupService; import org.lowcoder.domain.organization.model.MemberRole; import org.lowcoder.domain.organization.model.OrgMember; import org.lowcoder.domain.organization.service.OrgMemberService; -import org.lowcoder.domain.organization.service.OrganizationService; import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.service.UserService; import org.lowcoder.infra.util.TupleUtils; import org.lowcoder.sdk.exception.BizError; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -53,7 +50,6 @@ public class GroupApiServiceImpl implements GroupApiService { private final UserService userService; private final GroupService groupService; private final AbstractBizThresholdChecker bizThresholdChecker; - private final OrganizationService organizationService; private final OrgMemberService orgMemberService; @Override @@ -311,4 +307,63 @@ public Mono removeUser(String groupId, String userId) { return groupMemberService.removeMember(groupId, userId); }); } + + @Override + public Mono getPotentialGroupMembers(String groupId, String searchName, Integer pageNum, Integer pageSize) { + return groupService.getById(groupId) + .flatMap(group -> { + String orgId = group.getOrganizationId(); + Mono> orgMemberUserIdsMono = orgMemberService.getOrganizationMembers(orgId).collectList(); + Mono> groupMemberUserIdsMono = groupMemberService.getGroupMembers(groupId); + + return Mono.zip(orgMemberUserIdsMono, groupMemberUserIdsMono) + .flatMap(tuple -> { + List orgMembers = tuple.getT1(); + List groupMembers = tuple.getT2(); + + Set groupMemberUserIds = groupMembers.stream() + .map(GroupMember::getUserId) + .collect(Collectors.toSet()); + + Collection potentialUserIds = orgMembers.stream() + .map(OrgMember::getUserId) + .filter(uid -> !groupMemberUserIds.contains(uid)) + .collect(Collectors.toList()); + + if (potentialUserIds.isEmpty()) { + return Mono.just(OrgMemberListView.builder() + .members(List.of()) + .total(0) + .pageNum(pageNum) + .pageSize(pageSize) + .build()); + } + + Pageable pageable = PageRequest.of(pageNum - 1, pageSize); + String searchRegex = searchName != null && !searchName.isBlank() ? ".*" + Pattern.quote(searchName) + ".*" : ".*"; + + return userService.findUsersByIdsAndSearchNameForPagination( + potentialUserIds, String.valueOf(UserState.ACTIVATED), true, searchRegex, pageable) + .collectList() + .zipWith(userService.countUsersByIdsAndSearchName( + potentialUserIds, String.valueOf(UserState.ACTIVATED), true, searchRegex)) + .map(tupleUser -> { + List users = tupleUser.getT1(); + long total = tupleUser.getT2(); + List memberViews = users.stream() + .map(u -> OrgMemberListView.OrgMemberView.builder() + .userId(u.getId()) + .name(u.getName()) + .build()) + .collect(Collectors.toList()); + return OrgMemberListView.builder() + .members(memberViews) + .total((int) total) + .pageNum(pageNum) + .pageSize(pageSize) + .build(); + }); + }); + }); + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java index 110a838378..992bba1fe0 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java @@ -20,10 +20,8 @@ import org.lowcoder.domain.organization.service.OrgMemberService; import org.lowcoder.sdk.exception.BizError; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.lowcoder.api.usermanagement.view.OrgMemberListView; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -180,4 +178,15 @@ public Mono> removeUser(@PathVariable String groupId, .map(Tuple2::getT2) .map(ResponseView::success)); } + + @Override + public Mono> searchPotentialGroupMembers( + @PathVariable String groupId, + @RequestParam(required = false) String searchName, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "1000") Integer pageSize) { + return gidService.convertGroupIdToObjectId(groupId).flatMap(id -> + groupApiService.getPotentialGroupMembers(id, searchName, pageNum, pageSize) + .map(ResponseView::success)); + } } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java index 89e294628d..3a4b90d567 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java @@ -115,4 +115,19 @@ public Mono> updateRoleForMember(@RequestBody UpdateRoleRe @DeleteMapping("/{groupId}/remove") public Mono> removeUser(@PathVariable String groupId, @RequestParam String userId); + + @Operation( + tags = TAG_GROUP_MEMBERS, + operationId = "searchPotentialGroupMembers", + summary = "Search Potential Group Members", + description = "Retrieve a list of users who are not currently members of the specified group within an organization." + ) + + @GetMapping("/{groupId}/potential-members") + public Mono> searchPotentialGroupMembers( + @PathVariable String groupId, + @RequestParam(required = false) String searchName, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, + @RequestParam(required = false, defaultValue = "1000") Integer pageSize + ); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java index c87732d35c..2901aeb0dc 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiService.java @@ -53,7 +53,5 @@ public interface OrgApiService { Mono getOrganizationConfigs(String orgId); Mono getApiUsageCount(String orgId, Boolean lastMonthOnly); - - Mono getOrganizationMembersForSearch(String orgId, String searchMemberName, String searchGroupId, Integer pageNum, Integer pageSize); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java index 2a5b0d0c30..2d2b6cd2d5 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrgApiServiceImpl.java @@ -90,78 +90,6 @@ public Mono getOrganizationMembers(String orgId, int page, in .then(getOrgMemberListView(orgId, page, count)); } -// Update getOrgMemberListViewForSearch to filter by group membership -private Mono getOrgMemberListViewForSearch(String orgId, String searchMemberName, String searchGroupId, Integer page, Integer pageSize) { - return orgMemberService.getOrganizationMembers(orgId) - .collectList() - .flatMap(orgMembers -> { - List userIds = orgMembers.stream() - .map(OrgMember::getUserId) - .collect(Collectors.toList()); - Mono> users = userService.getByIds(userIds); - - // If searchGroupId is provided, fetch group members - Mono> groupUserIdsMono = StringUtils.isBlank(searchGroupId) - ? Mono.just(Collections.emptySet()) - : groupMemberService.getGroupMembers(searchGroupId) - .map(list -> list.stream() - .map(GroupMember::getUserId) - .collect(Collectors.toSet())); - - return Mono.zip(users, groupUserIdsMono) - .map(tuple -> { - Map userMap = tuple.getT1(); - Set groupUserIds = tuple.getT2(); - - var list = orgMembers.stream() - .map(orgMember -> { - User user = userMap.get(orgMember.getUserId()); - if (user == null) { - log.warn("user {} not exist and will be removed from the result.", orgMember.getUserId()); - return null; - } - return buildOrgMemberView(user, orgMember); - }) - .filter(Objects::nonNull) - .filter(orgMemberView -> { - // Filter by name - boolean matchesName = StringUtils.isBlank(searchMemberName) || - StringUtils.containsIgnoreCase(orgMemberView.getName(), searchMemberName); - - // Filter by group - boolean matchesGroup = StringUtils.isBlank(searchGroupId) || - groupUserIds.contains(orgMemberView.getUserId()); - - return matchesName && matchesGroup; - }) - .collect(Collectors.toList()); - var pageTotal = list.size(); - list = list.subList((page - 1) * pageSize, pageSize == 0 ? pageTotal : Math.min(page * pageSize, pageTotal)); - return Pair.of(list, pageTotal); - }); - }) - .zipWith(sessionUserService.getVisitorOrgMemberCache()) - .map(tuple -> { - List memberViews = tuple.getT1().getLeft(); - var pageTotal = tuple.getT1().getRight(); - OrgMember orgMember = tuple.getT2(); - return OrgMemberListView.builder() - .members(memberViews) - .total(pageTotal) - .pageNum(page) - .pageSize(pageSize) - .visitorRole(orgMember.getRole().getValue()) - .build(); - }); - } - @Override - public Mono getOrganizationMembersForSearch(String orgId, String searchMemberName, String searchGroupId, Integer page, Integer pageSize) { - return sessionUserService.getVisitorId() - .flatMap(visitorId -> orgMemberService.getOrgMember(orgId, visitorId)) - .switchIfEmpty(deferredError(BizError.NOT_AUTHORIZED, "NOT_AUTHORIZED")) - .then(getOrgMemberListViewForSearch(orgId, searchMemberName, searchGroupId, page, pageSize)); - } - private Mono getOrgMemberListView(String orgId, int page, int count) { return orgMemberService.getOrganizationMembers(orgId) .collectList() diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java index f73758127d..15637f364b 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java @@ -118,16 +118,6 @@ public Mono> getOrgMembers(@PathVariable String orgApiService.getOrganizationMembers(id, pageNum, pageSize) .map(ResponseView::success)); } - @Override - public Mono> getOrgMembersForSearch(@PathVariable String orgId, - @PathVariable String searchMemberName, - @PathVariable String searchGroupId, - @RequestParam(required = false, defaultValue = "1") int pageNum, - @RequestParam(required = false, defaultValue = "1000") int pageSize) { - return gidService.convertOrganizationIdToObjectId(orgId).flatMap(id -> - orgApiService.getOrganizationMembersForSearch(id, searchMemberName, searchGroupId, pageNum, pageSize) - .map(ResponseView::success)); - } @Override public Mono> updateRoleForMember(@RequestBody UpdateRoleRequest updateRoleRequest, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java index 6fee2a511f..86ed6888b2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java @@ -98,13 +98,6 @@ public Mono> getOrgMembers(@PathVariable String @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "1000") int pageSize); - @GetMapping("/{orgId}/{searchMemberName}/{searchGroupId}/members") - public Mono> getOrgMembersForSearch(@PathVariable String orgId, - @PathVariable String searchMemberName, - @PathVariable String searchGroupId, - @RequestParam(required = false, defaultValue = "1") int pageNum, - @RequestParam(required = false, defaultValue = "1000") int pageSize); - @Operation( tags = TAG_ORGANIZATION_MEMBERS, operationId = "updateOrganizationMemberRole",