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
94 lines
2.6 KiB
TypeScript
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;
|
|
}
|