feat(25-06-03): 修改重置密码

This commit is contained in:
nie 2025-06-04 22:44:02 +08:00
parent ab7badc4ae
commit 5407e27b13
9 changed files with 340 additions and 48 deletions

147
star-tune/cursor.md Normal file
View File

@ -0,0 +1,147 @@
# 本项目使用cursou说明
## 服务注入
> 大部分服务已经全局注入直接在service中使用即可
- 日志服务private readonly logger: CustomLogger,
- redis服务private readonly redis: RedisService,
- 配置服务private readonly config: ConfigService,
- 工具服务private readonly utils: UtilsService,
- 数据库服务private readonly database: DatabaseService,
以上服务均已在appModule全局注入不需要再向module中注入
## 路径别名
@代指src目录,请注意,引入任何文件采用路径别名,除开当前目录下的文件,不允许使用../获取上级目录
## 数据库
1. 数据库实体在@/drizzle/schema通过这样导入
```ts
import {
user,
userPassword,
userProfile,
userSignatureHistory,
} from '@/drizzle/schema';
```
**注意**操作数据严格按照数据库实体的定义
2. 数据库使用drizzle和mysql2下面是使用示例
```ts
import { user } from '@/drizzle/schema';
// dto为参数
const userExists = await this.database.db
.select({
userId: user.userId,
})
.from(user)
.where(
or(
eq(user.email, dto.email),
eq(user.username, dto.username),
),
)
.execute();
if (userExists.length > 0) {
throw new BadRequestException('用户已存在');
}
```
## 注释
**非常重要**
1. 编写代码已经要有非常详细的注释,最好每行都有注释
参考
```ts
// 检查邮箱是否可用
async checkEmail(dto: CheckEmailDto) {
const exists = await this.database.db
.select()
.from(user)
.where(and(eq(user.email, dto.email), eq(user.isDeleted, 0)))
.execute();
return { available: exists.length === 0 };
}
// 发送验证码
async sendEmailCode(dto: SendEmailCodeDto) {
// 邮箱验证码key
const codeKey = `${EmailCodeType[dto.type]}:${dto.email}`;
// 判断Key是否存在
const keyExists = await this.redis.exists(codeKey);
// 验证码过期时间
const codeEX = this.config.get<number>('email.codeEX') || 300;
// 验证码冷却时间
const codeEP = this.config.get<number>('email.codeEP') || 60;
// 判断是否存在验证码
if (keyExists) {
// 获取Key的过期时间
const ttl = await this.redis.ttl(codeKey);
if (ttl > codeEX - codeEP) {
// 再等等吧
throw new BadRequestException(`请等待${ttl}秒后再试`);
} else {
// 续杯
await this.redis.expire(codeKey, codeEX);
// 获取验证码
const code = await this.redis.get(codeKey);
// todo重新发送验证码
this.logger.debug(`重新发送验证码: ${code}`);
await this.utils.sendEmail({
to: dto.email,
subject: '账户注册验证码',
text: `您的验证码是:${code}5分钟内有效`,
});
}
} else {
// 生成验证码
const code = Math.random().toString().slice(2, 8);
// todo发送验证码
this.logger.debug(`发送验证码: ${code}`);
await this.utils.sendEmail({
to: dto.email,
subject: '账户注册验证码',
text: `您的验证码是:${code}5分钟内有效`,
});
// 存储验证码
await this.redis.set(codeKey, code, codeEX);
}
return { message: '验证码已发送' };
}
```
2. 每个方法前都要携带写作者、写作时间、方法描述,写作者为`Nie`
## 常量
常量这样导入 `import { EmailCodeType } from '@/type/enum';`
常量用于一些系统的关键字
## redis
使用参考
```ts
// 加锁
const lock = await this.redis.lock(
`${EmailCodeType.REGISTER}:${dto.email}`,
);
```
方法一般在RedisService中定义
## 文件命名
采用小驼峰加`.service`、`.dto`类似的方式命名,禁止使用中划线
错误示例
`add-user.dto.ts`
正确示例
`addUser.dto.ts

View File

@ -1,10 +1,10 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { UserGuard } from '@/type/userGuard';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
(data: keyof UserGuard | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<{ user: UserGuard }>();
const user = request.user;
return data ? user?.[data] : user;
},
);

View File

@ -6,7 +6,7 @@ import {
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UtilsService } from '@/common/utils/utils.service';
import { UserGuard } from '@/type/userGuard';
import { UserGuard, UserGuardType } from '@/type/userGuard';
import { FastifyRequest } from 'fastify';
// 扩展 FastifyRequest 类型以包含 user 属性
@ -26,15 +26,14 @@ export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const path = request.url;
console.log(path);
// console.log(path);
// 检查白名单路由
const whitelist = this.config.get<string[]>('jwt.whitelist') || [];
console.log(whitelist);
// console.log(whitelist);
if (whitelist.some((route) => path.startsWith(route))) {
return true;
}
console.log('?', request.headers);
// 获取并验证 token
const authHeader = request.headers.authorization;
@ -48,7 +47,20 @@ export class AuthGuard implements CanActivate {
try {
userGuard = this.utils.verifyToken(token);
} catch {
throw new UnauthorizedException('无效的访问令牌');
throw new UnauthorizedException('无效的访问令牌1');
}
if (this.config.get<string>('env') === 'production') {
if (userGuard.type === UserGuardType.REFRESH) {
if (path === '/api/user/refreshToken') {
return true;
} else {
throw new UnauthorizedException('无效的访问令牌2');
}
} else if (userGuard.type === UserGuardType.ACCESS) {
if (path === '/api/user/refreshToken') {
throw new UnauthorizedException('无效的访问令牌3');
}
}
}
// 将用户信息附加到请求对象

View File

@ -70,6 +70,7 @@ async function bootstrap() {
SwaggerModule.setup('api/docs', app, document);
logger.debug(
`Swagger 配置完成: http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}/api/docs`,
'Swagger',
);
// 测试不同级别的日志输出

View File

@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';
export class ChangePasswordDto {
@ApiProperty({ description: '旧密码' })
@IsString()
@MinLength(6)
oldPassword: string;
@ApiProperty({ description: '新密码' })
@IsString()
@MinLength(6)
newPassword: string;
}

View File

@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength, IsEmail } from 'class-validator';
export class ResetPasswordDto {
@ApiProperty({ description: '邮箱' })
@IsEmail()
email: string;
@ApiProperty({ description: '验证码' })
@IsString()
code: string;
@ApiProperty({ description: '新密码' })
@IsString()
@MinLength(6)
newPassword: string;
}

View File

@ -18,6 +18,8 @@ import { EmailLoginDto } from './dto/email-login.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UpdateProfileDto } from './dto/update-profile.dto';
import { UpdateSignatureDto } from './dto/update-signature.dto';
import { ChangePasswordDto } from './dto/change-password.dto';
import { ResetPasswordDto } from './dto/reset-password.dto';
import { AuthGuard } from '../../guards/auth.guard';
import { User } from '../../decorators/user.decorator';
import {
@ -26,6 +28,7 @@ import {
ApiBearerAuth,
ApiResponse,
} from '@nestjs/swagger';
import { UserGuard } from '@/type/userGuard';
@ApiTags('用户管理')
@ApiBearerAuth('access-token')
@ -62,7 +65,7 @@ export class UserController {
@ApiOperation({ summary: '检查邮箱是否可用' })
@ApiResponse({ status: 200, description: '邮箱检查结果' })
@Post('check-email')
@Post('checkEmail')
checkEmail(@Body() dto: CheckEmailDto) {
return this.userService.checkEmail(dto);
}
@ -73,7 +76,7 @@ export class UserController {
status: 400,
description: '发送失败,可能是因为发送过于频繁',
})
@Post('send-code')
@Post('sendCode')
sendEmailCode(@Body() dto: SendEmailCodeDto) {
return this.userService.sendEmailCode(dto);
}
@ -101,13 +104,11 @@ export class UserController {
// return this.userService.emailLogin(dto);
// }
// @Post('refresh-token')
// refreshToken(@Headers('refresh-token') refreshToken: string) {
// if (!refreshToken) {
// throw new UnauthorizedException('缺少刷新令牌');
// }
// return this.userService.refreshToken(refreshToken);
// }
@UseGuards(AuthGuard)
@Post('refreshToken')
refreshToken(@User() userGuard: UserGuard) {
return this.userService.refreshToken(userGuard);
}
// @Post('logout')
// @UseGuards(AuthGuard)
@ -156,4 +157,25 @@ export class UserController {
// deleteUser(@User('userId') userId: number) {
// return this.userService.deleteUser(userId);
// }
@ApiOperation({ summary: '修改密码' })
@ApiResponse({ status: 200, description: '密码修改成功' })
@ApiResponse({ status: 400, description: '旧密码错误' })
@ApiBearerAuth('access-token')
@Patch('password')
@UseGuards(AuthGuard)
changePassword(
@User('userId') userId: number,
@Body() dto: ChangePasswordDto,
) {
return this.userService.changePassword(userId, dto);
}
@ApiOperation({ summary: '重置密码(忘记密码)' })
@ApiResponse({ status: 200, description: '密码重置成功' })
@ApiResponse({ status: 400, description: '验证码错误或已过期' })
@Post('resetPassword')
resetPassword(@Body() dto: ResetPasswordDto) {
return this.userService.resetPassword(dto);
}
}

View File

@ -3,10 +3,15 @@ import {
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import * as dayjs from 'dayjs';
import { eq, and, or } from 'drizzle-orm';
import { RedisService } from '@/service/redis/redis.service';
import { ConfigService } from '@nestjs/config';
import { UtilsService } from '@/common/utils/utils.service';
import { eq, and, or } from 'drizzle-orm';
import { DatabaseService } from '@/service/database/database.service';
import { CustomLogger } from '@/common/logger/logger.service';
import { CheckEmailDto } from './dto/check-email.dto';
import { SendEmailCodeDto } from './dto/send-email-code.dto';
import { RegisterDto } from './dto/register.dto';
@ -15,20 +20,16 @@ import { EmailLoginDto } from './dto/email-login.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UpdateProfileDto } from './dto/update-profile.dto';
import { UpdateSignatureDto } from './dto/update-signature.dto';
import * as bcrypt from 'bcrypt';
import * as jwt from 'jsonwebtoken';
import { DatabaseService } from '@/service/database/database.service';
import { ChangePasswordDto } from './dto/change-password.dto';
import { ResetPasswordDto } from './dto/reset-password.dto';
import {
user,
userPassword,
userProfile,
userSignatureHistory,
} from '@/drizzle/schema';
import { CustomLogger } from '@/common/logger/logger.service';
import { EmailCodeType } from '@/type/enum';
import * as dayjs from 'dayjs';
import { UserGuard } from '@/type/userGuard';
import { SQL } from 'drizzle-orm';
@Injectable()
export class UserService {
@ -259,29 +260,17 @@ export class UserService {
return { accessToken, refreshToken };
}
// // 刷新令牌
// async refreshToken(refreshToken: string) {
// try {
// const decoded = jwt.verify(
// refreshToken,
// this.config.get('JWT_SECRET'),
// ) as { userId: number };
// const storedToken = await this.redis.get(
// `refresh_token:${decoded.userId}`,
// );
// if (!storedToken || storedToken !== refreshToken) {
// throw new UnauthorizedException('无效的刷新令牌');
// }
// await this.redis.del(`refresh_token:${decoded.userId}`);
// await this.redis.del(`access_token:${decoded.userId}`);
// return this.generateTokens(decoded.userId);
// } catch (error) {
// throw new UnauthorizedException('无效的刷新令牌');
// }
// }
// 刷新令牌
refreshToken(userGuard: UserGuard) {
const { userId, username, email, time } = userGuard;
const accessToken = this.utils.generateAccessToken({
userId,
username,
email,
time,
});
return { accessToken };
}
// // 退出登录
// async logout(userId: number) {
@ -367,4 +356,92 @@ export class UserService {
// await this.logout(userId);
// return { message: '删除成功' };
// }
async changePassword(userId: number, dto: ChangePasswordDto) {
const result = await this.database.db
.select({
userId: user.userId,
})
.from(user)
.where(eq(user.userId, userId))
.execute();
if (result.length === 0) {
throw new BadRequestException('用户不存在');
}
// 比对密码
const userData = result[0];
const passwordData = await this.database.db
.select({
passwordHash: userPassword.passwordHash,
})
.from(userPassword)
.where(eq(userPassword.userId, userData.userId))
.execute();
// 验证旧密码
if (
!(await this.utils.verifyPassword(
dto.oldPassword,
passwordData[0].passwordHash,
))
) {
throw new UnauthorizedException('旧密码错误');
}
// 加密新密码
const hashedPassword = await this.utils.hashPassword(dto.newPassword);
// 更新密码
await this.database.db
.update(userPassword)
.set({
passwordHash: hashedPassword,
})
.where(eq(userPassword.userId, userId));
return { message: '密码修改成功' };
}
async resetPassword(dto: ResetPasswordDto) {
// 邮箱验证码key
const codeKey = `${EmailCodeType.resetpassword}:${dto.email}`;
this.logger.debug(
`邮箱验证码key: ${EmailCodeType.resetpassword}:${dto.email} ${codeKey}`,
);
// 获取验证码
const code = await this.redis.get(codeKey);
// 验证码错误或已过期
if (!code || code !== dto.code) {
throw new BadRequestException('验证码错误或已过期');
}
const result = await this.database.db
.select({
userId: user.userId,
})
.from(user)
.where(eq(user.email, dto.email))
.execute();
if (result.length === 0) {
throw new BadRequestException('用户不存在');
}
// 加密新密码
const hashedPassword = await this.utils.hashPassword(dto.newPassword);
// 更新密码
// 更新密码
await this.database.db
.update(userPassword)
.set({
passwordHash: hashedPassword,
})
.where(eq(userPassword.userId, result[0].userId));
// 删除验证码
await this.redis.del(codeKey);
return { message: '密码重置成功' };
}
}

View File

@ -4,6 +4,8 @@ export enum EmailCodeType {
LOGIN = 'login',
register = 'REGISTER',
login = 'LOGIN',
resetpassword = 'RESETPASSWORD',
RESETPASSWORD = 'resetpassword',
}
export const allColors = {
@ -57,4 +59,4 @@ export const allColors = {
strikethrough: '\x1b[9m', // 删除线
},
reset: '\x1b[0m',
};
};