feat(25-06-03): 修改重置密码
This commit is contained in:
parent
ab7badc4ae
commit
5407e27b13
147
star-tune/cursor.md
Normal file
147
star-tune/cursor.md
Normal 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
|
@ -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;
|
||||
},
|
||||
);
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将用户信息附加到请求对象
|
||||
|
@ -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',
|
||||
);
|
||||
|
||||
// 测试不同级别的日志输出
|
||||
|
14
star-tune/src/module/user/dto/change-password.dto.ts
Normal file
14
star-tune/src/module/user/dto/change-password.dto.ts
Normal 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;
|
||||
}
|
17
star-tune/src/module/user/dto/reset-password.dto.ts
Normal file
17
star-tune/src/module/user/dto/reset-password.dto.ts
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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: '密码重置成功' };
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user