format + lint
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { IsString, Length, Matches } from "class-validator";
|
||||
import { IsString, Length, Matches } from 'class-validator';
|
||||
|
||||
export class UpdateUserPasswordDto {
|
||||
@IsString({ message: '密码不得为空' })
|
||||
@Length(6, 32, { message: '密码长度只能为6~32' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
|
||||
{ message: '密码必须包含字母和数字,且长度在6~32之间' }
|
||||
)
|
||||
password: string;
|
||||
}
|
||||
@IsString({ message: '密码不得为空' })
|
||||
@Length(6, 32, { message: '密码长度只能为6~32' })
|
||||
@Matches(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
|
||||
{ message: '密码必须包含字母和数字,且长度在6~32之间' },
|
||||
)
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
@Index(['sessionId', 'userId'])
|
||||
export class UserSession {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 36 })
|
||||
sessionId: string;
|
||||
@Column({ length: 36 })
|
||||
sessionId: string;
|
||||
|
||||
@Column({ length: 36 })
|
||||
userId: string;
|
||||
@Column({ length: 36 })
|
||||
userId: string;
|
||||
|
||||
@CreateDateColumn({ precision: 3 })
|
||||
createdAt: Date;
|
||||
@CreateDateColumn({ precision: 3 })
|
||||
createdAt: Date;
|
||||
|
||||
@DeleteDateColumn({ nullable: true, precision: 3 })
|
||||
deletedAt: Date;
|
||||
}
|
||||
@DeleteDateColumn({ nullable: true, precision: 3 })
|
||||
deletedAt: Date;
|
||||
}
|
||||
|
||||
@@ -1,72 +1,87 @@
|
||||
import { BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Entity()
|
||||
@Index("IDX_user_userid", ["userId"], { unique: true })
|
||||
@Index("IDX_user_username", ["username"], { unique: true })
|
||||
@Index("IDX_user_email", ["email"], { unique: true, where: "email IS NOT NULL" })
|
||||
@Index("IDX_user_phone", ["phone"], { unique: true, where: "phone IS NOT NULL" })
|
||||
@Index('IDX_user_userid', ['userId'], { unique: true })
|
||||
@Index('IDX_user_username', ['username'], { unique: true })
|
||||
@Index('IDX_user_email', ['email'], {
|
||||
unique: true,
|
||||
where: 'email IS NOT NULL',
|
||||
})
|
||||
@Index('IDX_user_phone', ['phone'], {
|
||||
unique: true,
|
||||
where: 'phone IS NOT NULL',
|
||||
})
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
userId: string;
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
userId: string;
|
||||
|
||||
@Column({ length: 32 })
|
||||
username: string;
|
||||
@Column({ length: 32 })
|
||||
username: string;
|
||||
|
||||
@Column({ length: 30 })
|
||||
nickname: string;
|
||||
@Column({ length: 30 })
|
||||
nickname: string;
|
||||
|
||||
@BeforeInsert()
|
||||
generateDefaults() {
|
||||
if (!this.username) {
|
||||
this.username = `user_${uuidv4().replace(/-/g, '').slice(0, 27)}`;
|
||||
}
|
||||
if (!this.nickname) {
|
||||
this.nickname = `用户_${uuidv4().replace(/-/g, '').slice(0, 8)}`;
|
||||
}
|
||||
@BeforeInsert()
|
||||
generateDefaults() {
|
||||
if (!this.username) {
|
||||
this.username = `user_${uuidv4().replace(/-/g, '').slice(0, 27)}`;
|
||||
}
|
||||
if (!this.nickname) {
|
||||
this.nickname = `用户_${uuidv4().replace(/-/g, '').slice(0, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@Column({ nullable: true, type: 'char', length: 32 })
|
||||
salt: string;
|
||||
@Column({ nullable: true, type: 'char', length: 32 })
|
||||
salt: string;
|
||||
|
||||
@Column({ nullable: true, type: 'char', length: 64 })
|
||||
password_hash: string;
|
||||
@Column({ nullable: true, type: 'char', length: 64 })
|
||||
password_hash: string;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
length: 254,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
}
|
||||
})// RFC 5321
|
||||
email: string | null;
|
||||
@Column({
|
||||
nullable: true,
|
||||
length: 254,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
},
|
||||
}) // RFC 5321
|
||||
email: string | null;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
length: 20,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
}
|
||||
})// China Mainland
|
||||
phone: string | null;
|
||||
@Column({
|
||||
nullable: true,
|
||||
length: 20,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
},
|
||||
}) // China Mainland
|
||||
phone: string | null;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
}
|
||||
})
|
||||
avatar: string;
|
||||
@Column({
|
||||
nullable: true,
|
||||
transformer: {
|
||||
to: (value: string | null) => value?.trim() || null,
|
||||
from: (value: string | null) => value,
|
||||
},
|
||||
})
|
||||
avatar: string;
|
||||
|
||||
@CreateDateColumn({ precision: 3 })
|
||||
createdAt: Date;
|
||||
@CreateDateColumn({ precision: 3 })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ precision: 3 })
|
||||
updatedAt: Date;
|
||||
@UpdateDateColumn({ precision: 3 })
|
||||
updatedAt: Date;
|
||||
|
||||
@DeleteDateColumn({ nullable: true, precision: 3 })
|
||||
deletedAt: Date;
|
||||
}
|
||||
@DeleteDateColumn({ nullable: true, precision: 3 })
|
||||
deletedAt: Date;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { UserSession } from "../entities/user-session.entity";
|
||||
import { Repository } from "typeorm";
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserSession } from '../entities/user-session.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class UserSessionService {
|
||||
constructor(
|
||||
@InjectRepository(UserSession)
|
||||
private readonly userSessionRepository: Repository<UserSession>,
|
||||
) { }
|
||||
constructor(
|
||||
@InjectRepository(UserSession)
|
||||
private readonly userSessionRepository: Repository<UserSession>,
|
||||
) {}
|
||||
|
||||
async createSession(userId: string, sessionId: string): Promise<UserSession> {
|
||||
const session = this.userSessionRepository.create({
|
||||
userId,
|
||||
sessionId,
|
||||
});
|
||||
return await this.userSessionRepository.save(session);
|
||||
}
|
||||
|
||||
async createSession(userId: string, sessionId: string): Promise<UserSession> {
|
||||
const session = this.userSessionRepository.create({
|
||||
userId,
|
||||
sessionId,
|
||||
});
|
||||
return await this.userSessionRepository.save(session);
|
||||
async isSessionValid(userId: string, sessionId: string): Promise<boolean> {
|
||||
const session = await this.userSessionRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
sessionId,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return !!session;
|
||||
}
|
||||
|
||||
async invalidateSession(userId: string, sessionId: string): Promise<void> {
|
||||
const session = await this.userSessionRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
sessionId,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (session) {
|
||||
await this.userSessionRepository.softDelete(session.id);
|
||||
}
|
||||
|
||||
async isSessionValid(userId: string, sessionId: string): Promise<boolean> {
|
||||
const session = await this.userSessionRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
sessionId,
|
||||
deletedAt: null,
|
||||
}
|
||||
});
|
||||
|
||||
return !!session;
|
||||
}
|
||||
|
||||
async invalidateSession(userId: string, sessionId: string): Promise<void> {
|
||||
const session = await this.userSessionRepository.findOne({
|
||||
where: {
|
||||
userId,
|
||||
sessionId,
|
||||
deletedAt: null,
|
||||
}
|
||||
});
|
||||
|
||||
if (session) {
|
||||
await this.userSessionRepository.softDelete(session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,25 +6,21 @@ import { AuthService } from 'src/auth/auth.service';
|
||||
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly authService: AuthService,
|
||||
) { }
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('me')
|
||||
async getMe(@Request() req) {
|
||||
const { user } = req;
|
||||
return this.userService.findOne({ userId: user.userId });
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Get('me')
|
||||
async getMe(@Request() req) {
|
||||
const { user } = req;
|
||||
return this.userService.findOne({ userId: user.userId });
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('password')
|
||||
async update(
|
||||
@Request() req,
|
||||
@Body() dto: UpdateUserPasswordDto,
|
||||
) {
|
||||
return this.userService.setPassword(req.user.userId, dto.password);
|
||||
}
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Put('password')
|
||||
async update(@Request() req, @Body() dto: UpdateUserPasswordDto) {
|
||||
return this.userService.setPassword(req.user.userId, dto.password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import { AuthModule } from 'src/auth/auth.module';
|
||||
import { UserSessionService } from './services/user-session.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, UserSession]),
|
||||
forwardRef(() => AuthModule),// 解决循环依赖问题
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [UserService, UserSessionService],
|
||||
exports: [UserService, UserSessionService],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, UserSession]),
|
||||
forwardRef(() => AuthModule), // 解决循环依赖问题
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [UserService, UserSessionService],
|
||||
exports: [UserService, UserSessionService],
|
||||
})
|
||||
export class UserModule { }
|
||||
export class UserModule {}
|
||||
|
||||
@@ -1,124 +1,138 @@
|
||||
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
import { QueryFailedError, Repository } from 'typeorm';
|
||||
import { createHash, ECDH } from 'crypto';
|
||||
import { createHash } from 'crypto';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
type UserFindOptions = Partial<Pick<User, 'userId' | 'username' | 'phone' | 'email'>>;
|
||||
type UserFindOptions = Partial<
|
||||
Pick<User, 'userId' | 'username' | 'phone' | 'email'>
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) { }
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async findOne(options: UserFindOptions | UserFindOptions[], additionalOptions?: { withDeleted?: boolean }): Promise<User | null> {
|
||||
if (Object.keys(options).length === 0) {
|
||||
throw new BadRequestException('查询条件不能为空');
|
||||
}
|
||||
return this.userRepository.findOne({
|
||||
where: options,
|
||||
withDeleted: additionalOptions?.withDeleted || false,
|
||||
});
|
||||
async findOne(
|
||||
options: UserFindOptions | UserFindOptions[],
|
||||
additionalOptions?: { withDeleted?: boolean },
|
||||
): Promise<User | null> {
|
||||
if (Object.keys(options).length === 0) {
|
||||
throw new BadRequestException('查询条件不能为空');
|
||||
}
|
||||
return this.userRepository.findOne({
|
||||
where: options,
|
||||
withDeleted: additionalOptions?.withDeleted || false,
|
||||
});
|
||||
}
|
||||
|
||||
async create(user: Partial<User>): Promise<User> {
|
||||
try {
|
||||
const newUser = this.userRepository.create(user);
|
||||
return await this.userRepository.save(newUser);
|
||||
} catch (error) {
|
||||
if (error instanceof QueryFailedError) {
|
||||
throw new ConflictException(this.getDuplicateErrorMessage(error));
|
||||
}
|
||||
throw new BadRequestException('创建用户失败');
|
||||
}
|
||||
}
|
||||
|
||||
async update(userId: string, user: Partial<User>): Promise<User> {
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
if (!existingUser) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
try {
|
||||
Object.assign(existingUser, user);
|
||||
return await this.userRepository.save(existingUser);
|
||||
} catch (error) {
|
||||
if (error instanceof QueryFailedError) {
|
||||
throw new ConflictException(this.getDuplicateErrorMessage(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(userId: string, soft: boolean) {
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { userId },
|
||||
withDeleted: true,
|
||||
});
|
||||
if (!existingUser) {
|
||||
throw new BadRequestException('用户不存在');
|
||||
}
|
||||
|
||||
async create(user: Partial<User>): Promise<User> {
|
||||
try {
|
||||
const newUser = this.userRepository.create(user);
|
||||
return await this.userRepository.save(newUser);
|
||||
} catch (error) {
|
||||
if (error instanceof QueryFailedError) {
|
||||
throw new ConflictException(this.getDuplicateErrorMessage(error));
|
||||
}
|
||||
throw new BadRequestException('创建用户失败');
|
||||
}
|
||||
if (existingUser.deletedAt && soft) {
|
||||
throw new BadRequestException('账户已注销,不得重复操作');
|
||||
}
|
||||
|
||||
async update(userId: string, user: Partial<User>): Promise<User> {
|
||||
const existingUser = await this.userRepository.findOne({ where: { userId } });
|
||||
if (!existingUser) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
try {
|
||||
Object.assign(existingUser, user);
|
||||
return await this.userRepository.save(existingUser);
|
||||
} catch (error) {
|
||||
if (error instanceof QueryFailedError) {
|
||||
throw new ConflictException(this.getDuplicateErrorMessage(error));
|
||||
}
|
||||
}
|
||||
if (!existingUser.deletedAt && !soft) {
|
||||
throw new BadRequestException('账号未注销,请先注销再执行删除操作');
|
||||
}
|
||||
|
||||
async delete(userId: string, soft: boolean) {
|
||||
const existingUser = await this.userRepository.findOne({ where: { userId }, withDeleted: true });
|
||||
if (!existingUser) {
|
||||
throw new BadRequestException('用户不存在');
|
||||
}
|
||||
return soft
|
||||
? await this.userRepository.softDelete(existingUser.userId)
|
||||
: await this.userRepository.delete(existingUser.userId);
|
||||
}
|
||||
|
||||
if (existingUser.deletedAt && soft) {
|
||||
throw new BadRequestException('账户已注销,不得重复操作')
|
||||
}
|
||||
hashPassword(password: string, salt: string): string {
|
||||
return createHash('sha256').update(`${password}${salt}`).digest('hex');
|
||||
}
|
||||
|
||||
if (!existingUser.deletedAt && !soft) {
|
||||
throw new BadRequestException('账号未注销,请先注销再执行删除操作')
|
||||
}
|
||||
generateSalt(): string {
|
||||
return uuid().replace(/-/g, '');
|
||||
}
|
||||
|
||||
return soft
|
||||
? await this.userRepository.softDelete(existingUser.userId)
|
||||
: await this.userRepository.delete(existingUser.userId)
|
||||
async setPassword(userId: string, password: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ where: { userId } });
|
||||
if (!user) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
const salt = this.generateSalt();
|
||||
user.password_hash = this.hashPassword(password, salt);
|
||||
user.salt = salt;
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
hashPassword(password: string, salt: string): string {
|
||||
return createHash('sha256').update(`${password}${salt}`).digest('hex');
|
||||
private getDuplicateErrorMessage(error: QueryFailedError): string {
|
||||
// 根据具体的错误信息返回友好的提示
|
||||
if (error.message.includes('IDX_user_username')) {
|
||||
return '账户名已被使用';
|
||||
}
|
||||
|
||||
generateSalt(): string {
|
||||
return uuid().replace(/-/g, '');
|
||||
if (error.message.includes('IDX_user_email')) {
|
||||
return '邮箱已被使用';
|
||||
}
|
||||
|
||||
async setPassword(userId: string, password: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ where: { userId } });
|
||||
if (!user) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
const salt = this.generateSalt();
|
||||
user.password_hash = this.hashPassword(password, salt);
|
||||
user.salt = salt;
|
||||
return this.userRepository.save(user);
|
||||
if (error.message.includes('IDX_user_phone')) {
|
||||
return '手机号已被使用';
|
||||
}
|
||||
return '数据已存在,请检查输入';
|
||||
}
|
||||
|
||||
private getDuplicateErrorMessage(error: QueryFailedError): string {
|
||||
// 根据具体的错误信息返回友好的提示
|
||||
if (error.message.includes('IDX_user_username')) {
|
||||
return '账户名已被使用';
|
||||
}
|
||||
if (error.message.includes('IDX_user_email')) {
|
||||
return '邮箱已被使用';
|
||||
}
|
||||
if (error.message.includes('IDX_user_phone')) {
|
||||
return '手机号已被使用';
|
||||
}
|
||||
return '数据已存在,请检查输入';
|
||||
}
|
||||
async list(page = 1, pageSize = 20) {
|
||||
const queryBuilder = this.userRepository.createQueryBuilder('user');
|
||||
|
||||
async list(page = 1, pageSize = 20) {
|
||||
const queryBuilder = this.userRepository.createQueryBuilder('user')
|
||||
queryBuilder.withDeleted();
|
||||
|
||||
queryBuilder.withDeleted();
|
||||
queryBuilder.orderBy('user.createdAt', 'DESC');
|
||||
|
||||
queryBuilder.orderBy('user.createdAt', 'DESC');
|
||||
queryBuilder.skip((page - 1) * pageSize);
|
||||
queryBuilder.take(pageSize);
|
||||
|
||||
queryBuilder.skip((page - 1) * pageSize);
|
||||
queryBuilder.take(pageSize);
|
||||
|
||||
const [items, total] = await queryBuilder.getManyAndCount();
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
}
|
||||
}
|
||||
const [items, total] = await queryBuilder.getManyAndCount();
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user