diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index d447567e17d..f04029234bc 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -4,7 +4,7 @@ import { Promise } from 'meteor/promise'; import { API } from '../api'; import { Team } from '../../../../server/sdk'; import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/server'; -import { Subscriptions } from '../../../models/server'; +import { Users } from '../../../models/server'; import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom'; API.v1.addRoute('teams.list', { authRequired: true }, { @@ -126,9 +126,8 @@ API.v1.addRoute('teams.updateRoom', { authRequired: true }, { API.v1.addRoute('teams.listRooms', { authRequired: true }, { get() { - const { teamId, teamName } = this.queryParams; + const { teamId, teamName, filter, type } = this.queryParams; const { offset, count } = this.getPaginationItems(); - const { query } = this.parseJsonQuery(); const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); if (!team) { @@ -142,7 +141,14 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { getAllRooms = true; } - const { records, total } = Promise.await(Team.listRooms(this.userId, team._id, getAllRooms, allowPrivateTeam, { offset, count }, { query })); + const listFilter = { + name: filter, + isDefault: type === 'autoJoin', + getAllRooms, + allowPrivateTeam, + }; + + const { records, total } = Promise.await(Team.listRooms(this.userId, team._id, listFilter, { offset, count })); return API.v1.success({ rooms: records, @@ -241,9 +247,9 @@ API.v1.addRoute('teams.updateMember', { authRequired: true }, { }, }); -API.v1.addRoute('teams.removeMembers', { authRequired: true }, { +API.v1.addRoute('teams.removeMember', { authRequired: true }, { post() { - const { teamId, teamName, members } = this.bodyParams; + const { teamId, teamName, userId, rooms } = this.bodyParams; const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); if (!team) { @@ -254,29 +260,23 @@ API.v1.addRoute('teams.removeMembers', { authRequired: true }, { return API.v1.unauthorized(); } - Promise.await(Team.removeMembers(team._id, members)); - - return API.v1.success(); - }, -}); - -API.v1.addRoute('teams.removeMember', { authRequired: true }, { - post() { - const { teamId, teamName, uid, rooms } = this.bodyParams; - - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); - if (!team) { - return API.v1.failure('team-does-not-exist'); + const user = Users.findOneActiveById(userId, {}); + if (!user) { + return API.v1.failure('invalid-user'); } - if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'], team.roomId)) { - return API.v1.unauthorized(); + if (!Promise.await(Team.removeMembers(team._id, [{ userId }]))) { + return API.v1.failure(); } - Promise.await(Team.removeMembers(team._id, [{ userId: uid }])); - if (rooms?.length) { - rooms.forEach((rid: string) => removeUserFromRoom(rid, { _id: uid })); + const roomsFromTeam: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, rooms)); + + roomsFromTeam.forEach((rid) => { + removeUserFromRoom(rid, user, { + byUser: this.user, + }); + }); } return API.v1.success(); @@ -294,9 +294,15 @@ API.v1.addRoute('teams.leave', { authRequired: true }, { }])); if (rooms?.length) { - Subscriptions.removeByRoomIdsAndUserId(rooms, this.userId); + const roomsFromTeam: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, rooms)); + + roomsFromTeam.forEach((rid) => { + removeUserFromRoom(rid, this.user); + }); } + removeUserFromRoom(team.roomId, this.user); + return API.v1.success(); }, }); @@ -366,9 +372,9 @@ API.v1.addRoute('teams.delete', { authRequired: true }, { API.v1.addRoute('teams.autocomplete', { authRequired: true }, { get() { - const { name, userId } = this.queryParams; + const { name } = this.queryParams; - const teams = Promise.await(Team.autocomplete(userId, name)); + const teams = Promise.await(Team.autocomplete(this.userId, name)); return API.v1.success({ teams }); }, diff --git a/app/models/server/raw/Rooms.js b/app/models/server/raw/Rooms.js index 9dea472df45..9d8318e5958 100644 --- a/app/models/server/raw/Rooms.js +++ b/app/models/server/raw/Rooms.js @@ -118,16 +118,34 @@ export class RoomsRaw extends BaseRaw { return this.find(query, options); } - findByTeamId(teamId, options = {}, query = {}) { - const myQuery = { - ...query, + findByTeamId(teamId, options = {}) { + const query = { + teamId, + teamMain: { + $exists: false, + }, + }; + + return this.find(query, options); + } + + findByTeamIdContainingNameAndDefault(teamId, name, onlyDefault, options = {}) { + const query = { teamId, teamMain: { $exists: false, }, }; - return this.find(myQuery, options); + if (name) { + query.name = new RegExp(escapeRegExp(name), 'i'); + } + + if (onlyDefault) { + query.teamDefault = true; + } + + return this.find(query, options); } findByTeamIdAndRoomsId(teamId, rids, options = {}) { diff --git a/client/contexts/ServerContext/endpoints/v1/teams/listRooms.ts b/client/contexts/ServerContext/endpoints/v1/teams/listRooms.ts index 71053bf624b..261bdf4924e 100644 --- a/client/contexts/ServerContext/endpoints/v1/teams/listRooms.ts +++ b/client/contexts/ServerContext/endpoints/v1/teams/listRooms.ts @@ -3,7 +3,7 @@ import { IRecordsWithTotal } from '../../../../../../definition/ITeam'; export type ListRoomsEndpoint = { - GET: (params: { teamId: string; offset?: number; count?: number; query: string }) => Omit, 'records'> & { + GET: (params: { teamId: string; offset?: number; count?: number; filter: string; type: string }) => Omit, 'records'> & { count: number; offset: number; rooms: IRecordsWithTotal['records']; diff --git a/client/views/room/hooks/useUserInfoActions.js b/client/views/room/hooks/useUserInfoActions.js index 645db7e2413..4a5a0ef5b95 100644 --- a/client/views/room/hooks/useUserInfoActions.js +++ b/client/views/room/hooks/useUserInfoActions.js @@ -300,7 +300,7 @@ export const useUserInfoActions = (user = {}, rid, reload) => { onClose={closeModal} onCancel={closeModal} onConfirm={async (rooms) => { - await removeFromTeam({ teamId: room.teamId, uid, rooms: Object.keys(rooms) }); + await removeFromTeam({ teamId: room.teamId, userId: uid, rooms: Object.keys(rooms) }); closeModal(); reload && reload(); }} diff --git a/client/views/teams/contextualBar/TeamAutocomplete.js b/client/views/teams/contextualBar/TeamAutocomplete.js index d0a3be437ab..d9d199239e0 100644 --- a/client/views/teams/contextualBar/TeamAutocomplete.js +++ b/client/views/teams/contextualBar/TeamAutocomplete.js @@ -3,16 +3,13 @@ import { AutoComplete, Option, Options } from '@rocket.chat/fuselage'; import RoomAvatar from '../../../components/avatar/RoomAvatar'; import { useEndpointData } from '../../../hooks/useEndpointData'; -import { useUserId } from '../../../contexts/UserContext'; const Avatar = ({ _id, type, avatarETag, test, ...props }) => ; const TeamAutocomplete = React.memo((props) => { const [filter, setFilter] = useState(''); - const userId = useUserId(); - - const { value: data } = useEndpointData('teams.autocomplete', useMemo(() => ({ name: filter, userId }), [filter, userId])); + const { value: data } = useEndpointData('teams.autocomplete', useMemo(() => ({ name: filter }), [filter])); const options = useMemo(() => (data && data.teams.map(({ name, teamId, _id, avatarETag, t }) => ({ value: teamId, diff --git a/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts b/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts index 76d53f65dc8..124b2e39cfa 100644 --- a/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts +++ b/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts @@ -35,12 +35,8 @@ export const useTeamsChannelList = ( teamId: options.teamId, offset: start, count: end - start, - query: JSON.stringify({ - name: { $regex: options.text || '', $options: 'i' }, - ...options.type !== 'all' && { - teamDefault: true, - }, - }), + filter: options.text, + type: options.type, }); return { diff --git a/server/methods/browseChannels.js b/server/methods/browseChannels.js index b9472e5839b..9633e2f6f69 100644 --- a/server/methods/browseChannels.js +++ b/server/methods/browseChannels.js @@ -86,8 +86,6 @@ const getChannels = (user, canViewAnon, searchTerm, sort, pagination) => { return room; }); - console.log(results); - return { total, results, diff --git a/server/sdk/types/ITeamService.ts b/server/sdk/types/ITeamService.ts index 4a6ebb22f55..d7038015b86 100644 --- a/server/sdk/types/ITeamService.ts +++ b/server/sdk/types/ITeamService.ts @@ -16,8 +16,7 @@ export interface ITeamCreateParams { } export interface ITeamMemberParams { - userId?: string; - userName?: string; + userId: string; roles?: Array; } @@ -40,11 +39,18 @@ export interface ITeamInfo extends ITeam { numberOfUsers: number; } +export interface IListRoomsFilter { + name: string; + isDefault: boolean; + getAllRooms: boolean; + allowPrivateTeam: boolean; +} + export interface ITeamService { create(uid: string, params: ITeamCreateParams): Promise; addRooms(uid: string, rooms: Array, teamId: string): Promise>; removeRoom(uid: string, rid: string, teamId: string, canRemoveAnyRoom: boolean): Promise; - listRooms(uid: string, teamId: string, getAllRooms: boolean, allowPrivateTeam: boolean, pagination: IPaginationOptions, queryOptions: IQueryOptions): Promise>; + listRooms(uid: string, teamId: string, filter: IListRoomsFilter, pagination: IPaginationOptions): Promise>; listRoomsOfUser(uid: string, teamId: string, userId: string, allowPrivateTeam: boolean, pagination: IPaginationOptions): Promise>; updateRoom(uid: string, rid: string, isDefault: boolean, canUpdateAnyRoom: boolean): Promise; list(uid: string, paginationOptions?: IPaginationOptions, queryOptions?: IQueryOptions): Promise>; @@ -55,7 +61,7 @@ export interface ITeamService { members(uid: string, teamId: string, canSeeAll: boolean, options?: IPaginationOptions, queryOptions?: IQueryOptions): Promise>; addMembers(uid: string, teamId: string, members: Array): Promise; updateMember(teamId: string, members: ITeamMemberParams): Promise; - removeMembers(teamId: string, members: Array): Promise; + removeMembers(teamId: string, members: Array): Promise; getInfoByName(teamName: string): Promise | undefined>; getInfoById(teamId: string): Promise | undefined>; deleteById(teamId: string): Promise; diff --git a/server/services/team/service.ts b/server/services/team/service.ts index 1829f1bbb23..2537cda2381 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -1,23 +1,38 @@ import { Db, FindOneOptions } from 'mongodb'; -import { TeamRaw } from '../../../app/models/server/raw/Team'; -import { ITeam, ITeamMember, TEAM_TYPE, IRecordsWithTotal, IPaginationOptions, IQueryOptions, ITeamStats } from '../../../definition/ITeam'; -import { Room } from '../../sdk'; -import { ITeamCreateParams, ITeamInfo, ITeamMemberInfo, ITeamMemberParams, ITeamService } from '../../sdk/types/ITeamService'; -import { IUser } from '../../../definition/IUser'; -import { ServiceClass } from '../../sdk/types/ServiceClass'; -import { UsersRaw } from '../../../app/models/server/raw/Users'; +import { checkUsernameAvailability } from '../../../app/lib/server/functions'; +import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; +import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner'; +import { Subscriptions } from '../../../app/models/server'; +import { MessagesRaw } from '../../../app/models/server/raw/Messages'; import { RoomsRaw } from '../../../app/models/server/raw/Rooms'; import { SubscriptionsRaw } from '../../../app/models/server/raw/Subscriptions'; -import { Subscriptions } from '../../../app/models/server'; +import { TeamRaw } from '../../../app/models/server/raw/Team'; import { TeamMemberRaw } from '../../../app/models/server/raw/TeamMember'; -import { MessagesRaw } from '../../../app/models/server/raw/Messages'; +import { UsersRaw } from '../../../app/models/server/raw/Users'; import { IRoom } from '../../../definition/IRoom'; -import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; -import { canAccessRoom } from '../authorization/canAccessRoom'; +import { + IPaginationOptions, + IQueryOptions, + IRecordsWithTotal, + ITeam, + ITeamMember, + ITeamStats, + TEAM_TYPE, +} from '../../../definition/ITeam'; +import { IUser } from '../../../definition/IUser'; import { escapeRegExp } from '../../../lib/escapeRegExp'; -import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner'; -import { checkUsernameAvailability } from '../../../app/lib/server/functions'; +import { Room } from '../../sdk'; +import { + IListRoomsFilter, + ITeamCreateParams, + ITeamInfo, + ITeamMemberInfo, + ITeamMemberParams, + ITeamService, +} from '../../sdk/types/ITeamService'; +import { ServiceClass } from '../../sdk/types/ServiceClass'; +import { canAccessRoom } from '../authorization/canAccessRoom'; export class TeamService extends ServiceClass implements ITeamService { protected name = 'team'; @@ -368,7 +383,7 @@ export class TeamService extends ServiceClass implements ITeamService { }; } - async listRooms(uid: string, teamId: string, getAllRooms: boolean, allowPrivateTeam: boolean, { offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 }, { query }: IQueryOptions): Promise> { + async listRooms(uid: string, teamId: string, filter: IListRoomsFilter, { offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 }): Promise> { if (!teamId) { throw new Error('missing-teamId'); } @@ -376,18 +391,22 @@ export class TeamService extends ServiceClass implements ITeamService { if (!team) { throw new Error('invalid-team'); } + + const { getAllRooms, allowPrivateTeam, name, isDefault } = filter; + const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(uid, teamId); if (team.type === TEAM_TYPE.PRIVATE && !allowPrivateTeam && !isMember) { throw new Error('user-not-on-private-team'); } + if (getAllRooms) { - const teamRoomsCursor = this.RoomsModel.findByTeamId(teamId, { skip, limit }, query); + const teamRoomsCursor = this.RoomsModel.findByTeamIdContainingNameAndDefault(teamId, name, isDefault, { skip, limit }); return { total: await teamRoomsCursor.count(), records: await teamRoomsCursor.toArray(), }; } - const teamRooms = await this.RoomsModel.findByTeamId(teamId, { skip, limit, projection: { _id: 1, t: 1 } }, query).toArray(); + const teamRooms = await this.RoomsModel.findByTeamIdContainingNameAndDefault(teamId, name, isDefault, { skip, limit, projection: { _id: 1, t: 1 } }).toArray(); const privateTeamRoomIds = teamRooms.filter((room) => room.t === 'p').map((room) => room._id); const publicTeamRoomIds = teamRooms.filter((room) => room.t === 'c').map((room) => room._id); @@ -499,8 +518,8 @@ export class TeamService extends ServiceClass implements ITeamService { const membersList: Array> = members?.map((member) => ({ teamId, - userId: member.userId ? member.userId : '', - roles: member.roles ? member.roles : [], + userId: member.userId, + roles: member.roles || [], createdAt: new Date(), createdBy, _updatedAt: new Date(), // TODO how to avoid having to do this? @@ -512,10 +531,7 @@ export class TeamService extends ServiceClass implements ITeamService { async updateMember(teamId: string, member: ITeamMemberParams): Promise { if (!member.userId) { - member.userId = await this.Users.findOneByUsername(member.userName); - if (!member.userId) { - throw new Error('invalid-user'); - } + throw new Error('invalid-user'); } const memberUpdate: Partial = { @@ -530,7 +546,7 @@ export class TeamService extends ServiceClass implements ITeamService { await this.TeamMembersModel.deleteByUserIdAndTeamId(userId, teamId); } - async removeMembers(teamId: string, members: Array): Promise { + async removeMembers(teamId: string, members: Array): Promise { const team = await this.TeamModel.findOneById(teamId, { projection: { _id: 1, roomId: 1 } }); if (!team) { throw new Error('team-does-not-exist'); @@ -538,10 +554,7 @@ export class TeamService extends ServiceClass implements ITeamService { for await (const member of members) { if (!member.userId) { - member.userId = await this.Users.findOneByUsername(member.userName); - if (!member.userId) { - throw new Error('invalid-user'); - } + throw new Error('invalid-user'); } const existingMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(member.userId, team._id); @@ -561,6 +574,8 @@ export class TeamService extends ServiceClass implements ITeamService { await this.unsubscribeFromMain(team.roomId, member.userId); } + + return true; } async addMember(inviter: IUser, userId: string, teamId: string): Promise { diff --git a/tests/end-to-end/api/25-teams.js b/tests/end-to-end/api/25-teams.js index 5e3b6e7d87c..9abe7cc56a8 100644 --- a/tests/end-to-end/api/25-teams.js +++ b/tests/end-to-end/api/25-teams.js @@ -381,7 +381,7 @@ describe('[Teams]', () => { }); }); - describe('/teams.removeMembers', () => { + describe('/teams.removeMember', () => { let testTeam; before('Create test team', (done) => { const teamName = `test-team-${ Date.now() }`; @@ -398,15 +398,11 @@ describe('[Teams]', () => { }); it('should not be able to remove the last owner', (done) => { - request.post(api('teams.removeMembers')) + request.post(api('teams.removeMember')) .set(credentials) .send({ teamName: testTeam.name, - members: [ - { - userId: credentials['X-User-Id'], - }, - ], + userId: credentials['X-User-Id'], }) .expect('Content-Type', 'application/json') .expect(400) @@ -436,15 +432,11 @@ describe('[Teams]', () => { ], }) .then(() => - request.post(api('teams.removeMembers')) + request.post(api('teams.removeMember')) .set(credentials) .send({ teamName: testTeam.name, - members: [ - { - userId: testUser2._id, - }, - ], + userId: testUser2._id, }) .expect('Content-Type', 'application/json') .expect(200)