From 83b5eb69b00a4dfb02eff3c14fc6813ca7e5cdb2 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Mon, 18 Nov 2024 17:04:10 +0100 Subject: [PATCH] feat(env-vars): Add warning validation decorator (#8555) Introduced a custom decorator 'WarningIf' to log warnings for specific environment variable conditions. Implemented this for SESSION_STORE_SECRET to ensure users change it from the default value. --- .../assert-or-warn.decorator.spec.ts | 73 +++++++++++++++++++ .../decorators/assert-or-warn.decorator.ts | 31 ++++++++ .../environment/environment-variables.ts | 27 ++++++- 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/environment/decorators/__tests__/assert-or-warn.decorator.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/environment/decorators/assert-or-warn.decorator.ts diff --git a/packages/twenty-server/src/engine/core-modules/environment/decorators/__tests__/assert-or-warn.decorator.spec.ts b/packages/twenty-server/src/engine/core-modules/environment/decorators/__tests__/assert-or-warn.decorator.spec.ts new file mode 100644 index 0000000000..7479e0237a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/environment/decorators/__tests__/assert-or-warn.decorator.spec.ts @@ -0,0 +1,73 @@ +import 'reflect-metadata'; +import { IsString, validateSync } from 'class-validator'; +import { plainToClass } from 'class-transformer'; + +import { AssertOrWarn } from 'src/engine/core-modules/environment/decorators/assert-or-warn.decorator'; + +describe('AssertOrWarn Decorator', () => { + it('should pass validation if the condition is met', () => { + class EnvironmentVariables { + @AssertOrWarn((object, value) => value > 10, { + message: 'Value should be higher than 10', + }) + someProperty!: number; + } + + const validatedConfig = plainToClass(EnvironmentVariables, { + someProperty: 15, + }); + + const warnings = validateSync(validatedConfig, { groups: ['warning'] }); + + expect(warnings.length).toBe(0); + }); + + it('should provide a warning message if the condition is not met', () => { + class EnvironmentVariables { + @AssertOrWarn((object, value) => value > 10, { + message: 'Value should be higher than 10', + }) + someProperty!: number; + } + + const validatedConfig = plainToClass(EnvironmentVariables, { + someProperty: 9, + }); + + const warnings = validateSync(validatedConfig, { groups: ['warning'] }); + + expect(warnings.length).toBe(1); + expect(warnings[0].constraints!.AssertOrWarn).toBe( + 'Value should be higher than 10', + ); + }); + + it('should not impact errors if the condition is not met', () => { + class EnvironmentVariables { + @IsString() + unit: string; + + @AssertOrWarn( + (object, value) => object.unit == 's' && value.toString().length <= 10, + { + message: 'The unit is in seconds but the duration in milliseconds', + }, + ) + duration!: number; + } + + const validatedConfig = plainToClass(EnvironmentVariables, { + duration: 1731944140876000, + unit: 's', + }); + + const warnings = validateSync(validatedConfig, { groups: ['warning'] }); + const errors = validateSync(validatedConfig, { strictGroups: true }); + + expect(errors.length).toBe(0); + expect(warnings.length).toBe(1); + expect(warnings[0].constraints!.AssertOrWarn).toBe( + 'The unit is in seconds but the duration in milliseconds', + ); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/environment/decorators/assert-or-warn.decorator.ts b/packages/twenty-server/src/engine/core-modules/environment/decorators/assert-or-warn.decorator.ts new file mode 100644 index 0000000000..c9726a6d02 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/environment/decorators/assert-or-warn.decorator.ts @@ -0,0 +1,31 @@ +import { + ValidationOptions, + registerDecorator, + ValidationArguments, +} from 'class-validator'; + +export const AssertOrWarn = ( + condition: (object: any, value: any) => boolean, + validationOptions?: ValidationOptions, +) => { + return function (object: any, propertyName: string) { + registerDecorator({ + name: 'AssertOrWarn', + target: object.constructor, + propertyName: propertyName, + options: { + ...validationOptions, + groups: ['warning'], + }, + constraints: [condition], + validator: { + validate(value: any, args: ValidationArguments) { + return condition(args.object, value); + }, + defaultMessage(args: ValidationArguments) { + return `'${args.property}' failed the warning validation.`; + }, + }, + }); + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index 888540b5fc..e80b41c873 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -1,4 +1,4 @@ -import { LogLevel } from '@nestjs/common'; +import { LogLevel, Logger } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { @@ -28,6 +28,7 @@ import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/ca import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator'; import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator'; import { CastToStringArray } from 'src/engine/core-modules/environment/decorators/cast-to-string-array.decorator'; +import { AssertOrWarn } from 'src/engine/core-modules/environment/decorators/assert-or-warn.decorator'; import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator'; import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator'; import { IsStrictlyLowerThan } from 'src/engine/core-modules/environment/decorators/is-strictly-lower-than.decorator'; @@ -449,6 +450,16 @@ export class EnvironmentVariables { @IsString() @IsOptional() + @AssertOrWarn( + (env, value) => + !env.AUTH_SSO_ENABLED || + (env.AUTH_SSO_ENABLED && + value !== 'replace_me_with_a_random_string_session'), + { + message: + 'SESSION_STORE_SECRET should be changed to a secure, random string.', + }, + ) SESSION_STORE_SECRET = 'replace_me_with_a_random_string_session'; @CastToBoolean() @@ -471,7 +482,19 @@ export const validate = ( ): EnvironmentVariables => { const validatedConfig = plainToClass(EnvironmentVariables, config); - const errors = validateSync(validatedConfig); + const errors = validateSync(validatedConfig, { strictGroups: true }); + + const warnings = validateSync(validatedConfig, { groups: ['warning'] }); + + if (warnings.length > 0) { + warnings.forEach((warning) => { + if (warning.constraints && warning.property) { + Object.values(warning.constraints).forEach((message) => { + Logger.warn(message); + }); + } + }); + } assert(!errors.length, errors.toString());