diff --git a/api/src/modules/custom-projects/custom-projects.controller.ts b/api/src/modules/custom-projects/custom-projects.controller.ts index a00ce809..72f96eba 100644 --- a/api/src/modules/custom-projects/custom-projects.controller.ts +++ b/api/src/modules/custom-projects/custom-projects.controller.ts @@ -144,4 +144,31 @@ export class CustomProjectsController { }, ); } + + @UseGuards(AuthGuard('jwt'), RolesGuard) + @RequiredRoles(ROLES.PARTNER, ROLES.ADMIN) + @TsRestHandler(customProjectContract.deleteCustomProjects) + async deleteCustomProjects( + @GetUser() user: User, + @Body() body: { ids: string[] }, + ): Promise { + return tsRestHandler( + customProjectContract.deleteCustomProjects, + async () => { + if ( + !(await this.customProjects.canUserDeleteProjects(user.id, body.ids)) + ) { + return { + status: 401, + body: null, + }; + } + await this.customProjects.removeMany(body.ids); + return { + status: 200, + body: null, + }; + }, + ); + } } diff --git a/api/src/modules/custom-projects/custom-projects.service.ts b/api/src/modules/custom-projects/custom-projects.service.ts index e900a875..49908f05 100644 --- a/api/src/modules/custom-projects/custom-projects.service.ts +++ b/api/src/modules/custom-projects/custom-projects.service.ts @@ -22,6 +22,7 @@ import { FetchSpecification } from 'nestjs-base-service'; import { GetActivityTypesDefaults } from '@shared/dtos/custom-projects/get-activity-types-defaults.dto'; import { z } from 'zod'; import { customProjecsQuerySchema } from '@shared/contracts/custom-projects.contract'; +import { In } from 'typeorm'; export type CustomProjectFetchSpecificacion = z.infer< typeof customProjecsQuerySchema @@ -143,4 +144,15 @@ export class CustomProjectsService extends AppBaseService< return query; } + + async canUserDeleteProjects( + userId: string, + projectIds: string[], + ): Promise { + const customProjects = await this.repo.findBy({ id: In(projectIds) }); + + return customProjects.every( + (customProject) => customProject.user?.id === userId, + ); + } } diff --git a/api/test/integration/custom-projects/custom-projects-delete.spec.ts b/api/test/integration/custom-projects/custom-projects-delete.spec.ts new file mode 100644 index 00000000..e84e0e1e --- /dev/null +++ b/api/test/integration/custom-projects/custom-projects-delete.spec.ts @@ -0,0 +1,87 @@ +import { TestManager } from '../../utils/test-manager'; +import { customProjectContract } from '@shared/contracts/custom-projects.contract'; +import { User } from '@shared/entities/users/user.entity'; +import { HttpStatus } from '@nestjs/common'; + +describe('Delete Custom projects', () => { + let testManager: TestManager; + let jwtToken: string; + let user: User; + + beforeAll(async () => { + testManager = await TestManager.createTestManager(); + }); + + beforeEach(async () => { + ({ jwtToken, user } = await testManager.setUpTestUser()); + await testManager.ingestCountries(); + await testManager.ingestExcel(jwtToken); + }); + + afterEach(async () => { + await testManager.clearDatabase(); + }); + + afterAll(async () => { + await testManager.close(); + }); + + describe('Delete Custom Projects', () => { + test('An anonymous user should be UNAUTHORIZED to delete a custom project', async () => { + // Given + const customProject = await testManager.mocks().createCustomProject({ + user: { id: user.id } as User, + }); + + // When deleting the custom project + const response = await testManager + .request() + .delete(customProjectContract.deleteCustomProjects.path) + .send({ ids: [customProject.id] }); + + // Then + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + expect(response.body.errors).toBeDefined(); + }); + + test.only('An authenticated user should not be able to delete projects that do not belong to them', async () => { + // Given a custom project exists + const customProject = await testManager.mocks().createCustomProject(); + + // When deleting the custom project + const response = await testManager + .request() + .delete(customProjectContract.deleteCustomProjects.path) + .set('Authorization', `Bearer ${jwtToken}`) + .send({ ids: [customProject.id] }); + + // Then + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + }); + + test('An authenticated user should be able to delete projects', async () => { + // Given a custom project exists + const customProject = await testManager.mocks().createCustomProject({ + user: { id: user.id } as User, + }); + + // When deleting the custom project + const response = await testManager + .request() + .delete(customProjectContract.deleteCustomProjects.path) + .set('Authorization', `Bearer ${jwtToken}`) + .send({ ids: [customProject.id] }); + + expect(response.status).toBe(HttpStatus.OK); + + // Then the project should no longer exist + const getProjectResponse = await testManager + .request() + .get( + `${customProjectContract.getCustomProject.path}/${customProject.id}`, + ) + .set('Authorization', `Bearer ${jwtToken}`); + expect(getProjectResponse.status).toBe(HttpStatus.NOT_FOUND); + }); + }); +});