diff --git a/api/src/modules/custom-projects/custom-projects.controller.ts b/api/src/modules/custom-projects/custom-projects.controller.ts index 72f96eba..08d9f5c8 100644 --- a/api/src/modules/custom-projects/custom-projects.controller.ts +++ b/api/src/modules/custom-projects/custom-projects.controller.ts @@ -17,6 +17,7 @@ import { AuthGuard } from '@nestjs/passport'; import { RolesGuard } from '@api/modules/auth/guards/roles.guard'; import { RequiredRoles } from '@api/modules/auth/decorators/roles.decorator'; import { ROLES } from '@shared/entities/users/roles.enum'; +import { CustomProject } from '@shared/entities/custom-project.entity'; @Controller() export class CustomProjectsController { @@ -145,6 +146,35 @@ export class CustomProjectsController { ); } + @UseGuards(AuthGuard('jwt'), RolesGuard) + @RequiredRoles(ROLES.PARTNER, ROLES.ADMIN) + @TsRestHandler(customProjectContract.updateCustomProject) + async updateCustomProject( + @GetUser() user: User, + @Body(new ValidationPipe({ enableDebugMessages: true, transform: true })) + dto: Partial, + ): Promise { + return tsRestHandler( + customProjectContract.updateCustomProject, + async ({ params: { id } }) => { + if ( + !(await this.customProjects.areProjectsCreatedByUser(user.id, [id])) + ) { + return { + status: 401, + body: null, + }; + } + + const updatedEntity = await this.customProjects.update(id, dto); + return { + status: 200, + body: updatedEntity, + }; + }, + ); + } + @UseGuards(AuthGuard('jwt'), RolesGuard) @RequiredRoles(ROLES.PARTNER, ROLES.ADMIN) @TsRestHandler(customProjectContract.deleteCustomProjects) @@ -156,7 +186,10 @@ export class CustomProjectsController { customProjectContract.deleteCustomProjects, async () => { if ( - !(await this.customProjects.canUserDeleteProjects(user.id, body.ids)) + !(await this.customProjects.areProjectsCreatedByUser( + user.id, + body.ids, + )) ) { return { status: 401, diff --git a/api/src/modules/custom-projects/custom-projects.service.ts b/api/src/modules/custom-projects/custom-projects.service.ts index 49908f05..48ca5d09 100644 --- a/api/src/modules/custom-projects/custom-projects.service.ts +++ b/api/src/modules/custom-projects/custom-projects.service.ts @@ -145,7 +145,7 @@ export class CustomProjectsService extends AppBaseService< return query; } - async canUserDeleteProjects( + async areProjectsCreatedByUser( userId: string, projectIds: string[], ): Promise { diff --git a/api/test/integration/custom-projects/custom-projects-update.spec.ts b/api/test/integration/custom-projects/custom-projects-update.spec.ts new file mode 100644 index 00000000..ed5aaf79 --- /dev/null +++ b/api/test/integration/custom-projects/custom-projects-update.spec.ts @@ -0,0 +1,89 @@ +import { TestManager } from '../../utils/test-manager'; +import { customProjectContract } from '@shared/contracts/custom-projects.contract'; +import { User } from '@shared/entities/users/user.entity'; + +describe('Update 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('Update Custom Projects', () => { + test('An authenticated user should be able to update/edit one of their custom projects by id', async () => { + // Given + const customProject = await testManager.mocks().createCustomProject({ + user: { id: user.id } as User, + projectName: 'A', + }); + + // When + const response = await testManager + .request() + .patch( + `${customProjectContract.updateCustomProject.path.replace(':id', customProject.id)}`, + ) + .set('Authorization', `Bearer ${jwtToken}`) + .send({ + projectName: 'B', + }); + + // Then + expect(response.status).toBe(200); + expect(response.body.projectName).toBe('B'); + }); + + test('An authenticated user should not be able to update projects that do not belong to them', async () => { + // Given a custom project exists + const customProject = await testManager.mocks().createCustomProject(); + + // When updating the custom project + const response = await testManager + .request() + .patch( + `${customProjectContract.updateCustomProject.path.replace(':id', customProject.id)}`, + ) + .set('Authorization', `Bearer ${jwtToken}`) + .send({ + projectName: 'B', + }); + + // Then + expect(response.status).toBe(401); + }); + + test('An unauthenticated user should not be able to update a custom project', async () => { + // Given a custom project exists + const customProject = await testManager.mocks().createCustomProject(); + + // When updating the custom project + const response = await testManager + .request() + .patch( + `${customProjectContract.updateCustomProject.path.replace(':id', customProject.id)}`, + ) + .send({ + projectName: 'B', + }); + + // Then + expect(response.status).toBe(401); + }); + }); +}); diff --git a/shared/contracts/custom-projects.contract.ts b/shared/contracts/custom-projects.contract.ts index 60a1c66d..211868c0 100644 --- a/shared/contracts/custom-projects.contract.ts +++ b/shared/contracts/custom-projects.contract.ts @@ -72,6 +72,18 @@ export const customProjectContract = contract.router({ }, body: CreateCustomProjectSchema, }, + updateCustomProject: { + method: "PATCH", + path: "/custom-projects/:id", + pathParams: z.object({ + id: z.coerce.string(), + }), + responses: { + 201: contract.type(), + }, + body: contract.type>(), + summary: "Update an existing custom-project", + }, getCustomProjects: { method: "GET", path: "/custom-projects",