format + lint

This commit is contained in:
2025-06-14 14:12:18 +08:00
parent 95e8f8c648
commit 1de3a3f197
69 changed files with 1756 additions and 1583 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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 {}

View File

@@ -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,
};
}
}