From 16b67aa0ff4f0d88be9208098e2a66cfe87b537c Mon Sep 17 00:00:00 2001 From: Gustavo Reis Bauer Date: Wed, 12 Jun 2024 11:10:26 -0300 Subject: [PATCH] feat: Limit of members that can be added using the autojoin feature in a team's channel to the value of the API_Users_Limit setting (#31859) Co-authored-by: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> --- .changeset/clean-moose-cover.md | 6 ++ .../channels/TeamsChannelItem.tsx | 5 +- .../channels/TeamsChannelItemMenu.tsx | 4 +- .../contextualBar/channels/TeamsChannels.tsx | 6 +- .../channels/TeamsChannelsWithData.tsx | 1 + .../channels/hooks/useToggleAutoJoin.tsx | 31 +++++++- apps/meteor/server/services/team/service.ts | 17 +++- apps/meteor/tests/data/teams.helper.ts | 13 ++- apps/meteor/tests/end-to-end/api/25-teams.js | 79 ++++++++++++++++++- packages/i18n/src/locales/en.i18n.json | 2 + 10 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 .changeset/clean-moose-cover.md diff --git a/.changeset/clean-moose-cover.md b/.changeset/clean-moose-cover.md new file mode 100644 index 000000000000..39e6204ce9b4 --- /dev/null +++ b/.changeset/clean-moose-cover.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Introduced the use of the `API_User_Limit` setting to limit amount of members to simultaneously auto-join a room in a team diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx index 981c65630d0a..01f92d38488c 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItem.tsx @@ -22,11 +22,12 @@ import TeamsChannelItemMenu from './TeamsChannelItemMenu'; type TeamsChannelItemProps = { room: IRoom; + mainRoom: IRoom; onClickView: (room: IRoom) => void; reload: () => void; }; -const TeamsChannelItem = ({ room, onClickView, reload }: TeamsChannelItemProps) => { +const TeamsChannelItem = ({ room, mainRoom, onClickView, reload }: TeamsChannelItemProps) => { const t = useTranslation(); const rid = room._id; const type = room.t; @@ -68,7 +69,7 @@ const TeamsChannelItem = ({ room, onClickView, reload }: TeamsChannelItemProps) {(canRemoveTeamChannel || canEditTeamChannel || canDeleteTeamChannel) && ( - {showButton ? : } + {showButton ? : } )} diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx index 54ee216eaaa0..97b1bdceb670 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx @@ -9,12 +9,12 @@ import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom'; import { useRemoveRoomFromTeam } from './hooks/useRemoveRoomFromTeam'; import { useToggleAutoJoin } from './hooks/useToggleAutoJoin'; -const TeamsChannelItemMenu = ({ room, reload }: { room: IRoom; reload?: () => void }) => { +const TeamsChannelItemMenu = ({ room, mainRoom, reload }: { room: IRoom; mainRoom: IRoom; reload?: () => void }) => { const t = useTranslation(); const { handleRemoveRoom, canRemoveTeamChannel } = useRemoveRoomFromTeam(room, { reload }); const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload }); - const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload }); + const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload, mainRoom }); const toggleAutoJoin = { id: 'toggleAutoJoin', diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx index 6d0bfddd8610..d82dd19b1af8 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx @@ -23,6 +23,7 @@ import TeamsChannelItem from './TeamsChannelItem'; type TeamsChannelsProps = { loading: boolean; channels: IRoom[]; + mainRoom: IRoom; text: string; type: 'all' | 'autoJoin'; setType: Dispatch>; @@ -39,6 +40,7 @@ type TeamsChannelsProps = { const TeamsChannels = ({ loading, channels = [], + mainRoom, text, type, setText, @@ -123,7 +125,9 @@ const TeamsChannels = ({ data={channels} // eslint-disable-next-line react/no-multi-comp components={{ Scroller: VirtuosoScrollbars, Footer: () => }} - itemContent={(index, data) => } + itemContent={(index, data) => ( + + )} /> diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx index 965414400ee5..a886479b535b 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelsWithData.tsx @@ -54,6 +54,7 @@ const TeamsChannelsWithData = () => { return ( void }) => { +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +export const useToggleAutoJoin = (room: IRoom, { reload, mainRoom }: { reload?: () => void; mainRoom: IRoom }) => { const dispatchToastMessage = useToastMessageDispatch(); const updateRoomEndpoint = useEndpoint('POST', '/v1/teams.updateRoom'); const canEditTeamChannel = usePermission('edit-team-channel', room._id); + const maxNumberOfAutoJoinMembers = useSetting('API_User_Limit'); const handleToggleAutoJoin = async () => { + // Sanity check, the setting has a default value, therefore it should always be defined + if (!maxNumberOfAutoJoinMembers) { + return; + } + try { - await updateRoomEndpoint({ + const { room: updatedRoom } = await updateRoomEndpoint({ roomId: room._id, isDefault: !room.teamDefault, }); - dispatchToastMessage({ type: 'success', message: room.teamDefault ? 'channel set as non autojoin' : 'channel set as autojoin' }); + if (updatedRoom.teamDefault) { + // If the number of members in the mainRoom (the team) is greater than the limit, show an info message + // informing that not all members will be auto-joined to the channel + const messageType = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'info' : 'success'; + const message = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'Team_Auto-join_exceeded_user_limit' : 'Team_Auto-join_updated'; + + dispatchToastMessage({ + type: messageType, + message: t(message, { + channelName: roomCoordinator.getRoomName(room.t, room), + numberOfMembers: updatedRoom.usersCount, + limit: maxNumberOfAutoJoinMembers, + }), + }); + } reload?.(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 71d403fd84b5..f81b21d7fa01 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -32,6 +32,7 @@ import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; import { checkUsernameAvailability } from '../../../app/lib/server/functions/checkUsernameAvailability'; import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner'; import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUserFromRoom'; +import { settings } from '../../../app/settings/server'; export class TeamService extends ServiceClassInternal implements ITeamService { protected name = 'team'; @@ -472,11 +473,21 @@ export class TeamService extends ServiceClassInternal implements ITeamService { room.teamDefault = isDefault; await Rooms.setTeamDefaultById(rid, isDefault); - if (room.teamDefault) { - const teamMembers = await this.members(uid, room.teamId, true, undefined, undefined); + if (isDefault) { + const maxNumberOfAutoJoinMembers = settings.get('API_User_Limit'); + const teamMembers = await this.members( + uid, + room.teamId, + true, + { offset: 0, count: maxNumberOfAutoJoinMembers }, + // We should not get the owner of the room, since he is already a member + { _id: { $ne: room.u._id } }, + ); for await (const m of teamMembers.records) { - await addUserToRoom(room._id, m.user, user); + if (await addUserToRoom(room._id, m.user, user)) { + room.usersCount++; + } } } diff --git a/apps/meteor/tests/data/teams.helper.ts b/apps/meteor/tests/data/teams.helper.ts index 308fc60f445e..62da06eea71a 100644 --- a/apps/meteor/tests/data/teams.helper.ts +++ b/apps/meteor/tests/data/teams.helper.ts @@ -1,5 +1,6 @@ import { ITeam, TEAM_TYPE } from "@rocket.chat/core-typings" import { api, request } from "./api-data" +import { IUser } from "@rocket.chat/apps-engine/definition/users"; export const createTeam = async (credentials: Record, teamName: string, type: TEAM_TYPE): Promise => { const response = await request.post(api('teams.create')).set(credentials).send({ @@ -14,4 +15,14 @@ export const deleteTeam = async (credentials: Record, teamName: str await request.post(api('teams.delete')).set(credentials).send({ teamName, }); -}; \ No newline at end of file +}; + +export const addMembers = async (credentials: Record, teamName: string, members: IUser['id'][]): Promise => { + await request + .post(api('teams.addMembers')) + .set(credentials) + .send({ + teamName: teamName, + members: members.map((userId) => ({ userId, roles: ['member'] })) + }); +}; diff --git a/apps/meteor/tests/end-to-end/api/25-teams.js b/apps/meteor/tests/end-to-end/api/25-teams.js index 34383e3aaa69..49a1d663aef2 100644 --- a/apps/meteor/tests/end-to-end/api/25-teams.js +++ b/apps/meteor/tests/end-to-end/api/25-teams.js @@ -1,11 +1,11 @@ import { TEAM_TYPE } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it, after } from 'mocha'; +import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; -import { updatePermission } from '../../data/permissions.helper'; +import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; -import { createTeam, deleteTeam } from '../../data/teams.helper'; +import { addMembers, createTeam, deleteTeam } from '../../data/teams.helper'; import { adminUsername, password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; @@ -1594,6 +1594,79 @@ describe('[Teams]', () => { .end(done); }); }); + + describe('team auto-join', () => { + let testTeam; + let createdRoom; + let testUser1; + let testUser2; + + before(async () => { + const [testUserResult, testUser1Result] = await Promise.all([createUser(), createUser()]); + testUser1 = testUserResult; + testUser2 = testUser1Result; + }); + + beforeEach(async () => { + const createTeamPromise = createTeam(credentials, `test-team-name${Date.now()}`, 0); + const createRoomPromise = createRoom({ name: `test-room-name${Date.now()}`, type: 'c' }); + const [testTeamCreationResult, testRoomCreationResult] = await Promise.all([createTeamPromise, createRoomPromise]); + + testTeam = testTeamCreationResult; + createdRoom = testRoomCreationResult; + + await request + .post(api('teams.addRooms')) + .set(credentials) + .expect(200) + .send({ + rooms: [createdRoom.body.channel._id], + teamName: testTeam.name, + }); + }); + + afterEach(() => + Promise.all([deleteTeam(credentials, testTeam.name), deleteRoom({ roomId: createdRoom.body.channel._id, type: 'c' })]), + ); + + after(() => Promise.all([updateSetting('API_User_Limit', 500), deleteUser(testUser1), deleteUser(testUser2)])); + + it('should add members when the members count is less than or equal to the API_User_Limit setting', async () => { + await updateSetting('API_User_Limit', 2); + + await addMembers(credentials, testTeam.name, [testUser1._id, testUser2._id]); + await request + .post(api('teams.updateRoom')) + .set(credentials) + .send({ + roomId: createdRoom.body.channel._id, + isDefault: true, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('room.usersCount').and.to.be.equal(3); + }); + }); + + it('should not add all members when we update a team channel to be auto-join and the members count is greater than the API_User_Limit setting', async () => { + await updateSetting('API_User_Limit', 1); + + await addMembers(credentials, testTeam.name, [testUser1._id, testUser2._id]); + await request + .post(api('teams.updateRoom')) + .set(credentials) + .send({ + roomId: createdRoom.body.channel._id, + isDefault: true, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('room.usersCount').and.to.be.equal(2); + }); + }); + }); }); describe('/teams.removeRoom', () => { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 9c90020d94d0..3a50e7d39df6 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5127,6 +5127,8 @@ "Team_Add_existing_channels": "Add Existing Channels", "Team_Add_existing": "Add Existing", "Team_Auto-join": "Auto-join", + "Team_Auto-join_exceeded_user_limit": "Auto-join has a limit of {{limit}} members, #{{channelName}} now has {{numberOfMembers}} members", + "Team_Auto-join_updated": "#{{channelName}} now has {{numberOfMembers}} members", "Team_Channels": "Team Channels", "Team_Delete_Channel_modal_content_danger": "This can’t be undone.", "Team_Delete_Channel_modal_content": "Would you like to delete this Channel?",