import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsString, MinLength, Validate, ValidatorConstraint, } from 'class-validator'; const EMAIL_PART_COUNT = 2; const FIRST_CHARACTER_INDEX = 0; const MAX_EMAIL_LENGTH = 254; const ASCII_SPACE = 32; const ASCII_DELETE = 127; const MIN_PASSWORD_LENGTH = 12; const MAX_PASSWORD_BYTES = 72; @ValidatorConstraint({ name: 'email' }) class EmailConstraint { validate(value: unknown): boolean { return ( typeof value === 'string' && value.length <= MAX_EMAIL_LENGTH && value.split('@').length === EMAIL_PART_COUNT && !value .split('') .some( (character) => /\s/.test(character) || character.charCodeAt(FIRST_CHARACTER_INDEX) < ASCII_SPACE || character.charCodeAt(FIRST_CHARACTER_INDEX) === ASCII_DELETE, ) && value.split('@').every(Boolean) ); } defaultMessage(): string { return 'must be a valid email address'; } } @ValidatorConstraint({ name: 'passwordBytes' }) class PasswordBytesConstraint { validate(value: unknown): boolean { return ( typeof value === 'string' && Buffer.byteLength(value, 'utf8') <= MAX_PASSWORD_BYTES ); } defaultMessage(): string { return 'must not exceed 72 UTF-8 bytes'; } } export class CredentialsDto { @ApiProperty({ example: 'user@example.com' }) @IsString() @Transform(({ value }: { value: unknown }) => typeof value === 'string' ? value.trim().toLowerCase() : value, ) @Validate(EmailConstraint) declare email: string; @ApiProperty({ minLength: MIN_PASSWORD_LENGTH, maxLength: MAX_PASSWORD_BYTES, example: 'a secure password', }) @IsString() @MinLength(MIN_PASSWORD_LENGTH) @Validate(PasswordBytesConstraint) declare password: string; }