Password confirmation in TypeScript with `class-validator`
Finally I managed to solve the password matching problem thanks to the suggestion of @ChristopheGeers in the comments of my question:
@piero: It's not supported yet as mentioned. But here's an example decorator (@IsLongerThan): LINK .... it checks if a property is longer than another one. So it's possible to compare one property against another. You can use this example to create a decorator that does what you want.
Here it is the solution I propose:
sign-up.dto.ts
export class SignUpDto {
@IsString()
@MinLength(4)
@MaxLength(20)
username: string;
@IsString()
@MinLength(4)
@MaxLength(20)
@Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, {message: 'password too weak'})
password: string;
@IsString()
@MinLength(4)
@MaxLength(20)
@Match('password')
passwordConfirm: string;
}
match.decorator.ts
import {registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface} from 'class-validator';
export function Match(property: string, validationOptions?: ValidationOptions) {
return (object: any, propertyName: string) => {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [property],
validator: MatchConstraint,
});
};
}
@ValidatorConstraint({name: 'Match'})
export class MatchConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
}
}
I like the approach of the IsEqualTo decorator, but I were also concerned about the typos when using a property that is not in my Dto, so I end up with this:
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
export function IsEqualTo<T>(
property: keyof T,
validationOptions?: ValidationOptions,
) {
return (object: any, propertyName: string) => {
registerDecorator({
name: 'isEqualTo',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
},
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${propertyName} must match ${relatedPropertyName} exactly`;
},
},
});
};
}
and use it like this:
export class CreateUserDto {
@IsEqualTo<CreateUserDto>('password')
readonly password_confirmation: string;
}
The accepted answer is pretty good for me, but we could make a spelling mistake like:
@Match('passwordd')
// ð
So I would like to make it more strict with Generics
@Match(SignUpDto, (s) => s.password)
match.decorator.ts
import { ClassConstructor } from "class-transformer";
export const Match = <T>(
type: ClassConstructor<T>,
property: (o: T) => any,
validationOptions?: ValidationOptions,
) => {
return (object: any, propertyName: string) => {
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [property],
validator: MatchConstraint,
});
};
};
@ValidatorConstraint({ name: "Match" })
export class MatchConstraint implements ValidatorConstraintInterface {
validate(value: any, args: ValidationArguments) {
const [fn] = args.constraints;
return fn(args.object) === value;
}
defaultMessage(args: ValidationArguments) {
const [constraintProperty]: (() => any)[] = args.constraints;
return `${constraintProperty} and ${args.property} does not match`;
}
}
So we can use Match
decorator like this:
export class SignUpDto {
// ...
password: string;
// finally, we have ð
@Match(SignUpDto, (s) => s.password)
passwordConfirm: string;
}
Here's an extended example which inlines the validator and provides a default message for it. This way you don't have to enter a message each time you use the @IsEqualTo
decorator.
import {
registerDecorator,
ValidationArguments,
ValidationOptions
} from 'class-validator';
export function IsEqualTo(property: string, validationOptions?: ValidationOptions) {
return (object: any, propertyName: string) => {
registerDecorator({
name: 'isEqualTo',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
},
defaultMessage(args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
return `${propertyName} must match ${relatedPropertyName} exactly`;
},
},
});
};
}