apps/meteor/app/api/server/v1/teams.ts
import { Team } from '@rocket.chat/core-services';
import type { ITeam, UserStatus } from '@rocket.chat/core-typings';
import { TEAM_TYPE, isValidSidepanel } from '@rocket.chat/core-typings';
import { Users, Rooms } from '@rocket.chat/models';
import {
isTeamsConvertToChannelProps,
isTeamsRemoveRoomProps,
isTeamsUpdateMemberProps,
isTeamsRemoveMemberProps,
isTeamsAddMembersProps,
isTeamsDeleteProps,
isTeamsLeaveProps,
isTeamsUpdateProps,
isTeamsListChildrenProps,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { canAccessRoomAsync } from '../../../authorization/server';
import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';
API.v1.addRoute(
'teams.list',
{ authRequired: true },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort, query } = await this.parseJsonQuery();
const { records, total } = await Team.list(this.userId, { offset, count }, { sort, query });
return API.v1.success({
teams: records,
total,
count: records.length,
offset,
});
},
},
);
API.v1.addRoute(
'teams.listAll',
{ authRequired: true },
{
async get() {
if (!(await hasPermissionAsync(this.userId, 'view-all-teams'))) {
return API.v1.unauthorized();
}
const { offset, count } = await getPaginationItems(this.queryParams);
const { records, total } = await Team.listAll({ offset, count });
return API.v1.success({
teams: records,
total,
count: records.length,
offset,
});
},
},
);
API.v1.addRoute(
'teams.create',
{ authRequired: true },
{
async post() {
if (!(await hasPermissionAsync(this.userId, 'create-team'))) {
return API.v1.unauthorized();
}
check(
this.bodyParams,
Match.ObjectIncluding({
name: String,
type: Match.OneOf(TEAM_TYPE.PRIVATE, TEAM_TYPE.PUBLIC),
members: Match.Maybe([String]),
room: Match.Maybe(Match.Any),
owner: Match.Maybe(String),
}),
);
const { name, type, members, room, owner, sidepanel } = this.bodyParams;
if (sidepanel?.items && !isValidSidepanel(sidepanel)) {
throw new Error('error-invalid-sidepanel');
}
const team = await Team.create(this.userId, {
team: {
name,
type,
},
room,
members,
owner,
sidepanel,
});
return API.v1.success({ team });
},
},
);
const getTeamByIdOrName = async (params: { teamId: string } | { teamName: string }): Promise<ITeam | null> => {
if ('teamId' in params && params.teamId) {
return Team.getOneById<ITeam>(params.teamId);
}
if ('teamName' in params && params.teamName) {
return Team.getOneByName(params.teamName);
}
return null;
};
API.v1.addRoute(
'teams.convertToChannel',
{
authRequired: true,
validateParams: isTeamsConvertToChannelProps,
},
{
async post() {
const { roomsToRemove = [] } = this.bodyParams;
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasPermissionAsync(this.userId, 'convert-team', team.roomId))) {
return API.v1.unauthorized();
}
const rooms = await Team.getMatchingTeamRooms(team._id, roomsToRemove);
if (rooms.length) {
for await (const room of rooms) {
await Meteor.callAsync('eraseRoom', room);
}
}
await Promise.all([Team.unsetTeamIdOfRooms(this.userId, team._id), Team.removeAllMembersFromTeam(team._id)]);
await Team.deleteById(team._id);
return API.v1.success();
},
},
);
API.v1.addRoute(
'teams.addRooms',
{ authRequired: true },
{
async post() {
check(
this.bodyParams,
Match.OneOf(
Match.ObjectIncluding({
teamId: String,
rooms: [String] as [StringConstructor],
}),
Match.ObjectIncluding({
teamName: String,
rooms: [String] as [StringConstructor],
}),
),
);
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasPermissionAsync(this.userId, 'add-team-channel', team.roomId))) {
return API.v1.unauthorized('error-no-permission-team-channel');
}
const { rooms } = this.bodyParams;
const validRooms = await Team.addRooms(this.userId, rooms, team._id);
return API.v1.success({ rooms: validRooms });
},
},
);
API.v1.addRoute(
'teams.removeRoom',
{
authRequired: true,
validateParams: isTeamsRemoveRoomProps,
},
{
async post() {
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasPermissionAsync(this.userId, 'remove-team-channel', team.roomId))) {
return API.v1.unauthorized();
}
const canRemoveAny = !!(await hasPermissionAsync(this.userId, 'view-all-team-channels', team.roomId));
const { roomId } = this.bodyParams;
const room = await Team.removeRoom(this.userId, roomId, team._id, canRemoveAny);
return API.v1.success({ room });
},
},
);
API.v1.addRoute(
'teams.updateRoom',
{ authRequired: true },
{
async post() {
check(
this.bodyParams,
Match.ObjectIncluding({
roomId: String,
isDefault: Boolean,
}),
);
const { roomId, isDefault } = this.bodyParams;
const team = await Team.getOneByRoomId(roomId);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasPermissionAsync(this.userId, 'edit-team-channel', team.roomId))) {
return API.v1.unauthorized();
}
const canUpdateAny = !!(await hasPermissionAsync(this.userId, 'view-all-team-channels', team.roomId));
const room = await Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny);
return API.v1.success({ room });
},
},
);
API.v1.addRoute(
'teams.listRooms',
{ authRequired: true },
{
async get() {
check(
this.queryParams,
Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
),
);
check(
this.queryParams,
Match.ObjectIncluding({
filter: Match.Maybe(String),
type: Match.Maybe(String),
offset: Match.Maybe(String),
count: Match.Maybe(String),
}),
);
const { filter, type } = this.queryParams;
const { offset, count } = await getPaginationItems(this.queryParams);
const team = await getTeamByIdOrName(this.queryParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
const allowPrivateTeam: boolean = await hasPermissionAsync(this.userId, 'view-all-teams', team.roomId);
let getAllRooms = false;
if (await hasPermissionAsync(this.userId, 'view-all-team-channels', team.roomId)) {
getAllRooms = true;
}
const listFilter = {
name: filter ?? undefined,
isDefault: type === 'autoJoin',
getAllRooms,
allowPrivateTeam,
};
const { records, total } = await Team.listRooms(this.userId, team._id, listFilter, {
offset,
count,
});
return API.v1.success({
rooms: records,
total,
count: records.length,
offset,
});
},
},
);
API.v1.addRoute(
'teams.listRoomsOfUser',
{ authRequired: true },
{
async get() {
check(
this.queryParams,
Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
),
);
check(
this.queryParams,
Match.ObjectIncluding({
userId: String,
canUserDelete: Match.Maybe(String),
offset: Match.Maybe(String),
count: Match.Maybe(String),
}),
);
const { offset, count } = await getPaginationItems(this.queryParams);
const team = await getTeamByIdOrName(this.queryParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
const allowPrivateTeam = await hasPermissionAsync(this.userId, 'view-all-teams', team.roomId);
const { userId, canUserDelete } = this.queryParams;
if (!(this.userId === userId || (await hasPermissionAsync(this.userId, 'view-all-team-channels', team.roomId)))) {
return API.v1.unauthorized();
}
const booleanCanUserDelete = canUserDelete === 'true';
const { records, total } = await Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, booleanCanUserDelete, {
offset,
count,
});
return API.v1.success({
rooms: records,
total,
count: records.length,
offset: 0,
});
},
},
);
const getTeamByIdOrNameOrParentRoom = async (
params: { teamId: string } | { teamName: string } | { roomId: string },
): Promise<Pick<ITeam, 'type' | 'roomId' | '_id'> | null> => {
if ('teamId' in params && params.teamId) {
return Team.getOneById<ITeam>(params.teamId, { projection: { type: 1, roomId: 1 } });
}
if ('teamName' in params && params.teamName) {
return Team.getOneByName(params.teamName, { projection: { type: 1, roomId: 1 } });
}
if ('roomId' in params && params.roomId) {
return Team.getOneByRoomId(params.roomId, { projection: { type: 1, roomId: 1 } });
}
return null;
};
// This should accept a teamId, filter (search by name on rooms collection) and sort/pagination
// should return a list of rooms/discussions from the team. the discussions will only be returned from the main room
API.v1.addRoute(
'teams.listChildren',
{ authRequired: true, validateParams: isTeamsListChildrenProps },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();
const { filter, type } = this.queryParams;
const team = await getTeamByIdOrNameOrParentRoom(this.queryParams);
if (!team) {
return API.v1.notFound();
}
const data = await Team.listChildren(this.userId, team, filter, type, sort, offset, count);
return API.v1.success({ ...data, offset, count });
},
},
);
API.v1.addRoute(
'teams.members',
{ authRequired: true },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
check(
this.queryParams,
Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
),
);
check(
this.queryParams,
Match.ObjectIncluding({
status: Match.Maybe([String]),
username: Match.Maybe(String),
name: Match.Maybe(String),
}),
);
const { status, username, name } = this.queryParams;
const team = await getTeamByIdOrName(this.queryParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
const canSeeAllMembers = await hasPermissionAsync(this.userId, 'view-all-teams', team.roomId);
const query = {
username: username ? new RegExp(escapeRegExp(username), 'i') : undefined,
name: name ? new RegExp(escapeRegExp(name), 'i') : undefined,
status: status ? { $in: status as UserStatus[] } : undefined,
};
const { records, total } = await Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query);
return API.v1.success({
members: records,
total,
count: records.length,
offset,
});
},
},
);
API.v1.addRoute(
'teams.addMembers',
{
authRequired: true,
validateParams: isTeamsAddMembersProps,
},
{
async post() {
const { bodyParams } = this;
const { members } = bodyParams;
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasAtLeastOnePermissionAsync(this.userId, ['add-team-member', 'edit-team-member'], team.roomId))) {
return API.v1.unauthorized();
}
await Team.addMembers(this.userId, team._id, members);
return API.v1.success();
},
},
);
API.v1.addRoute(
'teams.updateMember',
{
authRequired: true,
validateParams: isTeamsUpdateMemberProps,
},
{
async post() {
const { bodyParams } = this;
const { member } = bodyParams;
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasAtLeastOnePermissionAsync(this.userId, ['edit-team-member'], team.roomId))) {
return API.v1.unauthorized();
}
await Team.updateMember(team._id, member);
return API.v1.success();
},
},
);
API.v1.addRoute(
'teams.removeMember',
{
authRequired: true,
validateParams: isTeamsRemoveMemberProps,
},
{
async post() {
const { bodyParams } = this;
const { userId, rooms } = bodyParams;
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasAtLeastOnePermissionAsync(this.userId, ['edit-team-member'], team.roomId))) {
return API.v1.unauthorized();
}
const user = await Users.findOneActiveById(userId, {});
if (!user) {
return API.v1.failure('invalid-user');
}
if (!(await Team.removeMembers(this.userId, team._id, [{ userId }]))) {
return API.v1.failure();
}
if (rooms?.length) {
const roomsFromTeam: string[] = await Team.getMatchingTeamRooms(team._id, rooms);
await Promise.all(
roomsFromTeam.map((rid) =>
removeUserFromRoom(rid, user, {
byUser: this.user,
}),
),
);
}
return API.v1.success();
},
},
);
API.v1.addRoute(
'teams.leave',
{
authRequired: true,
validateParams: isTeamsLeaveProps,
},
{
async post() {
const { rooms = [] } = this.bodyParams;
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
await Team.removeMembers(this.userId, team._id, [
{
userId: this.userId,
},
]);
if (rooms.length) {
const roomsFromTeam: string[] = await Team.getMatchingTeamRooms(team._id, rooms);
await Promise.all(roomsFromTeam.map((rid) => removeUserFromRoom(rid, this.user)));
}
return API.v1.success();
},
},
);
API.v1.addRoute(
'teams.info',
{ authRequired: true },
{
async get() {
check(
this.queryParams,
Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
),
);
const teamInfo = await getTeamByIdOrName(this.queryParams);
if (!teamInfo) {
return API.v1.failure('Team not found');
}
const room = await Rooms.findOneById(teamInfo.roomId);
if (!room) {
return API.v1.failure('Room not found');
}
const canViewInfo =
(await canAccessRoomAsync(room, { _id: this.userId })) || (await hasPermissionAsync(this.userId, 'view-all-teams'));
if (!canViewInfo) {
return API.v1.unauthorized();
}
return API.v1.success({ teamInfo });
},
},
);
API.v1.addRoute(
'teams.delete',
{
authRequired: true,
validateParams: isTeamsDeleteProps,
},
{
async post() {
const { roomsToRemove = [] } = this.bodyParams;
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasPermissionAsync(this.userId, 'delete-team', team.roomId))) {
return API.v1.unauthorized();
}
const rooms: string[] = await Team.getMatchingTeamRooms(team._id, roomsToRemove);
// If we got a list of rooms to delete along with the team, remove them first
if (rooms.length) {
for await (const room of rooms) {
await Meteor.callAsync('eraseRoom', room);
}
}
// Move every other room back to the workspace
await Team.unsetTeamIdOfRooms(this.userId, team._id);
// Remove the team's main room
await Meteor.callAsync('eraseRoom', team.roomId);
// Delete all team memberships
await Team.removeAllMembersFromTeam(team._id);
// And finally delete the team itself
await Team.deleteById(team._id);
return API.v1.success();
},
},
);
API.v1.addRoute(
'teams.autocomplete',
{ authRequired: true },
{
async get() {
check(
this.queryParams,
Match.ObjectIncluding({
name: String,
}),
);
const { name } = this.queryParams;
const teams = await Team.autocomplete(this.userId, name);
return API.v1.success({ teams });
},
},
);
API.v1.addRoute(
'teams.update',
{
authRequired: true,
validateParams: isTeamsUpdateProps,
},
{
async post() {
const { data } = this.bodyParams;
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
if (!(await hasPermissionAsync(this.userId, 'edit-team', team.roomId))) {
return API.v1.unauthorized();
}
await Team.update(this.userId, team._id, data);
return API.v1.success();
},
},
);