From b227d8896ddd49d0d5b0fcd1058926539ffa0062 Mon Sep 17 00:00:00 2001 From: JosephFak <160545644+JosephFak@users.noreply.github.com> Date: Wed, 24 Apr 2024 02:28:33 -0400 Subject: [PATCH] feat(api): Added validation for reason field (#190) --- .../controller/approval.controller.ts | 3 +- .../common/alphanumeric-reason-pipe.spec.ts | 29 +++++++++ .../src/common/alphanumeric-reason-pipe.ts | 12 ++++ .../controller/environment.controller.ts | 7 +- .../project/controller/project.controller.ts | 7 +- .../secret/controller/secret.controller.ts | 11 ++-- .../controller/variable.controller.ts | 14 ++-- .../controller/workspace.controller.ts | 5 +- package.json | 2 + pnpm-lock.yaml | 64 +++++++++++++++++++ 10 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 apps/api/src/common/alphanumeric-reason-pipe.spec.ts create mode 100644 apps/api/src/common/alphanumeric-reason-pipe.ts diff --git a/apps/api/src/approval/controller/approval.controller.ts b/apps/api/src/approval/controller/approval.controller.ts index ad5d0b7e..1e10a6d5 100644 --- a/apps/api/src/approval/controller/approval.controller.ts +++ b/apps/api/src/approval/controller/approval.controller.ts @@ -10,6 +10,7 @@ import { } from '@prisma/client' import { CurrentUser } from '../../decorators/user.decorator' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' +import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' @Controller('approval') export class ApprovalController { @@ -20,7 +21,7 @@ export class ApprovalController { async updateApproval( @CurrentUser() user: User, @Param('approvalId') approvalId: Approval['id'], - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return this.approvalService.updateApproval(user, reason, approvalId) } diff --git a/apps/api/src/common/alphanumeric-reason-pipe.spec.ts b/apps/api/src/common/alphanumeric-reason-pipe.spec.ts new file mode 100644 index 00000000..75542006 --- /dev/null +++ b/apps/api/src/common/alphanumeric-reason-pipe.spec.ts @@ -0,0 +1,29 @@ +import { AlphanumericReasonValidationPipe } from './alphanumeric-reason-pipe'; +import { BadRequestException } from '@nestjs/common'; + +describe('AlphanumericReasonValidationPipe', () => { + let pipe: AlphanumericReasonValidationPipe; + + beforeEach(() => { + pipe = new AlphanumericReasonValidationPipe(); + }); + + it('should allow alphanumeric string', () => { + const validInput = 'Test123'; + expect(pipe.transform(validInput)).toBe(validInput); + }); + + it('should not allow strings with only spaces', () => { + expect(() => pipe.transform(' ')).toThrow(BadRequestException); + }); + + it('should throw BadRequestException for non-alphanumeric string', () => { + const invalidInput = 'Test123$%^'; + try { + pipe.transform(invalidInput); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + expect(e.message).toBe('Reason must contain only alphanumeric characters and no leading or trailing spaces.'); + } + }); +}); diff --git a/apps/api/src/common/alphanumeric-reason-pipe.ts b/apps/api/src/common/alphanumeric-reason-pipe.ts new file mode 100644 index 00000000..26f0dd90 --- /dev/null +++ b/apps/api/src/common/alphanumeric-reason-pipe.ts @@ -0,0 +1,12 @@ +import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; + +@Injectable() +export class AlphanumericReasonValidationPipe implements PipeTransform { + transform(value: string) { + if (/^[a-zA-Z0-9]+(?: [a-zA-Z0-9]+)*$/.test(value)) { + return value; + } else { + throw new BadRequestException('Reason must contain only alphanumeric characters and no leading or trailing spaces.'); + } +} +} diff --git a/apps/api/src/environment/controller/environment.controller.ts b/apps/api/src/environment/controller/environment.controller.ts index 054acea9..ad2f6b7d 100644 --- a/apps/api/src/environment/controller/environment.controller.ts +++ b/apps/api/src/environment/controller/environment.controller.ts @@ -15,6 +15,7 @@ import { Authority, User } from '@prisma/client' import { UpdateEnvironment } from '../dto/update.environment/update.environment' import { ApiTags } from '@nestjs/swagger' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' +import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' @ApiTags('Environment Controller') @Controller('environment') @@ -27,7 +28,7 @@ export class EnvironmentController { @CurrentUser() user: User, @Body() dto: CreateEnvironment, @Param('projectId') projectId: string, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.environmentService.createEnvironment( user, @@ -43,7 +44,7 @@ export class EnvironmentController { @CurrentUser() user: User, @Body() dto: UpdateEnvironment, @Param('environmentId') environmentId: string, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.environmentService.updateEnvironment( user, @@ -89,7 +90,7 @@ export class EnvironmentController { async deleteEnvironment( @CurrentUser() user: User, @Param('environmentId') environmentId: string, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.environmentService.deleteEnvironment( user, diff --git a/apps/api/src/project/controller/project.controller.ts b/apps/api/src/project/controller/project.controller.ts index e6a51010..dce762ef 100644 --- a/apps/api/src/project/controller/project.controller.ts +++ b/apps/api/src/project/controller/project.controller.ts @@ -15,6 +15,7 @@ import { CreateProject } from '../dto/create.project/create.project' import { UpdateProject } from '../dto/update.project/update.project' import { ApiTags } from '@nestjs/swagger' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' +import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' @ApiTags('Project Controller') @Controller('project') @@ -27,7 +28,7 @@ export class ProjectController { @CurrentUser() user: User, @Param('workspaceId') workspaceId: Workspace['id'], @Body() dto: CreateProject, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.service.createProject(user, workspaceId, dto, reason) } @@ -38,7 +39,7 @@ export class ProjectController { @CurrentUser() user: User, @Param('projectId') projectId: Project['id'], @Body() dto: UpdateProject, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.service.updateProject(user, projectId, dto, reason) } @@ -48,7 +49,7 @@ export class ProjectController { async deleteProject( @CurrentUser() user: User, @Param('projectId') projectId: Project['id'], - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.service.deleteProject(user, projectId, reason) } diff --git a/apps/api/src/secret/controller/secret.controller.ts b/apps/api/src/secret/controller/secret.controller.ts index 2270ba37..0f71e330 100644 --- a/apps/api/src/secret/controller/secret.controller.ts +++ b/apps/api/src/secret/controller/secret.controller.ts @@ -15,6 +15,7 @@ import { CreateSecret } from '../dto/create.secret/create.secret' import { UpdateSecret } from '../dto/update.secret/update.secret' import { ApiTags } from '@nestjs/swagger' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' +import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' @ApiTags('Secret Controller') @Controller('secret') @@ -27,7 +28,7 @@ export class SecretController { @CurrentUser() user: User, @Param('projectId') projectId: string, @Body() dto: CreateSecret, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.secretService.createSecret(user, dto, projectId, reason) } @@ -38,7 +39,7 @@ export class SecretController { @CurrentUser() user: User, @Param('secretId') secretId: string, @Body() dto: UpdateSecret, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.secretService.updateSecret(user, secretId, dto, reason) } @@ -52,7 +53,7 @@ export class SecretController { @CurrentUser() user: User, @Param('secretId') secretId: string, @Param('environmentId') environmentId: string, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.secretService.updateSecretEnvironment( user, @@ -68,7 +69,7 @@ export class SecretController { @CurrentUser() user: User, @Param('secretId') secretId: string, @Param('rollbackVersion') rollbackVersion: number, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.secretService.rollbackSecret( user, @@ -83,7 +84,7 @@ export class SecretController { async deleteSecret( @CurrentUser() user: User, @Param('secretId') secretId: string, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.secretService.deleteSecret(user, secretId, reason) } diff --git a/apps/api/src/variable/controller/variable.controller.ts b/apps/api/src/variable/controller/variable.controller.ts index 24cdea2a..e2d16927 100644 --- a/apps/api/src/variable/controller/variable.controller.ts +++ b/apps/api/src/variable/controller/variable.controller.ts @@ -12,9 +12,9 @@ import { ApiTags } from '@nestjs/swagger' import { VariableService } from '../service/variable.service' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' import { Authority, User } from '@prisma/client' +import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' import { CurrentUser } from '../../decorators/user.decorator' import { CreateVariable } from '../dto/create.variable/create.variable' -import { UpdateVariable } from '../dto/update.variable/update.variable' @ApiTags('Variable Controller') @Controller('variable') @@ -27,7 +27,7 @@ export class VariableController { @CurrentUser() user: User, @Param('projectId') projectId: string, @Body() dto: CreateVariable, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe ) reason: string ) { return await this.variableService.createVariable( user, @@ -42,8 +42,8 @@ export class VariableController { async updateVariable( @CurrentUser() user: User, @Param('variableId') variableId: string, - @Body() dto: UpdateVariable, - @Query('reason') reason: string + @Body() dto: CreateVariable, + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.variableService.updateVariable( user, @@ -62,7 +62,7 @@ export class VariableController { @CurrentUser() user: User, @Param('variableId') variableId: string, @Param('environmentId') environmentId: string, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.variableService.updateVariableEnvironment( user, @@ -78,7 +78,7 @@ export class VariableController { @CurrentUser() user: User, @Param('variableId') variableId: string, @Param('rollbackVersion') rollbackVersion: number, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.variableService.rollbackVariable( user, @@ -93,7 +93,7 @@ export class VariableController { async deleteVariable( @CurrentUser() user: User, @Param('variableId') variableId: string, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return await this.variableService.deleteVariable(user, variableId, reason) } diff --git a/apps/api/src/workspace/controller/workspace.controller.ts b/apps/api/src/workspace/controller/workspace.controller.ts index e008cac8..34309792 100644 --- a/apps/api/src/workspace/controller/workspace.controller.ts +++ b/apps/api/src/workspace/controller/workspace.controller.ts @@ -11,7 +11,8 @@ import { import { WorkspaceService } from '../service/workspace.service' import { CurrentUser } from '../../decorators/user.decorator' import { Authority, User, Workspace, WorkspaceRole } from '@prisma/client' -import { +import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' +import{ CreateWorkspace, WorkspaceMemberDTO } from '../dto/create.workspace/create.workspace' @@ -34,7 +35,7 @@ export class WorkspaceController { @CurrentUser() user: User, @Param('workspaceId') workspaceId: Workspace['id'], @Body() dto: UpdateWorkspace, - @Query('reason') reason: string + @Query('reason', AlphanumericReasonValidationPipe) reason: string ) { return this.workspaceService.updateWorkspace(user, workspaceId, dto, reason) } diff --git a/package.json b/package.json index 6088f71f..ebe85915 100644 --- a/package.json +++ b/package.json @@ -127,11 +127,13 @@ "husky": "^9.0.11", "prettier": "^3.0.0", "prettier-plugin-tailwindcss": "^0.5.11", + "prisma": "5.12.1", "tsconfig": "workspace:*", "turbo": "^1.12.4" }, "dependencies": { "@million/lint": "^0.0.73", + "@prisma/client": "5.12.1", "@sentry/node": "^7.102.0", "@sentry/profiling-node": "^7.102.0", "million": "^3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18cd8285..f2bc9453 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@million/lint': specifier: ^0.0.73 version: 0.0.73(@types/react-dom@18.3.0)(@types/react@18.3.1) + '@prisma/client': + specifier: 5.12.1 + version: 5.12.1(prisma@5.12.1) '@sentry/node': specifier: ^7.102.0 version: 7.102.0 @@ -39,6 +42,9 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.5.11 version: 0.5.11(prettier@3.0.0) + prisma: + specifier: 5.12.1 + version: 5.12.1 tsconfig: specifier: workspace:* version: link:packages/tsconfig @@ -1820,21 +1826,45 @@ packages: prisma: optional: true + '@prisma/client@5.12.1': + resolution: {integrity: sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + '@prisma/debug@5.10.1': resolution: {integrity: sha512-Ipo9y/lCMzedXMtEBe4YCdvVVivSy6MdG7aYTM15t86g4CRzwdlEsw8Czxnw20w9Qgzdx0MX2iLsCCIG4JoHbA==} + '@prisma/debug@5.12.1': + resolution: {integrity: sha512-kd/wNsR0klrv79o1ITsbWxYyh4QWuBidvxsXSParPsYSu0ircUmNk3q4ojsgNc3/81b0ozg76iastOG43tbf8A==} + '@prisma/engines-version@5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9': resolution: {integrity: sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ==} + '@prisma/engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab': + resolution: {integrity: sha512-6yvO8s80Tym61aB4QNtYZfWVmE3pwqe807jEtzm8C5VDe7nw8O1FGX3TXUaXmWV0fQTIAfRbeL2Gwrndabp/0g==} + '@prisma/engines@5.10.1': resolution: {integrity: sha512-75oJa900Pw+GAXjPJmKZqsD7bgSgQbpeGLxCwchrbgPIM70y3h0FbjIsiSAjuhwIGUCCNWzctUNv67rvSmoQAQ==} + '@prisma/engines@5.12.1': + resolution: {integrity: sha512-HQDdglLw2bZR/TXD2Y+YfDMvi5Q8H+acbswqOsWyq9pPjBLYJ6gzM+ptlTU/AV6tl0XSZLU1/7F4qaWa8bqpJA==} + '@prisma/fetch-engine@5.10.1': resolution: {integrity: sha512-xg3I3RM/qENykZNGBna+14gBkkZL2TVkyX3OX2GWI8MV23Meq5jYdqvlgBrZne25ZxiulctSEA2D6Y5fq1eLog==} + '@prisma/fetch-engine@5.12.1': + resolution: {integrity: sha512-qSs3KcX1HKcea1A+hlJVK/ljj0PNIUHDxAayGMvgJBqmaN32P9tCidlKz1EGv6WoRFICYnk3Dd/YFLBwnFIozA==} + '@prisma/get-platform@5.10.1': resolution: {integrity: sha512-0rE8lSE3y+Ua3LaOcXlWADz21+kGkf9NWmGNuh8n9I6uaCq90LQxM002l4NSYg6ELtiJXyDgJ4nRhM0x0OXjDQ==} + '@prisma/get-platform@5.12.1': + resolution: {integrity: sha512-pgIR+pSvhYHiUcqXVEZS31NrFOTENC9yFUdEAcx7cdQBoZPmHVjtjN4Ss6NzVDMYPrKJJ51U14EhEoeuBlMioQ==} + '@radix-ui/number@1.0.1': resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} @@ -5834,6 +5864,11 @@ packages: engines: {node: '>=16.13'} hasBin: true + prisma@5.12.1: + resolution: {integrity: sha512-SkMnb6wyIxTv9ACqiHBI2u9gD6y98qXRoCoLEnZsF6yee5Qg828G+ARrESN+lQHdw4maSZFFSBPPDpvSiVTo0Q==} + engines: {node: '>=16.13'} + hasBin: true + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -8644,10 +8679,18 @@ snapshots: optionalDependencies: prisma: 5.10.1 + '@prisma/client@5.12.1(prisma@5.12.1)': + optionalDependencies: + prisma: 5.12.1 + '@prisma/debug@5.10.1': {} + '@prisma/debug@5.12.1': {} + '@prisma/engines-version@5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9': {} + '@prisma/engines-version@5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab': {} + '@prisma/engines@5.10.1': dependencies: '@prisma/debug': 5.10.1 @@ -8655,16 +8698,33 @@ snapshots: '@prisma/fetch-engine': 5.10.1 '@prisma/get-platform': 5.10.1 + '@prisma/engines@5.12.1': + dependencies: + '@prisma/debug': 5.12.1 + '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab + '@prisma/fetch-engine': 5.12.1 + '@prisma/get-platform': 5.12.1 + '@prisma/fetch-engine@5.10.1': dependencies: '@prisma/debug': 5.10.1 '@prisma/engines-version': 5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9 '@prisma/get-platform': 5.10.1 + '@prisma/fetch-engine@5.12.1': + dependencies: + '@prisma/debug': 5.12.1 + '@prisma/engines-version': 5.12.0-21.473ed3124229e22d881cb7addf559799debae1ab + '@prisma/get-platform': 5.12.1 + '@prisma/get-platform@5.10.1': dependencies: '@prisma/debug': 5.10.1 + '@prisma/get-platform@5.12.1': + dependencies: + '@prisma/debug': 5.12.1 + '@radix-ui/number@1.0.1': dependencies: '@babel/runtime': 7.24.4 @@ -13582,6 +13642,10 @@ snapshots: dependencies: '@prisma/engines': 5.10.1 + prisma@5.12.1: + dependencies: + '@prisma/engines': 5.12.1 + process-nextick-args@2.0.1: {} process-warning@2.3.2: {}