From b5f8e9306c701cfa4703a0b3955e0d9dbf773461 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 12 Mar 2021 14:43:36 -0600 Subject: [PATCH 1/5] Add property to identify main team room Create membership for users when they join main team channel Propagate ownership modifications to the teammember instance --- app/lib/server/functions/addUserToRoom.js | 6 +++ app/models/server/raw/Team.ts | 14 +++++++ app/models/server/raw/TeamMember.ts | 33 +++++++++++++++ definition/ITeam.ts | 1 + server/methods/addRoomOwner.js | 6 +++ server/methods/removeRoomOwner.js | 6 +++ server/sdk/types/IRoomService.ts | 3 +- server/services/team/service.ts | 50 +++++++++++++++++++++-- 8 files changed, 115 insertions(+), 4 deletions(-) diff --git a/app/lib/server/functions/addUserToRoom.js b/app/lib/server/functions/addUserToRoom.js index 69b7bd21f0ad6..1d82e7b1b54dd 100644 --- a/app/lib/server/functions/addUserToRoom.js +++ b/app/lib/server/functions/addUserToRoom.js @@ -4,6 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { AppEvents, Apps } from '../../../apps/server'; import { callbacks } from '../../../callbacks'; import { Messages, Rooms, Subscriptions } from '../../../models'; +import { Team } from '../../../../server/sdk'; import { RoomMemberActions, roomTypes } from '../../../utils/server'; export const addUserToRoom = function(rid, user, inviter, silenced) { @@ -84,5 +85,10 @@ export const addUserToRoom = function(rid, user, inviter, silenced) { }); } + if (room.teamMain && room.teamId) { + // if user is joining to main team channel, create a membership + Promise.await(Team.addMember(inviter, user._id, room.teamId)); + } + return true; }; diff --git a/app/models/server/raw/Team.ts b/app/models/server/raw/Team.ts index 226e745ed81c1..a5b3c057c8a2f 100644 --- a/app/models/server/raw/Team.ts +++ b/app/models/server/raw/Team.ts @@ -25,4 +25,18 @@ export class TeamRaw extends BaseRaw { findOneByName(name: string, options?: FindOneOptions): Promise { return this.col.findOne({ name }, options); } + + findOneByMainRoomId(roomId: string, options?: FindOneOptions): Promise { + return this.col.findOne({ roomId }, options); + } + + updateMainRoomForTeam(id: string, roomId: string) { + return this.col.updateOne({ + _id: id + }, { + $set: { + roomId, + } + }) + } } diff --git a/app/models/server/raw/TeamMember.ts b/app/models/server/raw/TeamMember.ts index 42cfcf4555fd6..352cddc214775 100644 --- a/app/models/server/raw/TeamMember.ts +++ b/app/models/server/raw/TeamMember.ts @@ -2,6 +2,7 @@ import { Collection, FindOneOptions, Cursor } from 'mongodb'; import { BaseRaw } from './BaseRaw'; import { ITeamMember } from '../../../../definition/ITeam'; +import { IUser } from '../../../../definition/IUser'; type T = ITeamMember; export class TeamMemberRaw extends BaseRaw { @@ -30,4 +31,36 @@ export class TeamMemberRaw extends BaseRaw { findByTeamId(teamId: string, options?: FindOneOptions): Cursor { return this.col.find({ teamId }, options); } + + createOneByTeamIdAndUserId(teamId: string, userId: string, createdBy: Pick) { + return this.insertOne({ + teamId, + userId, + createdAt: new Date(), + _updatedAt: new Date(), + createdBy, + }); + } + + updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array) { + return this.col.updateOne({ + teamId, + userId, + }, { + $addToSet: { + roles: { $each: roles }, + }, + }); + } + + removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array) { + return this.col.updateOne({ + teamId, + userId, + }, { + $pull: { + roles: { $in: roles }, + }, + }); + } } diff --git a/definition/ITeam.ts b/definition/ITeam.ts index 34574c212a2f4..2b7392b5c42e5 100644 --- a/definition/ITeam.ts +++ b/definition/ITeam.ts @@ -9,6 +9,7 @@ export enum TEAM_TYPE { export interface ITeam extends IRocketChatRecord { name: string; type: TEAM_TYPE; + roomId?: string; createdBy: Pick; createdAt: Date; } diff --git a/server/methods/addRoomOwner.js b/server/methods/addRoomOwner.js index 91473caef901d..a98405e8e2765 100644 --- a/server/methods/addRoomOwner.js +++ b/server/methods/addRoomOwner.js @@ -3,6 +3,7 @@ import { check } from 'meteor/check'; import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; +import { Team } from '../sdk'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; @@ -57,6 +58,11 @@ Meteor.methods({ role: 'owner', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.addRolesToMember(team._id, userId, ['owner'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'added', diff --git a/server/methods/removeRoomOwner.js b/server/methods/removeRoomOwner.js index b4dcf08ca26cf..d05a9559369bc 100644 --- a/server/methods/removeRoomOwner.js +++ b/server/methods/removeRoomOwner.js @@ -5,6 +5,7 @@ import { hasPermission, getUsersInRole } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ removeRoomOwner(rid, userId) { @@ -64,6 +65,11 @@ Meteor.methods({ role: 'owner', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.removeRolesFromMember(team._id, userId, ['owner'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed', diff --git a/server/sdk/types/IRoomService.ts b/server/sdk/types/IRoomService.ts index eb3862eccf265..ab5a470006be1 100644 --- a/server/sdk/types/IRoomService.ts +++ b/server/sdk/types/IRoomService.ts @@ -12,8 +12,9 @@ interface ICreateRoomOptions extends Partial { +interface ICreateRoomExtraData extends Record { teamId: string; + teamMain: boolean; } export interface ICreateRoomParams { diff --git a/server/services/team/service.ts b/server/services/team/service.ts index 265cfd5e5eec5..d77cab827d4c9 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -1,9 +1,10 @@ -import { Db } from 'mongodb'; +import { Db, InsertOneWriteOpResult } from 'mongodb'; import { TeamRaw } from '../../../app/models/server/raw/Team'; import { ITeam, ITeamMember, TEAM_TYPE, IRecordsWithTotal, IPaginationOptions } from '../../../definition/ITeam'; import { Authorization, Room } from '../../sdk'; import { ITeamCreateParams, 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 { RoomsRaw } from '../../../app/models/server/raw/Rooms'; @@ -67,8 +68,12 @@ export class TeamService extends ServiceClass implements ITeamService { try { const result = await this.TeamModel.insertOne(teamData); const teamId = result.insertedId; + // the same uid can be passed at 3 positions: owner, member list or via caller + // if the owner is present, remove it from the members list + // if the owner is not present, remove the caller from the members list + const excludeFromMembers = owner ? [owner] : [uid]; - const membersList: Array> = members?.filter((memberId) => ![uid, owner].includes(memberId)) + const membersList: Array> = members?.filter((memberId) => !excludeFromMembers.includes(memberId)) .map((memberId) => ({ teamId, userId: memberId, @@ -98,10 +103,13 @@ export class TeamService extends ServiceClass implements ITeamService { extraData: { ...room.extraData, teamId, + teamMain: true, }, }; - await Room.create(owner || uid, newRoom); + const createdRoom = await Room.create(owner || uid, newRoom); + + await this.TeamModel.updateMainRoomForTeam(teamId, createdRoom._id); return { _id: teamId, @@ -159,4 +167,40 @@ export class TeamService extends ServiceClass implements ITeamService { return this.TeamMembersModel.findByTeamId(teamId).toArray(); } + + async addMember({ _id, username }: IUser, userId: string, teamId: string): Promise | null | undefined> { + const isAlreadyAMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); + + if (isAlreadyAMember) { + return; + } + + return this.TeamMembersModel.createOneByTeamIdAndUserId(teamId, userId, { _id, username }); + } + + async getOneByRoomId(roomId: string): Promise { + return this.TeamModel.findOneByMainRoomId(roomId, { projection: { _id: 1 } }) + } + + async addRolesToMember(teamId: string, userId: string, roles: Array) { + const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); + + if (!isMember) { + // TODO should this throw an error instead? + return; + } + + return this.TeamMembersModel.updateRolesByTeamIdAndUserId(teamId, userId, roles); + } + + async removeRolesFromMember(teamId: string, userId: string, roles: Array) { + const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); + + if (!isMember) { + // TODO should this throw an error instead? + return; + } + + return this.TeamMembersModel.removeRolesByTeamIdAndUserId(teamId, userId, roles); + } } From 187571980890f165dbd35d4fe7f057410249323b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 12 Mar 2021 14:51:05 -0600 Subject: [PATCH 2/5] Add missing types --- app/models/server/raw/Team.ts | 10 +++++----- app/models/server/raw/TeamMember.ts | 8 ++++---- server/services/team/service.ts | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/models/server/raw/Team.ts b/app/models/server/raw/Team.ts index a5b3c057c8a2f..a754a63e435f8 100644 --- a/app/models/server/raw/Team.ts +++ b/app/models/server/raw/Team.ts @@ -1,4 +1,4 @@ -import { Collection, FindOneOptions, Cursor } from 'mongodb'; +import { Collection, FindOneOptions, Cursor, UpdateWriteOpResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; import { ITeam } from '../../../../definition/ITeam'; @@ -30,13 +30,13 @@ export class TeamRaw extends BaseRaw { return this.col.findOne({ roomId }, options); } - updateMainRoomForTeam(id: string, roomId: string) { + updateMainRoomForTeam(id: string, roomId: string): Promise { return this.col.updateOne({ - _id: id + _id: id, }, { $set: { roomId, - } - }) + }, + }); } } diff --git a/app/models/server/raw/TeamMember.ts b/app/models/server/raw/TeamMember.ts index 352cddc214775..8b03c27fc87ef 100644 --- a/app/models/server/raw/TeamMember.ts +++ b/app/models/server/raw/TeamMember.ts @@ -1,4 +1,4 @@ -import { Collection, FindOneOptions, Cursor } from 'mongodb'; +import { Collection, FindOneOptions, Cursor, InsertOneWriteOpResult, UpdateWriteOpResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; import { ITeamMember } from '../../../../definition/ITeam'; @@ -32,7 +32,7 @@ export class TeamMemberRaw extends BaseRaw { return this.col.find({ teamId }, options); } - createOneByTeamIdAndUserId(teamId: string, userId: string, createdBy: Pick) { + createOneByTeamIdAndUserId(teamId: string, userId: string, createdBy: Pick): Promise> { return this.insertOne({ teamId, userId, @@ -42,7 +42,7 @@ export class TeamMemberRaw extends BaseRaw { }); } - updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array) { + updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array): Promise { return this.col.updateOne({ teamId, userId, @@ -53,7 +53,7 @@ export class TeamMemberRaw extends BaseRaw { }); } - removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array) { + removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array): Promise { return this.col.updateOne({ teamId, userId, diff --git a/server/services/team/service.ts b/server/services/team/service.ts index d77cab827d4c9..4fe4fd9db3963 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -1,4 +1,4 @@ -import { Db, InsertOneWriteOpResult } from 'mongodb'; +import { Db, InsertOneWriteOpResult, UpdateWriteOpResult } from 'mongodb'; import { TeamRaw } from '../../../app/models/server/raw/Team'; import { ITeam, ITeamMember, TEAM_TYPE, IRecordsWithTotal, IPaginationOptions } from '../../../definition/ITeam'; @@ -179,10 +179,10 @@ export class TeamService extends ServiceClass implements ITeamService { } async getOneByRoomId(roomId: string): Promise { - return this.TeamModel.findOneByMainRoomId(roomId, { projection: { _id: 1 } }) + return this.TeamModel.findOneByMainRoomId(roomId, { projection: { _id: 1 } }); } - async addRolesToMember(teamId: string, userId: string, roles: Array) { + async addRolesToMember(teamId: string, userId: string, roles: Array): Promise { const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); if (!isMember) { @@ -193,7 +193,7 @@ export class TeamService extends ServiceClass implements ITeamService { return this.TeamMembersModel.updateRolesByTeamIdAndUserId(teamId, userId, roles); } - async removeRolesFromMember(teamId: string, userId: string, roles: Array) { + async removeRolesFromMember(teamId: string, userId: string, roles: Array): Promise { const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); if (!isMember) { From e00904e66935abb0182651ad75cd54f2578a345f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 15 Mar 2021 07:47:32 -0600 Subject: [PATCH 3/5] Fix PR comments --- definition/ITeam.ts | 2 +- server/services/team/service.ts | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/definition/ITeam.ts b/definition/ITeam.ts index 2b7392b5c42e5..2ea42cf79692e 100644 --- a/definition/ITeam.ts +++ b/definition/ITeam.ts @@ -9,7 +9,7 @@ export enum TEAM_TYPE { export interface ITeam extends IRocketChatRecord { name: string; type: TEAM_TYPE; - roomId?: string; + roomId: string; createdBy: Pick; createdAt: Date; } diff --git a/server/services/team/service.ts b/server/services/team/service.ts index 4fe4fd9db3963..728a4f78c186a 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -1,4 +1,4 @@ -import { Db, InsertOneWriteOpResult, UpdateWriteOpResult } from 'mongodb'; +import { Db } from 'mongodb'; import { TeamRaw } from '../../../app/models/server/raw/Team'; import { ITeam, ITeamMember, TEAM_TYPE, IRecordsWithTotal, IPaginationOptions } from '../../../definition/ITeam'; @@ -63,6 +63,7 @@ export class TeamService extends ServiceClass implements ITeamService { createdAt: new Date(), createdBy, _updatedAt: new Date(), // TODO how to avoid having to do this? + roomId: '', // this will be populated at the end }; try { @@ -168,39 +169,39 @@ export class TeamService extends ServiceClass implements ITeamService { return this.TeamMembersModel.findByTeamId(teamId).toArray(); } - async addMember({ _id, username }: IUser, userId: string, teamId: string): Promise | null | undefined> { + async addMember({ _id, username }: IUser, userId: string, teamId: string): Promise { const isAlreadyAMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); if (isAlreadyAMember) { - return; + return false; } - return this.TeamMembersModel.createOneByTeamIdAndUserId(teamId, userId, { _id, username }); + return !!await this.TeamMembersModel.createOneByTeamIdAndUserId(teamId, userId, { _id, username }); } async getOneByRoomId(roomId: string): Promise { return this.TeamModel.findOneByMainRoomId(roomId, { projection: { _id: 1 } }); } - async addRolesToMember(teamId: string, userId: string, roles: Array): Promise { + async addRolesToMember(teamId: string, userId: string, roles: Array): Promise { const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); if (!isMember) { // TODO should this throw an error instead? - return; + return false; } - return this.TeamMembersModel.updateRolesByTeamIdAndUserId(teamId, userId, roles); + return !!await this.TeamMembersModel.updateRolesByTeamIdAndUserId(teamId, userId, roles); } - async removeRolesFromMember(teamId: string, userId: string, roles: Array): Promise { + async removeRolesFromMember(teamId: string, userId: string, roles: Array): Promise { const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); if (!isMember) { // TODO should this throw an error instead? - return; + return false; } - return this.TeamMembersModel.removeRolesByTeamIdAndUserId(teamId, userId, roles); + return !!await this.TeamMembersModel.removeRolesByTeamIdAndUserId(teamId, userId, roles); } } From 4b8ffa58a350d6dd81f44e1fa729afc607d96dcf Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 15 Mar 2021 07:51:25 -0600 Subject: [PATCH 4/5] Return team member element on addMember call --- server/services/team/service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/services/team/service.ts b/server/services/team/service.ts index 728a4f78c186a..17970ca7e3bc4 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -169,14 +169,14 @@ export class TeamService extends ServiceClass implements ITeamService { return this.TeamMembersModel.findByTeamId(teamId).toArray(); } - async addMember({ _id, username }: IUser, userId: string, teamId: string): Promise { + async addMember({ _id, username }: IUser, userId: string, teamId: string): Promise { const isAlreadyAMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId, { projection: { _id: 1 } }); if (isAlreadyAMember) { return false; } - return !!await this.TeamMembersModel.createOneByTeamIdAndUserId(teamId, userId, { _id, username }); + return (await this.TeamMembersModel.createOneByTeamIdAndUserId(teamId, userId, { _id, username })).ops[0]; } async getOneByRoomId(roomId: string): Promise { From bb8585eea13fd8e21ca2f1d4f3506aafba55378e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 15 Mar 2021 08:49:47 -0600 Subject: [PATCH 5/5] Reflect role changes on other room role changes --- server/methods/addRoomLeader.js | 6 ++++++ server/methods/addRoomModerator.js | 6 ++++++ server/methods/removeRoomLeader.js | 6 ++++++ server/methods/removeRoomModerator.js | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/server/methods/addRoomLeader.js b/server/methods/addRoomLeader.js index b602c75f984e6..9265815e2620e 100644 --- a/server/methods/addRoomLeader.js +++ b/server/methods/addRoomLeader.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ addRoomLeader(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'leader', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.addRolesToMember(team._id, userId, ['leader'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'added', diff --git a/server/methods/addRoomModerator.js b/server/methods/addRoomModerator.js index 8b430cb667a4f..6d0d3879a9967 100644 --- a/server/methods/addRoomModerator.js +++ b/server/methods/addRoomModerator.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ addRoomModerator(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'moderator', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.addRolesToMember(team._id, userId, ['moderator'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'added', diff --git a/server/methods/removeRoomLeader.js b/server/methods/removeRoomLeader.js index b88a6a7729dc1..77ce4340c4441 100644 --- a/server/methods/removeRoomLeader.js +++ b/server/methods/removeRoomLeader.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ removeRoomLeader(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'leader', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.removeRolesFromMember(team._id, userId, ['leader'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed', diff --git a/server/methods/removeRoomModerator.js b/server/methods/removeRoomModerator.js index ef86a9c176be5..c4237dbda2317 100644 --- a/server/methods/removeRoomModerator.js +++ b/server/methods/removeRoomModerator.js @@ -5,6 +5,7 @@ import { hasPermission } from '../../app/authorization'; import { Users, Subscriptions, Messages } from '../../app/models'; import { settings } from '../../app/settings'; import { api } from '../sdk/api'; +import { Team } from '../sdk'; Meteor.methods({ removeRoomModerator(rid, userId) { @@ -57,6 +58,11 @@ Meteor.methods({ role: 'moderator', }); + const team = Promise.await(Team.getOneByRoomId(rid)); + if (team) { + Promise.await(Team.removeRolesFromMember(team._id, userId, ['moderator'])); + } + if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed',