From d926e8fe52dc861e3eeba4190d78f85a908f81e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Zi=C4=99cina?= Date: Mon, 22 Apr 2024 16:04:52 +0200 Subject: [PATCH] fix: add backend url validation (#4009) * fix: add backend url validation * test: update links to be valid * fix: add url validation for building selection criteria * test: update buildingSelectionCriteria to be valid url --- api/src/decorators/hasHttps.decorator.ts | 31 +++++++++++++++++++ .../application-method.dto.ts | 11 +++++++ api/src/dtos/listings/listing-event.dto.ts | 9 ++++++ api/src/dtos/listings/listing.dto.ts | 5 +++ .../multiselect-link.dto.ts | 6 +++- api/test/integration/listing.e2e-spec.ts | 2 +- .../multiselect-question.e2e-spec.ts | 24 +++++++------- .../integration/permission-tests/helpers.ts | 18 +++++------ .../unit/services/listing.service.spec.ts | 2 +- 9 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 api/src/decorators/hasHttps.decorator.ts diff --git a/api/src/decorators/hasHttps.decorator.ts b/api/src/decorators/hasHttps.decorator.ts new file mode 100644 index 0000000000..4d293c1a35 --- /dev/null +++ b/api/src/decorators/hasHttps.decorator.ts @@ -0,0 +1,31 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +export function hasHttps(validationOptions?: ValidationOptions) { + return function (object: unknown, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [], + validator: hasHttpsConstraint, + }); + }; +} + +@ValidatorConstraint({ name: 'hasHttps' }) +export class hasHttpsConstraint implements ValidatorConstraintInterface { + validate(url: string) { + const httpsRegex = /^https?:\/\//i; + return httpsRegex.test(url); + } + + defaultMessage(args: ValidationArguments) { + return `${args.property} must have https://`; + } +} diff --git a/api/src/dtos/application-methods/application-method.dto.ts b/api/src/dtos/application-methods/application-method.dto.ts index 4726e6d239..1b83dc16fc 100644 --- a/api/src/dtos/application-methods/application-method.dto.ts +++ b/api/src/dtos/application-methods/application-method.dto.ts @@ -6,12 +6,15 @@ import { IsString, MaxLength, ValidateNested, + ValidateIf, + IsUrl, } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { AbstractDTO } from '../shared/abstract.dto'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApplicationMethodsTypeEnum } from '@prisma/client'; import { PaperApplication } from '../paper-applications/paper-application.dto'; +import { hasHttps } from '../../decorators/hasHttps.decorator'; export class ApplicationMethod extends AbstractDTO { @Expose() @@ -36,6 +39,14 @@ export class ApplicationMethod extends AbstractDTO { @IsString({ groups: [ValidationsGroupsEnum.default] }) @MaxLength(4096, { groups: [ValidationsGroupsEnum.default] }) @ApiPropertyOptional() + @ValidateIf((o) => o.type === ApplicationMethodsTypeEnum.ExternalLink, { + groups: [ValidationsGroupsEnum.default], + }) + @hasHttps({ groups: [ValidationsGroupsEnum.default] }) + @IsUrl( + { require_protocol: true }, + { groups: [ValidationsGroupsEnum.default] }, + ) externalReference?: string; @Expose() diff --git a/api/src/dtos/listings/listing-event.dto.ts b/api/src/dtos/listings/listing-event.dto.ts index 494b54c76f..174e1be967 100644 --- a/api/src/dtos/listings/listing-event.dto.ts +++ b/api/src/dtos/listings/listing-event.dto.ts @@ -5,6 +5,8 @@ import { IsDefined, IsString, ValidateNested, + ValidateIf, + IsUrl, } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ListingEventsTypeEnum } from '@prisma/client'; @@ -43,6 +45,13 @@ export class ListingEvent extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiPropertyOptional() + @ValidateIf((o) => o.url && o.url.length > 0, { + groups: [ValidationsGroupsEnum.default], + }) + @IsUrl( + { require_protocol: true }, + { groups: [ValidationsGroupsEnum.default] }, + ) url?: string; @Expose() diff --git a/api/src/dtos/listings/listing.dto.ts b/api/src/dtos/listings/listing.dto.ts index 567e283bdc..9c593bd32d 100644 --- a/api/src/dtos/listings/listing.dto.ts +++ b/api/src/dtos/listings/listing.dto.ts @@ -7,6 +7,7 @@ import { IsEnum, IsNumber, IsString, + IsUrl, MaxLength, ValidateNested, } from 'class-validator'; @@ -192,6 +193,10 @@ class Listing extends AbstractDTO { @Expose() @IsString({ groups: [ValidationsGroupsEnum.default] }) @ApiPropertyOptional() + @IsUrl( + { require_protocol: true }, + { groups: [ValidationsGroupsEnum.default] }, + ) buildingSelectionCriteria?: string; @Expose() diff --git a/api/src/dtos/multiselect-questions/multiselect-link.dto.ts b/api/src/dtos/multiselect-questions/multiselect-link.dto.ts index 9b9da8446a..58cc7e0826 100644 --- a/api/src/dtos/multiselect-questions/multiselect-link.dto.ts +++ b/api/src/dtos/multiselect-questions/multiselect-link.dto.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { IsString, IsDefined } from 'class-validator'; +import { IsString, IsDefined, IsUrl } from 'class-validator'; import { ValidationsGroupsEnum } from '../../enums/shared/validation-groups-enum'; import { ApiProperty } from '@nestjs/swagger'; @@ -14,5 +14,9 @@ export class MultiselectLink { @IsString({ groups: [ValidationsGroupsEnum.default] }) @IsDefined({ groups: [ValidationsGroupsEnum.default] }) @ApiProperty() + @IsUrl( + { require_protocol: true }, + { groups: [ValidationsGroupsEnum.default] }, + ) url: string; } diff --git a/api/test/integration/listing.e2e-spec.ts b/api/test/integration/listing.e2e-spec.ts index f7631f8132..6ea3ec52e1 100644 --- a/api/test/integration/listing.e2e-spec.ts +++ b/api/test/integration/listing.e2e-spec.ts @@ -282,7 +282,7 @@ describe('Listing Controller Tests', () => { applicationDropOffAddressOfficeHours: 'drop off office hours string', applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, - buildingSelectionCriteria: 'selection criteria', + buildingSelectionCriteria: 'https://selection-criteria.com', costsNotIncluded: 'all costs included', creditHistory: 'credit history', criminalBackground: 'criminal background', diff --git a/api/test/integration/multiselect-question.e2e-spec.ts b/api/test/integration/multiselect-question.e2e-spec.ts index 6975456a7f..e3dde1d448 100644 --- a/api/test/integration/multiselect-question.e2e-spec.ts +++ b/api/test/integration/multiselect-question.e2e-spec.ts @@ -146,11 +146,11 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 1', - url: 'title 1', + url: 'https://title-1.com', }, { title: 'title 2', - url: 'title 2', + url: 'https://title-2.com', }, ], jurisdictions: [{ id: jurisdictionId }], @@ -162,7 +162,7 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 3', - url: 'title 3', + url: 'https://title-3.com', }, ], collectAddress: true, @@ -175,7 +175,7 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 4', - url: 'title 4', + url: 'https://title-4.com', }, ], collectAddress: true, @@ -204,11 +204,11 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 1', - url: 'title 1', + url: 'https://title-1.com', }, { title: 'title 2', - url: 'title 2', + url: 'https://title-2.com', }, ], jurisdictions: [{ id: jurisdictionId }], @@ -220,7 +220,7 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 3', - url: 'title 3', + url: 'https://title-3.com', }, ], collectAddress: true, @@ -233,7 +233,7 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 4', - url: 'title 4', + url: 'https://title-4.com', }, ], collectAddress: true, @@ -266,11 +266,11 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 1', - url: 'title 1', + url: 'https://title-1.com', }, { title: 'title 2', - url: 'title 2', + url: 'https://title-2.com', }, ], jurisdictions: [{ id: jurisdictionId }], @@ -282,7 +282,7 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 3', - url: 'title 3', + url: 'https://title-3.com', }, ], collectAddress: true, @@ -295,7 +295,7 @@ describe('MultiselectQuestion Controller Tests', () => { links: [ { title: 'title 4', - url: 'title 4', + url: 'https://title-4.com', }, ], collectAddress: true, diff --git a/api/test/integration/permission-tests/helpers.ts b/api/test/integration/permission-tests/helpers.ts index 40093d6d59..39cbf63ec3 100644 --- a/api/test/integration/permission-tests/helpers.ts +++ b/api/test/integration/permission-tests/helpers.ts @@ -165,11 +165,11 @@ export const buildMultiselectQuestionCreateMock = ( links: [ { title: 'title 1', - url: 'title 1', + url: 'https://title-1.com', }, { title: 'title 2', - url: 'title 2', + url: 'https://title-2.com', }, ], jurisdictions: [{ id: jurisId }], @@ -181,7 +181,7 @@ export const buildMultiselectQuestionCreateMock = ( links: [ { title: 'title 3', - url: 'title 3', + url: 'https://title-3.com', }, ], collectAddress: true, @@ -194,7 +194,7 @@ export const buildMultiselectQuestionCreateMock = ( links: [ { title: 'title 4', - url: 'title 4', + url: 'https://title-4.com', }, ], collectAddress: true, @@ -219,11 +219,11 @@ export const buildMultiselectQuestionUpdateMock = ( links: [ { title: 'title 1', - url: 'title 1', + url: 'https://title-1.com', }, { title: 'title 2', - url: 'title 2', + url: 'https://title-2.com', }, ], jurisdictions: [{ id: jurisId }], @@ -235,7 +235,7 @@ export const buildMultiselectQuestionUpdateMock = ( links: [ { title: 'title 3', - url: 'title 3', + url: 'https://title-3.com', }, ], collectAddress: true, @@ -248,7 +248,7 @@ export const buildMultiselectQuestionUpdateMock = ( links: [ { title: 'title 4', - url: 'title 4', + url: 'https://title-4.com', }, ], collectAddress: true, @@ -662,7 +662,7 @@ export const constructFullListingData = async ( applicationDropOffAddressOfficeHours: 'drop off office hours string', applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, - buildingSelectionCriteria: 'selection criteria', + buildingSelectionCriteria: 'https://selection-criteria.com', costsNotIncluded: 'all costs included', creditHistory: 'credit history', criminalBackground: 'criminal background', diff --git a/api/test/unit/services/listing.service.spec.ts b/api/test/unit/services/listing.service.spec.ts index 76476db819..9e05da05d7 100644 --- a/api/test/unit/services/listing.service.spec.ts +++ b/api/test/unit/services/listing.service.spec.ts @@ -365,7 +365,7 @@ describe('Testing listing service', () => { applicationDropOffAddressOfficeHours: 'drop off office hours string', applicationDropOffAddressType: ApplicationAddressTypeEnum.leasingAgent, applicationMailingAddressType: ApplicationAddressTypeEnum.leasingAgent, - buildingSelectionCriteria: 'selection criteria', + buildingSelectionCriteria: 'https://selection-criteria.com', costsNotIncluded: 'all costs included', creditHistory: 'credit history', criminalBackground: 'criminal background',