From ab7badc4ae89c6cd9a6f7de8441aeb79e3e4e4c9 Mon Sep 17 00:00:00 2001 From: nie Date: Tue, 3 Jun 2025 22:40:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(25-06-03):=20=E7=94=A8=E6=88=B7=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=92=8C=E6=8E=A5=E5=8F=A3=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- star-tune/src/common/config/configuration.ts | 2 +- star-tune/src/common/utils/utils.service.ts | 37 ++++- star-tune/src/guards/auth.guard.ts | 5 +- star-tune/src/main.ts | 19 ++- .../middleware/request-logger.middleware.ts | 4 +- star-tune/src/module/user/user.controller.ts | 24 +-- star-tune/src/module/user/user.service.ts | 137 ++++++++++-------- star-tune/src/type/userGuard.ts | 6 + 8 files changed, 151 insertions(+), 83 deletions(-) diff --git a/star-tune/src/common/config/configuration.ts b/star-tune/src/common/config/configuration.ts index 4961426..7ce3c3f 100644 --- a/star-tune/src/common/config/configuration.ts +++ b/star-tune/src/common/config/configuration.ts @@ -32,7 +32,7 @@ export default () => { '/user/login', '/user/register', '/user/refreshToken', - '/', + // '/', // '/module', '/docs*', '/docs/json', diff --git a/star-tune/src/common/utils/utils.service.ts b/star-tune/src/common/utils/utils.service.ts index c027fcf..09b1f5d 100644 --- a/star-tune/src/common/utils/utils.service.ts +++ b/star-tune/src/common/utils/utils.service.ts @@ -4,7 +4,7 @@ import * as crypto from 'crypto'; import { CustomLogger } from '@/common/logger/logger.service'; import { sign, verify } from 'jsonwebtoken'; import { type StringValue } from 'ms'; -import { UserGuard } from '@/type/userGuard'; +import { UserGuard, UserGuardType } from '@/type/userGuard'; export type SendEmailOptions = { to: string; @@ -97,8 +97,42 @@ export class UtilsService { // 过期时间 const expiresIn = this.configService.get('jwt.accessExpiresIn') || '20m'; + userGuard.type = UserGuardType.ACCESS; return sign(userGuard, secret, { expiresIn }); } + + /** + * 将时间字符串转换为毫秒 + * @param time 时间字符串,例如: '1ms', '1s', '1m', '1h', '1d', '1M', '1y' + * @returns 毫秒数 + * @throws Error 当时间格式不正确时抛出错误 + */ + parseTimeToMs(time: string): number { + const regex = /^(\d+)(ms|s|m|h|d|M|y)$/; + const match = time.match(regex); + + if (!match) { + throw new Error( + 'Invalid time format. Expected format: number + unit (ms|s|m|h|d|M|y)', + ); + } + + const value = parseInt(match[1], 10); + const unit = match[2]; + + const conversions = { + ms: 1, + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000, + M: 30 * 24 * 60 * 60 * 1000, + y: 365 * 24 * 60 * 60 * 1000, + }; + + return value * conversions[unit]; + } + // 生成刷新令牌 generateRefreshToken(userGuard: UserGuard): string { const secret = @@ -106,6 +140,7 @@ export class UtilsService { const expiresIn = this.configService.get('jwt.refreshExpiresIn') || '14d'; + userGuard.type = UserGuardType.REFRESH; return sign(userGuard, secret, { expiresIn }); } diff --git a/star-tune/src/guards/auth.guard.ts b/star-tune/src/guards/auth.guard.ts index 1f434ff..1ff8794 100644 --- a/star-tune/src/guards/auth.guard.ts +++ b/star-tune/src/guards/auth.guard.ts @@ -5,7 +5,6 @@ import { UnauthorizedException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { RedisService } from '@/service/redis/redis.service'; import { UtilsService } from '@/common/utils/utils.service'; import { UserGuard } from '@/type/userGuard'; import { FastifyRequest } from 'fastify'; @@ -21,19 +20,21 @@ declare module 'fastify' { export class AuthGuard implements CanActivate { constructor( private readonly config: ConfigService, - private readonly redis: RedisService, private readonly utils: UtilsService, ) {} canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const path = request.url; + console.log(path); // 检查白名单路由 const whitelist = this.config.get('jwt.whitelist') || []; + console.log(whitelist); if (whitelist.some((route) => path.startsWith(route))) { return true; } + console.log('?', request.headers); // 获取并验证 token const authHeader = request.headers.authorization; diff --git a/star-tune/src/main.ts b/star-tune/src/main.ts index 5867797..1bd0116 100644 --- a/star-tune/src/main.ts +++ b/star-tune/src/main.ts @@ -50,10 +50,21 @@ async function bootstrap() { // 配置 Swagger const config = new DocumentBuilder() - .setTitle('Star Tune API') - .setDescription('Star Tune 项目的 API 文档') - .setVersion('1.0') - .addBearerAuth() + .setTitle('Star Tune API') // 设置API文档标题 + .setDescription('Star Tune 项目的 API 文档') // 设置API文档描述 + .setVersion('1.0') // 设置API版本号 + .addSecurityRequirements('access-token') // 全局启用Bearer认证要求 + .addBearerAuth( + { + type: 'http', // 认证类型为HTTP认证 + scheme: 'bearer', // 使用Bearer方案 + // bearerFormat: 'JWT', // token格式为JWT + // name: 'Authorization', // 请求头的名称 + // description: '请输入 JWT token', // UI中显示的描述文本 + // in: 'header', // token在请求头中传递 + }, + 'access-token', // 安全方案的名称 + ) .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api/docs', app, document); diff --git a/star-tune/src/middleware/request-logger.middleware.ts b/star-tune/src/middleware/request-logger.middleware.ts index 916829c..e32287e 100644 --- a/star-tune/src/middleware/request-logger.middleware.ts +++ b/star-tune/src/middleware/request-logger.middleware.ts @@ -28,8 +28,8 @@ export class RequestLoggerMiddleware implements NestMiddleware { // 只在开发环境下记录请求日志 if (environment === 'development') { this.use = this.useBack; - for (let i of Object.keys(allColors.text)) { - for (let j of Object.keys(allColors.bg)) { + for (const i of Object.keys(allColors.text)) { + for (const j of Object.keys(allColors.bg)) { this.color.push(`${allColors.text[i]}${allColors.bg[j]}`); } } diff --git a/star-tune/src/module/user/user.controller.ts b/star-tune/src/module/user/user.controller.ts index d507939..64cc1e1 100644 --- a/star-tune/src/module/user/user.controller.ts +++ b/star-tune/src/module/user/user.controller.ts @@ -28,6 +28,7 @@ import { } from '@nestjs/swagger'; @ApiTags('用户管理') +@ApiBearerAuth('access-token') @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} @@ -88,10 +89,12 @@ export class UserController { return this.userService.register(dto); } - // @Post('login') - // login(@Body() dto: LoginDto) { - // return this.userService.login(dto); - // } + @ApiOperation({ summary: '用户登录' }) + @ApiResponse({ status: 201, description: '登录成功并返回用户信息' }) + @Post('login') + login(@Body() dto: LoginDto) { + return this.userService.login(dto); + } // @Post('email-login') // emailLogin(@Body() dto: EmailLoginDto) { @@ -112,11 +115,14 @@ export class UserController { // return this.userService.logout(userId); // } - // @Get('info') - // @UseGuards(AuthGuard) - // getUserInfo(@User('userId') userId: number) { - // return this.userService.getUserInfo(userId); - // } + @ApiOperation({ summary: '获取用户信息' }) + @ApiResponse({ status: 200, description: '获取成功,返回用户信息' }) + @ApiBearerAuth('access-token') + @Get('info') + @UseGuards(AuthGuard) + getUserInfo(@User('userId') userId: number) { + return this.userService.getUserInfo(userId); + } // @Patch() // @UseGuards(AuthGuard) diff --git a/star-tune/src/module/user/user.service.ts b/star-tune/src/module/user/user.service.ts index cec0746..ac99cbe 100644 --- a/star-tune/src/module/user/user.service.ts +++ b/star-tune/src/module/user/user.service.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { RedisService } from '@/service/redis/redis.service'; import { ConfigService } from '@nestjs/config'; import { UtilsService } from '@/common/utils/utils.service'; @@ -24,6 +28,7 @@ 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 { @@ -163,50 +168,66 @@ export class UserService { time: dayjs().format('YYYY-MM-DD HH:mm:ss'), }; await this.redis.del(codeKey); - const accessToken = this.utils.generateAccessToken(userGuard); - const refreshToken = this.utils.generateRefreshToken(userGuard); - - return { accessToken, refreshToken }; + return this.generateTokens(userGuard); } finally { await this.redis.unlock(`register:${dto.email}`); } } - // // 普通登录 - // async login(dto: LoginDto) { - // const result = await this.database.db - // .select() - // .from(user) - // .where( - // and( - // dto.email - // ? eq(user.email, dto.email) - // : eq(user.username, dto.username), - // eq(user.isDeleted, 0), - // ), - // ) - // .execute(); + // 普通登录 + async login(dto: LoginDto) { + // 判断用户是否存在 + const result = await this.database.db + .select({ + userId: user.userId, + username: user.username, + email: user.email, + }) + .from(user) + .where( + and( + dto.email + ? eq(user.email, dto.email) + : eq(user.username, dto.username!), + eq(user.isDeleted, 0), + ), + ) + .execute(); - // if (result.length === 0) { - // throw new UnauthorizedException('用户不存在'); - // } + 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.password, + passwordData[0].passwordHash, + )) + ) { + throw new UnauthorizedException('邮箱、用户名或密码不正确'); + } - // const userData = result[0]; - // const passwordData = await this.database.db - // .select() - // .from(userPassword) - // .where(eq(userPassword.userId, userData.userId)) - // .execute(); + // 返回token + const userGuard: UserGuard = { + userId: userData.userId, + username: userData.username, + email: userData.email, + time: dayjs().format('YYYY-MM-DD HH:mm:ss'), + }; - // if ( - // !passwordData.length || - // !(await bcrypt.compare(dto.password, passwordData[0].passwordHash)) - // ) { - // throw new UnauthorizedException('密码错误'); - // } - - // return this.generateTokens(userData.userId); - // } + return { + ...this.generateTokens(userGuard), + userInfo: await this.getUserInfo(userData.userId), + }; + } // // 邮箱登录 // async emailLogin(dto: EmailLoginDto) { @@ -231,22 +252,10 @@ export class UserService { // return this.generateTokens(result[0].userId); // } - 生成令牌; - private async generateTokens(userGuard: UserGuard) { + // 生成令牌 + private generateTokens(userGuard: UserGuard) { const accessToken = this.utils.generateAccessToken(userGuard); const refreshToken = this.utils.generateRefreshToken(userGuard); - - await this.redis.set( - `access_token:${userGuard.userId}`, - accessToken, - 600, - ); - await this.redis.set( - `refresh_token:${userGuard.userId}`, - refreshToken, - 1209600, - ); - return { accessToken, refreshToken }; } @@ -281,21 +290,21 @@ export class UserService { // return { message: '退出成功' }; // } - // // 获取用户信息 - // async getUserInfo(userId: number) { - // const result = await this.database.db - // .select() - // .from(user) - // .where(and(eq(user.userId, userId), eq(user.isDeleted, 0))) - // .execute(); + // 获取用户信息 + async getUserInfo(userId: number) { + const result = await this.database.db + .select() + .from(user) + .where(and(eq(user.userId, userId), eq(user.isDeleted, 0))) + .execute(); - // if (result.length === 0) { - // throw new UnauthorizedException('用户不存在'); - // } + if (result.length === 0) { + throw new UnauthorizedException('用户不存在'); + } - // const { passwordHash, ...userData } = result[0]; - // return userData; - // } + const { ...userData } = result[0]; + return userData; + } // // 更新用户信息 // async updateUser(userId: number, dto: UpdateUserDto) { diff --git a/star-tune/src/type/userGuard.ts b/star-tune/src/type/userGuard.ts index ff7763f..3a28201 100644 --- a/star-tune/src/type/userGuard.ts +++ b/star-tune/src/type/userGuard.ts @@ -1,6 +1,12 @@ +export enum UserGuardType { + ACCESS = 'ACCESS', + REFRESH = 'REFRESH', +} + export type UserGuard = { userId: number; username: string; email: string; time: string; + type?: UserGuardType; };