diff --git a/backend/src/libs/guards/teamRoles.guard.ts b/backend/src/libs/guards/teamRoles.guard.ts index f2982521e..c88c95744 100644 --- a/backend/src/libs/guards/teamRoles.guard.ts +++ b/backend/src/libs/guards/teamRoles.guard.ts @@ -1,4 +1,4 @@ -import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; @@ -17,9 +17,12 @@ export class TeamUserGuard implements CanActivate { const user = request.user; const teamId: string = request.params.teamId; + try { + const userFound = await this.teamUserModel.findOne({ user: user._id, team: teamId }).exec(); - const userFound = await this.teamUserModel.findOne({ user: user._id, team: teamId }).exec(); - - return user.isSAdmin || permission === userFound?.role; + return user.isSAdmin || permission === userFound?.role; + } catch (error) { + throw new ForbiddenException(); + } } } diff --git a/backend/src/modules/teams/applications/delete.team.application.ts b/backend/src/modules/teams/applications/delete.team.application.ts new file mode 100644 index 000000000..1be6aabed --- /dev/null +++ b/backend/src/modules/teams/applications/delete.team.application.ts @@ -0,0 +1,15 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DeleteTeamApplicationInterface } from '../interfaces/applications/delete.team.application.interface'; +import { TYPES } from '../interfaces/types'; + +@Injectable() +export class DeleteTeamApplication implements DeleteTeamApplicationInterface { + constructor( + @Inject(TYPES.services.DeleteTeamService) + private deleteTeamServices: DeleteTeamApplicationInterface + ) {} + + delete(teamId: string) { + return this.deleteTeamServices.delete(teamId); + } +} diff --git a/backend/src/modules/teams/controller/team.controller.ts b/backend/src/modules/teams/controller/team.controller.ts index cb8d55f52..e335d03d9 100644 --- a/backend/src/modules/teams/controller/team.controller.ts +++ b/backend/src/modules/teams/controller/team.controller.ts @@ -2,6 +2,7 @@ import { BadRequestException, Body, Controller, + Delete, Get, Inject, Param, @@ -41,6 +42,7 @@ import { UnauthorizedResponse } from 'src/libs/swagger/errors/unauthorized.swagg import { TeamUserGuard } from '../../../libs/guards/teamRoles.guard'; import { ForbiddenResponse } from '../../../libs/swagger/errors/forbidden.swagger'; import { NotFoundResponse } from '../../../libs/swagger/errors/not-found.swagger'; +import { DeleteTeamApplication } from '../applications/delete.team.application'; import { UpdateTeamApplication } from '../applications/update.team.application'; import { CreateTeamDto } from '../dto/crate-team.dto'; import TeamDto from '../dto/team.dto'; @@ -62,7 +64,9 @@ export default class TeamsController { @Inject(TYPES.applications.GetTeamApplication) private getTeamApp: GetTeamApplicationInterface, @Inject(TYPES.applications.UpdateTeamApplication) - private updateTeamApp: UpdateTeamApplication + private updateTeamApp: UpdateTeamApplication, + @Inject(TYPES.applications.DeleteTeamApplication) + private deleteTeamApp: DeleteTeamApplication ) {} @ApiOperation({ summary: 'Create a new team' }) @@ -220,4 +224,30 @@ export default class TeamsController { return teamUser; } + + @ApiOperation({ summary: 'Delete a specific team' }) + @ApiParam({ type: String, name: 'teamId', required: true }) + @ApiOkResponse({ type: Boolean, description: 'Team successfully deleted!' }) + @ApiBadRequestResponse({ + description: 'Bad Request', + type: BadRequestResponse + }) + @ApiUnauthorizedResponse({ + description: 'Unauthorized', + type: UnauthorizedResponse + }) + @ApiInternalServerErrorResponse({ + description: 'Internal Server Error', + type: InternalServerErrorResponse + }) + @ApiForbiddenResponse({ + description: 'Forbidden', + type: ForbiddenResponse + }) + @TeamUser(TeamRoles.ADMIN) + @UseGuards(TeamUserGuard) + @Delete(':teamId') + deleteTeam(@Param() { teamId }: TeamParams) { + return this.deleteTeamApp.delete(teamId); + } } diff --git a/backend/src/modules/teams/interfaces/applications/delete.team.application.interface.ts b/backend/src/modules/teams/interfaces/applications/delete.team.application.interface.ts new file mode 100644 index 000000000..651ffa40c --- /dev/null +++ b/backend/src/modules/teams/interfaces/applications/delete.team.application.interface.ts @@ -0,0 +1,3 @@ +export interface DeleteTeamApplicationInterface { + delete(teamId: string): Promise; +} diff --git a/backend/src/modules/teams/interfaces/services/delete.team.service.interface.ts b/backend/src/modules/teams/interfaces/services/delete.team.service.interface.ts new file mode 100644 index 000000000..e13d4ae87 --- /dev/null +++ b/backend/src/modules/teams/interfaces/services/delete.team.service.interface.ts @@ -0,0 +1,4 @@ +export interface DeleteTeamServiceInterface { + // delete doesn't return an object + delete(teamId: string, userId: string): Promise; +} diff --git a/backend/src/modules/teams/interfaces/types.ts b/backend/src/modules/teams/interfaces/types.ts index 2e8f45e71..889d462ca 100644 --- a/backend/src/modules/teams/interfaces/types.ts +++ b/backend/src/modules/teams/interfaces/types.ts @@ -2,14 +2,14 @@ export const TYPES = { services: { CreateTeamService: 'CreateTeamService', GetTeamService: 'GetTeamService', - UpdateTeamService: 'UpdateTeamService' - // DeleteTeamService: 'DeleteTeamService', + UpdateTeamService: 'UpdateTeamService', + DeleteTeamService: 'DeleteTeamService' }, applications: { CreateTeamApplication: 'CreateTeamApplication', GetTeamApplication: 'GetTeamApplication', - UpdateTeamApplication: 'UpdateTeamApplication' - // DeleteBoardApplication: 'DeleteBoardApplication', + UpdateTeamApplication: 'UpdateTeamApplication', + DeleteTeamApplication: 'DeleteTeamApplication' // UpdateBoardApplication: 'UpdateBoardApplication', } }; diff --git a/backend/src/modules/teams/providers.ts b/backend/src/modules/teams/providers.ts index a448fbb14..d2cad2429 100644 --- a/backend/src/modules/teams/providers.ts +++ b/backend/src/modules/teams/providers.ts @@ -1,8 +1,10 @@ import { CreateTeamApplication } from './applications/create.team.application'; +import { DeleteTeamApplication } from './applications/delete.team.application'; import { GetTeamApplication } from './applications/get.team.application'; import { UpdateTeamApplication } from './applications/update.team.application'; import { TYPES } from './interfaces/types'; import CreateTeamService from './services/create.team.service'; +import DeleteTeamService from './services/delete.team.service'; import GetTeamService from './services/get.team.service'; import UpdateTeamService from './services/update.team.service'; @@ -35,3 +37,13 @@ export const updateTeamApplication = { provide: TYPES.applications.UpdateTeamApplication, useClass: UpdateTeamApplication }; + +export const deleteTeamService = { + provide: TYPES.services.DeleteTeamService, + useClass: DeleteTeamService +}; + +export const deleteTeamApplication = { + provide: TYPES.applications.DeleteTeamApplication, + useClass: DeleteTeamApplication +}; diff --git a/backend/src/modules/teams/services/delete.team.service.ts b/backend/src/modules/teams/services/delete.team.service.ts new file mode 100644 index 000000000..7b993998e --- /dev/null +++ b/backend/src/modules/teams/services/delete.team.service.ts @@ -0,0 +1,63 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { ClientSession, Model } from 'mongoose'; +import { DELETE_FAILED } from 'src/libs/exceptions/messages'; +import { DeleteTeamServiceInterface } from '../interfaces/services/delete.team.service.interface'; +import TeamUser, { TeamUserDocument } from '../schemas/team.user.schema'; +import Team, { TeamDocument } from '../schemas/teams.schema'; + +@Injectable() +export default class DeleteTeamService implements DeleteTeamServiceInterface { + constructor( + @InjectModel(Team.name) private teamModel: Model, + @InjectModel(TeamUser.name) private teamUserModel: Model + ) {} + + async delete(teamId: string): Promise { + const teamSession = await this.teamModel.db.startSession(); + teamSession.startTransaction(); + const teamUserSession = await this.teamUserModel.db.startSession(); + teamUserSession.startTransaction(); + + try { + await this.deleteTeam(teamId, teamSession); + await this.deleteTeamUsers(teamId, teamUserSession); + + await teamSession.commitTransaction(); + await teamUserSession.commitTransaction(); + + return true; + } catch (e) { + await teamSession.abortTransaction(); + await teamUserSession.abortTransaction(); + } finally { + await teamSession.endSession(); + await teamUserSession.endSession(); + } + throw new BadRequestException(DELETE_FAILED); + } + + private async deleteTeam(teamId: string, teamSession: ClientSession) { + const result = await this.teamModel.findOneAndRemove( + { + _id: teamId + }, + { session: teamSession } + ); + + if (!result) throw new NotFoundException(DELETE_FAILED); + } + + private async deleteTeamUsers(teamId: string, teamUserSession: ClientSession) { + const { deletedCount } = await this.teamUserModel + .deleteMany( + { + team: teamId + }, + { session: teamUserSession } + ) + .exec(); + + if (deletedCount <= 0) throw new Error(DELETE_FAILED); + } +} diff --git a/backend/src/modules/teams/teams.module.ts b/backend/src/modules/teams/teams.module.ts index 39876ed1a..9a965d594 100644 --- a/backend/src/modules/teams/teams.module.ts +++ b/backend/src/modules/teams/teams.module.ts @@ -7,6 +7,8 @@ import TeamsController from './controller/team.controller'; import { createTeamApplication, createTeamService, + deleteTeamApplication, + deleteTeamService, getTeamApplication, getTeamService, updateTeamApplication, @@ -21,7 +23,9 @@ import { getTeamService, getTeamApplication, updateTeamService, - updateTeamApplication + updateTeamApplication, + deleteTeamApplication, + deleteTeamService ], controllers: [TeamsController], exports: [getTeamApplication, getTeamService, createTeamService, updateTeamService]