创建user模块
先用nest的命令创建一个 user 模块,
nest g res user
实现user实体
然后就生成了 user 模块,在它的实体中创建一个用户表user.entity.ts
,包含 id、用户名、密码
,头像、邮箱
等等一些字段:
@Entity('sys_user')
export class UserEntity {@Expose()@ApiProperty({ type: String, description: 'id' })@PrimaryGeneratedColumn({ type: 'bigint' })public id: string@Exclude({ toPlainOnly: true }) // 输出屏蔽密码@Column({ type: 'varchar', length: 200, nullable: false, comment: '用户登录密码' })public password: string@Exclude({ toPlainOnly: true }) // 输出屏蔽盐@Column({ type: 'varchar', length: 200, nullable: false, comment: '盐' })public salt: string@Expose()@ApiProperty({ type: String, description: '用户登录账号' })@Column({ type: 'varchar', length: 32, comment: '用户登录账号' })public account: string@Expose()@ApiProperty({ type: String, description: '手机号' })@Column({ type: 'varchar', name: 'phone_num', default: '', length: 20, comment: '用户手机号码' })public phoneNum: string@Expose()@ApiProperty({ type: String, description: '邮箱' })@Column({ type: 'varchar', comment: '邮箱地址', default: '' })public email: string....
}
实现注册路由
接下来看一下注册基本逻辑的实现。注册无非就是新增一个用户,在user.controller.ts
规定一个路由/user/register
接收用户传来的参数
@Post('register')@ApiOperation({ summary: '用户注册' })@ApiResult(UserEntity)@AllowAnon()async create(@Body() user: CreateUserDto): Promise<UserEntity> {return await this.userService.create(user)}
并将CreateUserDto
实现如下:
export class CreateUserDto {@ApiProperty({ description: '用户账号' })@IsString({ message: 'account 类型错误,正确类型 string' })@IsNotEmpty({ message: 'account 不能为空' })@MinLength(5, { message: '账号至少5个字符' })@MaxLength(20, { message: '账号最多20个字符' })readonly account: string@ApiProperty({ description: '密码' })@IsString({ message: 'password 类型错误,正确类型 string' })@IsNotEmpty({ message: 'password 不能为空' })password: string@ApiProperty({ description: '手机号', required: false })@IsString({ message: 'phoneNum 类型错误,正确类型 string' })@IsMobilePhone('zh-CN', { strictMode: false }, { message: '请输入正确的手机号' })@IsOptional()readonly phoneNum?: string@ApiProperty({ description: '邮箱', required: false })@IsString({ message: 'email 类型错误,正确类型 string' })@IsEmail()@IsOptional()readonly email?: string@ApiProperty({ description: '确认密码' })@IsString({ message: ' confirmPassword 类型错误,正确类型 string' })readonly confirmPassword: string@ApiProperty({ description: '头像', required: false })@IsString({ message: 'avatar 类型错误,正确类型 string' })@IsOptional()readonly avatar?: string
}
实现注册逻辑
在user.service.ts
写注册的逻辑:
/** 创建用户 */async create(dto: CreateUserDto): Promise<UserEntity> {if (dto.password !== dto.confirmPassword)throw new ApiException(ApiErrorCode.USER_PASSWORD_INVALID, '两次输入密码不一致,请重试')// 防止重复创建 startif (await this.findOneByAccount(dto.account))throw new ApiException(ApiErrorCode.USER_CREATE_EXISTING, '帐号已存在,请调整后重新注册!')if (await this.userRepo.findOne({ where: { phoneNum: dto.phoneNum } }))throw new ApiException(ApiErrorCode.USER_CREATE_EXISTING, '当前手机号已存在,请调整后重新注册')if (await this.userRepo.findOne({ where: { email: dto.email } }))throw new ApiException(ApiErrorCode.USER_CREATE_EXISTING, '当前邮箱已存在,请调整后重新注册')// 防止重复创建 endconst salt = await genSalt()dto.password = await hash(dto.password, salt)// plainToInstance 忽略转换 @Exclude 装饰器const user = plainToInstance(UserEntity, { salt, ...dto }, { ignoreDecorators: true })const result = await this.userManager.transaction(async (transactionalEntityManager) => {return await transactionalEntityManager.save<UserEntity>(user)})return result}
这里我们对提交的数据做一些判断,并对一些账号已存在的情况抛出具体的异常,来提醒用户。由于我们不能存储密码的明文,这里使用bcryptjs来生成salt,然后对密码进行hash之后,再将salt和hash值同用户信息一起存入数据库。需要安装依赖库:
npm i bcryptjs
npm i --save-dev @types/bcryptjs
测试注册接口
模拟一下注册请求:
发现注册成功了:
返回数据修复
但是这里有个问题,返回的数据里既包含了存入数据库的salt和password,也包含了仅用于数据验证的confirmPassword。我们要将返回类型修改为Partial<UserEntity>,并且利用 class-transformer将salt、password和confirmPassword排除在外,实现修改如下:
/** 创建用户 */async create(dto: CreateUserDto): Promise<Partial<UserEntity>> {if (dto.password !== dto.confirmPassword)throw new ApiException(ApiErrorCode.USER_PASSWORD_INVALID, '两次输入密码不一致,请重试')// 防止重复创建 startif (await this.findOneByAccount(dto.account))throw new ApiException(ApiErrorCode.USER_CREATE_EXISTING, '帐号已存在,请调整后重新注册!')if (await this.userRepo.findOne({ where: { phoneNum: dto.phoneNum } }))throw new ApiException(ApiErrorCode.USER_CREATE_EXISTING, '当前手机号已存在,请调整后重新注册')if (await this.userRepo.findOne({ where: { email: dto.email } }))throw new ApiException(ApiErrorCode.USER_CREATE_EXISTING, '当前邮箱已存在,请调整后重新注册')// 防止重复创建 endconst salt = await genSalt()dto.password = await hash(dto.password, salt)// plainToInstance 忽略转换 @Exclude 装饰器const user = plainToInstance(UserEntity, { salt, ...dto }, { ignoreDecorators: true })const result = await this.userManager.transaction(async (transactionalEntityManager) => {return await transactionalEntityManager.save<UserEntity>(user)})//去掉salt,password,confirmPasswordreturn plainToInstance(UserEntity, instanceToPlain(result), { excludeExtraneousValues: true })}
再请求一下,发现返回接口里不包含敏感信息了: