Files
backend/src/modules/auth/presentation/auth.dto.ts
T
codex-bot e03fad622b
Backend verify / commits (push) Skipped
Backend verify / commits (pull_request) Failing after 32s
Backend verify / security (pull_request) Skipped
Backend verify / verify (pull_request) Skipped
Backend verify / security (push) Successful in 10s
Backend verify / verify (push) Failing after 6m21s
fix: align password policy to 8-72 printable ASCII on register (WealthLedger/backend#60) (#109)
2026-10-16 10:00:47 +03:00

94 lines
2.6 KiB
TypeScript

import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import {
IsNotEmpty,
IsString,
Matches,
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;
// bcrypt reads at most 72 bytes; printable ASCII keeps characters == bytes.
const MIN_PASSWORD_LENGTH = 8;
const MAX_PASSWORD_BYTES = 72;
const PASSWORD_PATTERN = `^[\\x20-\\x7E]{${MIN_PASSWORD_LENGTH},${MAX_PASSWORD_BYTES}}$`;
@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';
}
}
// Login stays lenient: accounts created under an older policy must still sign in.
@ValidatorConstraint({ name: 'passwordMaxBytes' })
class PasswordMaxBytesConstraint {
validate(value: unknown): boolean {
return (
typeof value === 'string' &&
Buffer.byteLength(value, 'utf8') <= MAX_PASSWORD_BYTES
);
}
defaultMessage(): string {
return `must not exceed ${MAX_PASSWORD_BYTES} UTF-8 bytes`;
}
}
class EmailDto {
@ApiProperty({ example: 'user@example.com' })
@IsString()
@Transform(({ value }: { value: unknown }) =>
typeof value === 'string' ? value.trim().toLowerCase() : value,
)
@Validate(EmailConstraint)
declare email: string;
}
export class RegisterDto extends EmailDto {
@ApiProperty({
description: `Raw password: ${MIN_PASSWORD_LENGTH}-${MAX_PASSWORD_BYTES} printable ASCII characters (letters, digits, symbols, space). Not trimmed or normalized.`,
pattern: PASSWORD_PATTERN,
example: 'a secure password',
})
@IsString()
@Matches(new RegExp(PASSWORD_PATTERN), {
message: `must be ${MIN_PASSWORD_LENGTH}-${MAX_PASSWORD_BYTES} printable ASCII characters`,
})
declare password: string;
}
export class LoginDto extends EmailDto {
@ApiProperty({
description: `Raw password, at most ${MAX_PASSWORD_BYTES} UTF-8 bytes. Not trimmed or normalized.`,
example: 'a secure password',
})
@IsString()
@IsNotEmpty()
@Validate(PasswordMaxBytesConstraint)
declare password: string;
}