diff --git a/src/modules/core/constraints/match.constraint.ts b/src/modules/core/constraints/match.constraint.ts index bc47545..3f98927 100644 --- a/src/modules/core/constraints/match.constraint.ts +++ b/src/modules/core/constraints/match.constraint.ts @@ -21,7 +21,7 @@ export class MatchConstraint implements ValidatorConstraintInterface { } } -export function isMatch( +export function IsMatch( relatedProperty: string, reverse = false, validationOptions?: ValidationOptions, diff --git a/src/modules/user/dtos/account.dto.ts b/src/modules/user/dtos/account.dto.ts new file mode 100644 index 0000000..f43c730 --- /dev/null +++ b/src/modules/user/dtos/account.dto.ts @@ -0,0 +1,40 @@ +import { PickType } from '@nestjs/swagger'; + +import { Length } from 'class-validator'; + +import { IsPassword } from '@/modules/core/constraints/password.constraint'; +import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; +import { UserCommonDto } from '@/modules/user/dtos/user.common.dto'; + +/** + * 更新用户信息 + */ +@DtoValidation({ whitelist: false, groups: [UserValidateGroup.ACCOUNT_UPDATE] }) +export class UpdateAccountDto extends PickType(UserCommonDto, ['username', 'nickname']) { + /** + * 待更新的用户ID + */ + @IsUUID(undefined, { message: '用户ID格式不正确', groups: [UserValidateGroup.USER_UPDATE] }) + @IsDefined({ groups: ['update'], message: '用户ID必须指定' }) + id: string; +} + +/** + * 更改用户密码 + */ +@DtoValidation({ groups: [UserValidateGroup.CHANGE_PASSWORD] }) +export class UpdatePasswordDto extends PickType(UserCommonDto, ['password', 'plainPassword']) { + /** + * 待更新的用户ID + */ + @IsUUID(undefined, { message: '用户ID格式不正确', groups: [UserValidateGroup.USER_UPDATE] }) + @IsDefined({ groups: ['update'], message: '用户ID必须指定' }) + id: string; + + /** + * 旧密码:用户在更改密码时需要输入的原密码 + */ + @IsPassword(5, { message: '密码必须由小写字母,大写字母,数字以及特殊字符组成', always: true }) + @Length(8, 50, { message: '密码长度不得少于$constraint1', always: true }) + oldPassword: string; +} diff --git a/src/modules/user/dtos/auth.dto.ts b/src/modules/user/dtos/auth.dto.ts new file mode 100644 index 0000000..40ed663 --- /dev/null +++ b/src/modules/user/dtos/auth.dto.ts @@ -0,0 +1,21 @@ +import { PickType } from '@nestjs/swagger'; + +import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; +import { UserValidateGroup } from '@/modules/user/constants'; +import { UserCommonDto } from '@/modules/user/dtos/user.common.dto'; + +/** + * 用户正常方式登录 + */ +export class CredentialDto extends PickType(UserCommonDto, ['credential', 'password']) {} + +/** + * 普通方式注册用户 + */ +@DtoValidation({ groups: [UserValidateGroup.USER_REGISTER] }) +export class RegisterDto extends PickType(UserCommonDto, [ + 'username', + 'nickname', + 'password', + 'plainPassword', +] as const) {} diff --git a/src/modules/user/dtos/user.common.dto.ts b/src/modules/user/dtos/user.common.dto.ts new file mode 100644 index 0000000..51c94a9 --- /dev/null +++ b/src/modules/user/dtos/user.common.dto.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@nestjs/common'; +import { IsEmail, IsNotEmpty, IsOptional, Length } from 'class-validator'; + +import { IsMatch } from '@/modules/core/constraints/match.constraint'; +import { IsPassword } from '@/modules/core/constraints/password.constraint'; +import { IsMatchPhone } from '@/modules/core/constraints/phone.number.constraint'; +import { IsUnique, IsUniqueExist } from '@/modules/database/constraints'; +import { UserValidateGroup } from '@/modules/user/constants'; +import { UserEntity } from '@/modules/user/entities/UserEntity'; + +/** + * 用户模块DTO的通用基础字段 + */ +@Injectable() +export class UserCommonDto { + /** + * 登录凭证:可以是用户名,手机号,邮箱地址 + */ + @Length(4, 30, { message: '登录凭证长度必须为$constraint1到$constraint2', always: true }) + @IsNotEmpty({ message: '登录凭证不得为空', always: true }) + credential: string; + + /** + * 用户名 + */ + @IsUnique( + { entity: UserEntity }, + { + groups: [UserValidateGroup.USER_CREATE, UserValidateGroup.USER_REGISTER], + message: '该用户名已被注册', + }, + ) + @IsUniqueExist( + { entity: UserEntity, ignore: 'id' }, + { groups: [UserValidateGroup.USER_UPDATE], message: '该用户名已被注册' }, + ) + @IsUniqueExist( + { entity: UserEntity, ignore: 'id', ignoreKey: 'userId' }, + { groups: [UserValidateGroup.ACCOUNT_UPDATE], message: '该用户名已被注册' }, + ) + @Length(4, 50, { always: true, message: '用户名长度必须为$constraint1到$constraint2' }) + @IsOptional({ groups: [UserValidateGroup.USER_UPDATE, UserValidateGroup.ACCOUNT_UPDATE] }) + username: string; + + /** + * 昵称:不设置则为用户名 + */ + @Length(3, 20, { message: '昵称必须为$constraint1到$constraint2', always: true }) + @IsOptional({ always: true }) + nickname: string; + + /** + * 手机号:必须是区域开头的,比如+86.15005255555 + */ + @IsUnique( + { entity: UserEntity }, + { + message: '手机号已被注册', + groups: [UserValidateGroup.USER_CREATE, UserValidateGroup.USER_REGISTER], + }, + ) + @IsMatchPhone( + undefined, + { strictMode: trus }, + { message: '手机格式错误,示例: +86.15005255555', always: true }, + ) + @IsOptional({ + groups: [ + UserValidateGroup.USER_UPDATE, + UserValidateGroup.USER_CREATE, + UserValidateGroup.USER_REGISTER, + ], + }) + phone: string; + + /** + * 邮箱地址:必须符合邮箱地址规则 + */ + @IsUnique( + { entity: UserEntity }, + { + message: '邮箱已被注册', + groups: [UserValidateGroup.USER_CREATE, UserValidateGroup.USER_REGISTER], + }, + ) + @IsEmail(undefined, { message: '邮箱地址格式错误', always: true }) + @IsOptional({ + groups: [ + UserValidateGroup.USER_UPDATE, + UserValidateGroup.USER_CREATE, + UserValidateGroup.USER_REGISTER, + ], + }) + email: string; + + /** + * 用户密码:密码必须由小写字母,大写字母,数字以及特殊字符组成 + */ + @IsPassword(5, { message: '密码必须由小写字母,大写字母,数字以及特殊字符组成', always: true }) + @Length(8, 50, { message: '密码长度不得少于$constraint1', always: true }) + @IsMatch('oldPassword', true, { + message: '新密码与旧密码不得相同', + groups: [UserValidateGroup.CHANGE_PASSWORD], + }) + @IsOptional({ groups: [UserValidateGroup.USER_UPDATE] }) + password: string; + + /** + * 确认密码:必须与用户密码输入相同的字符串 + */ + @IsMatch('password', false, { message: '两次输入密码不同', always: true }) + @IsNotEmpty({ message: '请再次输入密码以确认', always: true }) + plainPassword: string; +} diff --git a/src/modules/user/dtos/user.dto.ts b/src/modules/user/dtos/user.dto.ts new file mode 100644 index 0000000..b23a3d0 --- /dev/null +++ b/src/modules/user/dtos/user.dto.ts @@ -0,0 +1,44 @@ +import { PartialType, PickType } from '@nestjs/swagger'; + +import { IsDefined, IsEnum, IsUUID } from 'class-validator'; + +import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator'; +import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-trashed.dto'; +import { UserOrderType } from '@/modules/user/constants'; +import { UserCommonDto } from '@/modules/user/dtos/user.common.dto'; + +/** + * 创建用户 + */ +@DtoValidation({ groups: [UserValidateGroup.USER_CREATE] }) +export class CreateUserDto extends PickType(UserCommonDto, [ + 'username', + 'nickname', + 'email', + 'password', + 'phone', +]) {} + +/** + * 更新用户 + */ +@DtoValidation({ groups: [UserValidateGroup.USER_UPDATE] }) +export class UpdateUserDto extends PartialType(CreateUserDto) { + /** + * 待更新的用户ID + */ + @IsUUID(undefined, { message: '用户ID格式不正确', groups: [UserValidateGroup.USER_UPDATE] }) + @IsDefined({ groups: ['update'], message: '用户ID必须指定' }) + id: string; +} + +/** + * 查询用户列表的Query数据验证 + */ +export class QueryUserDto extends PaginateWithTrashedDto { + /** + * 排序规则:可指定用户列表的排序规则,默认为按创建时间降序排序 + */ + @IsEnum(UserOrderType) + orderBy?: UserOrderType; +}