diff --git a/__tests__/defaultConverters.test.ts b/__tests__/defaultConverters.test.ts index d37a590..f326e9c 100644 --- a/__tests__/defaultConverters.test.ts +++ b/__tests__/defaultConverters.test.ts @@ -5,12 +5,14 @@ import _get from 'lodash.get' import { validationMetadatasToSchemas } from '../src' class Post {} + class Comment {} enum PostType { Public, Private, } + enum Role { Anonymous = 'anonymous', User = 'user', @@ -137,6 +139,17 @@ class User { @validator.ArrayMaxSize(10) arrayMaxSize: any[] @validator.ArrayUnique() arrayUnique: any[] + + @validator.IsStrongPassword() + password!: string + @validator.IsStrongPassword({ + minLength: 16, + minSymbols: 1, + minLowercase: 2, + minUppercase: 3, + minNumbers: 4, + }) + customPassword!: string } const metadata = _get( @@ -326,6 +339,16 @@ describe('defaultConverters', () => { arrayMinSize: { type: 'array', items: {}, minItems: 1 }, arrayMaxSize: { type: 'array', items: {}, maxItems: 10 }, arrayUnique: { type: 'array', items: {}, uniqueItems: true }, + password: { + type: 'string', + pattern: + '^(?=.{8})(?=.*(?:[^a-z]*[a-z]){1})(?=.*(?:[^A-Z]*[A-Z]){1})(?=.*(?:[^0-9]*[0-9]){1})(?=.*(?:[^-#!$@£%^&*()_+|~=`{}\\[\\]:";\'<>?,.\\/ ]*[-#!$@£%^&*()_+|~=`{}\\[\\]:";\'<>?,.\\/ ]){1}).*$', + }, + customPassword: { + pattern: + '^(?=.{16})(?=.*(?:[^a-z]*[a-z]){2})(?=.*(?:[^A-Z]*[A-Z]){3})(?=.*(?:[^0-9]*[0-9]){4})(?=.*(?:[^-#!$@£%^&*()_+|~=`{}\\[\\]:";\'<>?,.\\/ ]*[-#!$@£%^&*()_+|~=`{}\\[\\]:";\'<>?,.\\/ ]){1}).*$', + type: 'string', + }, }, required: expect.any(Array), type: 'object', diff --git a/src/defaultConverters.ts b/src/defaultConverters.ts index 393fa9d..ee286d0 100644 --- a/src/defaultConverters.ts +++ b/src/defaultConverters.ts @@ -365,6 +365,47 @@ export const defaultConverters: ISchemaConverters = { type: 'array', uniqueItems: true, }, + [cv.IS_STRONG_PASSWORD]: (meta, options) => { + const { + minLength = 8, + minLowercase = 1, + minUppercase = 1, + minNumbers = 1, + minSymbols = 1, + }: cv.IsStrongPasswordOptions = meta.constraints[0] ?? {} + + const requireChars = (chars: string, amount: number): string => { + const charMatcher = chars === '*' ? '' : `*(?:[^${chars}]*[${chars}])` + + return `(?=.${charMatcher}{${amount}})` + } + + const requirements = ( + [ + ['*', minLength], + ['a-z', minLowercase], + ['A-Z', minUppercase], + ['0-9', minNumbers], + [options.passwordSymbols, minSymbols], + ] as const + ) + .map(([chars, amount]) => { + if (!amount) { + return + } + + return requireChars(chars, amount) + }) + .filter(Boolean) + .join('') + + const pattern = `^${requirements}.*$` + + return { + type: 'string', + pattern, + } + }, } function getPropType(target: object, property: string) { diff --git a/src/options.ts b/src/options.ts index 02d0902..f1b4bc7 100644 --- a/src/options.ts +++ b/src/options.ts @@ -40,6 +40,12 @@ export interface IOptions extends ValidatorOptions { * Defaults to `name`, i.e., class name. */ schemaNameField: string + + /** + * Characters that are considered symbols n passwords. + * Defaults to the symbol character set of `validator.js`. + */ + passwordSymbols: string } export const defaultOptions: IOptions = { @@ -47,4 +53,5 @@ export const defaultOptions: IOptions = { classValidatorMetadataStorage: getMetadataStorage(), refPointerPrefix: '#/definitions/', schemaNameField: 'name', + passwordSymbols: '-#!$@£%^&*()_+|~=`{}\\[\\]:";\'<>?,.\\/ ', }