Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 65 additions & 6 deletions app/api/server/v1/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Promise } from 'meteor/promise';

import { API } from '../api';
import { Team } from '../../../../server/sdk';
import { hasPermission } from '../../../authorization/server';
import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/server';

API.v1.addRoute('teams.list', { authRequired: true }, {
get() {
Expand Down Expand Up @@ -40,6 +40,9 @@ API.v1.addRoute('teams.listAll', { authRequired: true }, {

API.v1.addRoute('teams.create', { authRequired: true }, {
post() {
if (!hasPermission(this.userId, 'create-team')) {
return API.v1.unauthorized();
}
const { name, type, members, room, owner } = this.bodyParams;

if (!name) {
Expand All @@ -62,15 +65,71 @@ API.v1.addRoute('teams.create', { authRequired: true }, {

API.v1.addRoute('teams.members', { authRequired: true }, {
get() {
const { teamId } = this.queryParams;
const { offset, count } = this.getPaginationItems();
const { teamId, teamName } = this.queryParams;

const { records, total } = Promise.await(Team.members(teamId, teamName, { offset, count }));

return API.v1.success({
members: records,
total,
count: records.length,
offset,
});
},
});

API.v1.addRoute('teams.addMembers', { authRequired: true }, {
post() {
if (!hasAtLeastOnePermission(this.userId, ['add-team-member', 'edit-team-member'])) {
return API.v1.unauthorized();
}

const { teamId, teamName, members } = this.bodyParams;

Promise.await(Team.addMembers(this.userId, teamId, teamName, members));

return API.v1.success();
},
});

API.v1.addRoute('teams.updateMember', { authRequired: true }, {
post() {
if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) {
return API.v1.unauthorized();
}

const { teamId, teamName, member } = this.bodyParams;

Promise.await(Team.updateMember(teamId, teamName, member));

return API.v1.success();
},
});

if (!teamId) {
return API.v1.failure('Team ID is required');
API.v1.addRoute('teams.removeMembers', { authRequired: true }, {
post() {
if (!hasAtLeastOnePermission(this.userId, ['edit-team-member'])) {
return API.v1.unauthorized();
}

const members = Promise.await(Team.members(this.userId, teamId));
const { teamId, teamName, members } = this.bodyParams;

Promise.await(Team.removeMembers(teamId, teamName, members));

return API.v1.success();
},
});

API.v1.addRoute('teams.leave', { authRequired: true }, {
post() {
const { teamId, teamName } = this.bodyParams;

Promise.await(Team.removeMembers(teamId, teamName, [{
userId: this.userId,
}]));

return API.v1.success({ members });
return API.v1.success();
},
});

Expand Down
4 changes: 4 additions & 0 deletions app/models/server/raw/TeamMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export class TeamMemberRaw extends BaseRaw<T> {
return this.col.find({ teamId }, options);
}

updateOneByUserIdAndTeamId(userId: string, teamId: string, update: Partial<T>): Promise<UpdateWriteOpResult> {
return this.col.updateOne({ userId, teamId }, { $set: update });
}

createOneByTeamIdAndUserId(teamId: string, userId: string, createdBy: Pick<IUser, '_id' | 'username'>): Promise<InsertOneWriteOpResult<T>> {
return this.insertOne({
teamId,
Expand Down
11 changes: 10 additions & 1 deletion server/sdk/types/ITeamService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,20 @@ export interface ITeamCreateParams {
owner?: string; // the team owner. If not present, owner = requester
}

export interface ITeamMemberParams {
userId?: string;
userName?: string;
roles?: Array<string>;
}

export interface ITeamService {
create(uid: string, params: ITeamCreateParams): Promise<ITeam>;
list(uid: string, options?: IPaginationOptions): Promise<IRecordsWithTotal<ITeam>>;
listAll(options?: IPaginationOptions): Promise<IRecordsWithTotal<ITeam>>;
members(uid: string, teamId: string): Promise<Array<ITeamMember>>;
members(teamId: string, teamName: string, options?: IPaginationOptions): Promise<IRecordsWithTotal<ITeamMember>>;
addMembers(uid: string, teamId: string, teamName: string, members: Array<ITeamMemberParams>): Promise<void>;
updateMember(teamId: string, teamName: string, members: ITeamMemberParams): Promise<void>;
removeMembers(teamId: string, teamName: string, members: Array<ITeamMemberParams>): Promise<void>;
getInfoByName(teamName: string): Promise<Partial<ITeam> | undefined>;
getInfoById(teamId: string): Promise<Partial<ITeam> | undefined>;
}
111 changes: 96 additions & 15 deletions server/services/team/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Db } 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 { Room } from '../../sdk';
import { ITeamCreateParams, 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';
Expand Down Expand Up @@ -33,11 +33,6 @@ export class TeamService extends ServiceClass implements ITeamService {
}

async create(uid: string, { team, room = { name: team.name, extraData: {} }, members, owner }: ITeamCreateParams): Promise<ITeam> {
const hasPermission = await Authorization.hasPermission(uid, 'create-team');
if (!hasPermission) {
throw new Error('no-permission');
}

const existingTeam = await this.TeamModel.findOneByName(team.name, { projection: { _id: 1 } });
if (existingTeam) {
throw new Error('team-name-already-exists');
Expand Down Expand Up @@ -155,18 +150,104 @@ export class TeamService extends ServiceClass implements ITeamService {
};
}

async members(userId: string, teamId: string): Promise<Array<ITeamMember>> {
const isMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(userId, teamId);
const hasPermission = await Authorization.hasAtLeastOnePermission(userId, ['add-team-member', 'edit-team-member', 'view-all-teams']);
if (!hasPermission) {
throw new Error('no-permission');
async members(teamId: string, teamName: string, { offset, count }: IPaginationOptions = { offset: 0, count: 50 }): Promise<IRecordsWithTotal<ITeamMember>> {
if (!teamId) {
const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } });
if (!teamIdName) {
throw new Error('team-does-not-exist');
}

teamId = teamIdName._id;
}

const cursor = this.TeamMembersModel.findByTeamId(teamId, {
limit: count,
skip: offset,
});

return {
total: await cursor.count(),
records: await cursor.toArray(),
};
}

async addMembers(uid: string, teamId: string, teamName: string, members: Array<ITeamMemberParams>): Promise<void> {
const createdBy = await this.Users.findOneById(uid, { projection: { username: 1 } });
if (!createdBy) {
throw new Error('invalid-user');
}

if (!teamId) {
const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } });
if (!teamIdName) {
throw new Error('team-does-not-exist');
}

teamId = teamIdName._id;
}

const membersList: Array<Omit<ITeamMember, '_id'>> = members?.map((member) => ({
teamId,
userId: member.userId ? member.userId : '',
roles: member.roles ? member.roles : [],
createdAt: new Date(),
createdBy,
_updatedAt: new Date(), // TODO how to avoid having to do this?
})) || [];

await this.TeamMembersModel.insertMany(membersList);
}

async updateMember(teamId: string, teamName: string, member: ITeamMemberParams): Promise<void> {
if (!teamId) {
const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } });
if (!teamIdName) {
throw new Error('team-does-not-exist');
}

teamId = teamIdName._id;
}

if (!isMember && !hasPermission) {
return [];
if (!member.userId) {
member.userId = await this.Users.findOneByUsername(member.userName);
if (!member.userId) {
throw new Error('invalid-user');
}
}

return this.TeamMembersModel.findByTeamId(teamId).toArray();
const memberUpdate: Partial<ITeamMember> = {
roles: member.roles ? member.roles : [],
_updatedAt: new Date(),
};

await this.TeamMembersModel.updateOneByUserIdAndTeamId(member.userId, teamId, memberUpdate);
}

async removeMembers(teamId: string, teamName: string, members: Array<ITeamMemberParams>): Promise<void> {
if (!teamId) {
const teamIdName = await this.TeamModel.findOneByName(teamName, { projection: { _id: 1 } });
if (!teamIdName) {
throw new Error('team-does-not-exist');
}

teamId = teamIdName._id;
}

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');
}
}

const existingMember = await this.TeamMembersModel.findOneByUserIdAndTeamId(member.userId, teamId);
if (!existingMember) {
throw new Error('member-does-not-exist');
}

this.TeamMembersModel.removeById(existingMember._id);
}
}

async addMember({ _id, username }: IUser, userId: string, teamId: string): Promise<boolean | ITeamMember> {
Expand Down
Loading