Compare commits
2 Commits
ee5af33a96
...
040b0b71d5
Author | SHA1 | Date | |
---|---|---|---|
040b0b71d5 | |||
92cfd94093 |
@ -21,7 +21,7 @@ export class MatchConstraint implements ValidatorConstraintInterface {
|
||||
}
|
||||
}
|
||||
|
||||
export function isMatch(
|
||||
export function IsMatch(
|
||||
relatedProperty: string,
|
||||
reverse = false,
|
||||
validationOptions?: ValidationOptions,
|
||||
|
33
src/modules/user/constants.ts
Normal file
33
src/modules/user/constants.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 用户请求DTO验证组
|
||||
*/
|
||||
export enum UserValidateGroup {
|
||||
/**
|
||||
* 创建用户验证
|
||||
*/
|
||||
USER_CREATE = 'user_create',
|
||||
/**
|
||||
* 更新用户验证
|
||||
*/
|
||||
USER_UPDATE = 'user_update',
|
||||
/**
|
||||
* 用户注册验证
|
||||
*/
|
||||
USER_REGISTER = 'user_register',
|
||||
/**
|
||||
* 用户账户信息更新验证
|
||||
*/
|
||||
ACCOUNT_UPDATE = 'account_update',
|
||||
/**
|
||||
* 用户密码更新验证
|
||||
*/
|
||||
CHANGE_PASSWORD = 'change_password',
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户查询排序
|
||||
*/
|
||||
export enum UserOrderType {
|
||||
CREATED = 'createdAt',
|
||||
UPDATED = 'updatedAt',
|
||||
}
|
40
src/modules/user/dtos/account.dto.ts
Normal file
40
src/modules/user/dtos/account.dto.ts
Normal file
@ -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;
|
||||
}
|
21
src/modules/user/dtos/auth.dto.ts
Normal file
21
src/modules/user/dtos/auth.dto.ts
Normal file
@ -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) {}
|
114
src/modules/user/dtos/user.common.dto.ts
Normal file
114
src/modules/user/dtos/user.common.dto.ts
Normal file
@ -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;
|
||||
}
|
44
src/modules/user/dtos/user.dto.ts
Normal file
44
src/modules/user/dtos/user.dto.ts
Normal file
@ -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;
|
||||
}
|
@ -11,6 +11,7 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { CommentEntity, PostEntity } from '@/modules/content/entities';
|
||||
import { AccessTokenEntity } from '@/modules/user/entities/access.token.entity';
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
@ -94,4 +95,10 @@ export class UserEntity {
|
||||
*/
|
||||
@OneToMany(() => CommentEntity, (comment) => comment.author, { cascade: true })
|
||||
comments: Relation<CommentEntity>[];
|
||||
|
||||
/**
|
||||
* 登录token
|
||||
*/
|
||||
@OneToMany(() => AccessTokenEntity, (token) => token.user, { cascade: true })
|
||||
accessTokens: Relation<AccessTokenEntity>[];
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Entity, OneToOne } from 'typeorm';
|
||||
import { Entity, ManyToOne, OneToOne, Relation } from 'typeorm';
|
||||
|
||||
import { UserEntity } from '@/modules/user/entities/UserEntity';
|
||||
import { BaseToken } from '@/modules/user/entities/base.token';
|
||||
import { RefreshTokenEntity } from '@/modules/user/entities/refresh.token.entity';
|
||||
|
||||
@ -13,4 +14,10 @@ export class AccessTokenEntity extends BaseToken {
|
||||
*/
|
||||
@OneToOne(() => RefreshTokenEntity, (token) => token.accessToken, { cascade: true })
|
||||
refreshToken: string;
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
@ManyToOne(() => UserEntity, (user) => user.accessTokens, { onDelete: 'CASCADE' })
|
||||
user: Relation<UserEntity>;
|
||||
}
|
||||
|
12
src/modules/user/repositories/UserRepository.ts
Normal file
12
src/modules/user/repositories/UserRepository.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { BaseRepository } from '@/modules/database/base/repository';
|
||||
import { CustomRepository } from '@/modules/database/decorators/repository.decorator';
|
||||
import { UserEntity } from '@/modules/user/entities/UserEntity';
|
||||
|
||||
@CustomRepository(UserEntity)
|
||||
export class UserRepository extends BaseRepository<UserEntity> {
|
||||
protected _qbName: string = 'user';
|
||||
|
||||
buildBaseQuery() {
|
||||
return this.createQueryBuilder(this.qbName).orderBy(`${this.qbName}.createdAt`, 'DESC');
|
||||
}
|
||||
}
|
39
src/modules/user/subscribers/UserSubscriber.ts
Normal file
39
src/modules/user/subscribers/UserSubscriber.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm';
|
||||
|
||||
import { BaseSubscriber } from '@/modules/database/base/subscriber';
|
||||
import { UserEntity } from '@/modules/user/entities/UserEntity';
|
||||
import { encrypt } from '@/modules/user/utils';
|
||||
|
||||
@EventSubscriber()
|
||||
export class UserSubscriber extends BaseSubscriber<UserEntity> {
|
||||
protected entity = UserEntity;
|
||||
|
||||
/**
|
||||
* 生成随机用户名
|
||||
* @param event
|
||||
* @protected
|
||||
*/
|
||||
protected async generateUserName(event: InsertEvent<UserEntity>): Promise<string> {
|
||||
const username = `user_${randomBytes(4).toString('hex').slice(0, 8)}`;
|
||||
const user = await event.manager.findOne(UserEntity, { where: { username } });
|
||||
return user ? this.generateUserName(event) : username;
|
||||
}
|
||||
|
||||
async beforeInsert(event: InsertEvent<UserEntity>): Promise<void> {
|
||||
if (!event.entity.username) {
|
||||
event.entity.username = await this.generateUserName(event);
|
||||
}
|
||||
if (!event.entity.password) {
|
||||
event.entity.password = randomBytes(11).toString('hex').slice(0, 22);
|
||||
}
|
||||
event.entity.password = await encrypt(this.configure, event.entity.password);
|
||||
}
|
||||
|
||||
async beforeUpdate(event: UpdateEvent<UserEntity>) {
|
||||
if (this.isUpdated('password', event)) {
|
||||
event.entity.password = await encrypt(this.configure, event.entity.password);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user