Regression: General improvement to Teams (#21402)

pull/21419/head
Diego Sampaio 5 years ago committed by GitHub
parent d964cd924e
commit 8eb3ef4ec5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      app/api/server/v1/teams.ts
  2. 26
      app/models/server/raw/Rooms.js
  3. 2
      client/contexts/ServerContext/endpoints/v1/teams/listRooms.ts
  4. 2
      client/views/room/hooks/useUserInfoActions.js
  5. 5
      client/views/teams/contextualBar/TeamAutocomplete.js
  6. 8
      client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts
  7. 2
      server/methods/browseChannels.js
  8. 14
      server/sdk/types/ITeamService.ts
  9. 69
      server/services/team/service.ts
  10. 18
      tests/end-to-end/api/25-teams.js

@ -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 });
},

@ -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 = {}) {

@ -3,7 +3,7 @@ import { IRecordsWithTotal } from '../../../../../../definition/ITeam';
export type ListRoomsEndpoint = {
GET: (params: { teamId: string; offset?: number; count?: number; query: string }) => Omit<IRecordsWithTotal<IRoom>, 'records'> & {
GET: (params: { teamId: string; offset?: number; count?: number; filter: string; type: string }) => Omit<IRecordsWithTotal<IRoom>, 'records'> & {
count: number;
offset: number;
rooms: IRecordsWithTotal<IRoom>['records'];

@ -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();
}}

@ -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 }) => <RoomAvatar size={Options.AvatarSize} room={{ type, _id, avatarETag }} {...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,

@ -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 {

@ -86,8 +86,6 @@ const getChannels = (user, canViewAnon, searchTerm, sort, pagination) => {
return room;
});
console.log(results);
return {
total,
results,

@ -16,8 +16,7 @@ export interface ITeamCreateParams {
}
export interface ITeamMemberParams {
userId?: string;
userName?: string;
userId: string;
roles?: Array<string>;
}
@ -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<ITeam>;
addRooms(uid: string, rooms: Array<string>, teamId: string): Promise<Array<IRoom>>;
removeRoom(uid: string, rid: string, teamId: string, canRemoveAnyRoom: boolean): Promise<IRoom>;
listRooms(uid: string, teamId: string, getAllRooms: boolean, allowPrivateTeam: boolean, pagination: IPaginationOptions, queryOptions: IQueryOptions<IRoom>): Promise<IRecordsWithTotal<IRoom>>;
listRooms(uid: string, teamId: string, filter: IListRoomsFilter, pagination: IPaginationOptions): Promise<IRecordsWithTotal<IRoom>>;
listRoomsOfUser(uid: string, teamId: string, userId: string, allowPrivateTeam: boolean, pagination: IPaginationOptions): Promise<IRecordsWithTotal<IRoom>>;
updateRoom(uid: string, rid: string, isDefault: boolean, canUpdateAnyRoom: boolean): Promise<IRoom>;
list(uid: string, paginationOptions?: IPaginationOptions, queryOptions?: IQueryOptions<ITeam>): Promise<IRecordsWithTotal<ITeam>>;
@ -55,7 +61,7 @@ export interface ITeamService {
members(uid: string, teamId: string, canSeeAll: boolean, options?: IPaginationOptions, queryOptions?: IQueryOptions<ITeamMember>): Promise<IRecordsWithTotal<ITeamMemberInfo>>;
addMembers(uid: string, teamId: string, members: Array<ITeamMemberParams>): Promise<void>;
updateMember(teamId: string, members: ITeamMemberParams): Promise<void>;
removeMembers(teamId: string, members: Array<ITeamMemberParams>): Promise<void>;
removeMembers(teamId: string, members: Array<ITeamMemberParams>): Promise<boolean>;
getInfoByName(teamName: string): Promise<Partial<ITeam> | undefined>;
getInfoById(teamId: string): Promise<Partial<ITeam> | undefined>;
deleteById(teamId: string): Promise<boolean>;

@ -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<IRoom>): Promise<IRecordsWithTotal<IRoom>> {
async listRooms(uid: string, teamId: string, filter: IListRoomsFilter, { offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 }): Promise<IRecordsWithTotal<IRoom>> {
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<Omit<ITeamMember, '_id'>> = 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<void> {
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<ITeamMember> = {
@ -530,7 +546,7 @@ export class TeamService extends ServiceClass implements ITeamService {
await this.TeamMembersModel.deleteByUserIdAndTeamId(userId, teamId);
}
async removeMembers(teamId: string, members: Array<ITeamMemberParams>): Promise<void> {
async removeMembers(teamId: string, members: Array<ITeamMemberParams>): Promise<boolean> {
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<boolean> {

@ -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)

Loading…
Cancel
Save