feat(25-06-03): 用户登录和接口认证

This commit is contained in:
nie 2025-06-03 22:40:24 +08:00
parent 804d14d8fd
commit ab7badc4ae
8 changed files with 151 additions and 83 deletions

View File

@ -32,7 +32,7 @@ export default () => {
'/user/login', '/user/login',
'/user/register', '/user/register',
'/user/refreshToken', '/user/refreshToken',
'/', // '/',
// '/module', // '/module',
'/docs*', '/docs*',
'/docs/json', '/docs/json',

View File

@ -4,7 +4,7 @@ import * as crypto from 'crypto';
import { CustomLogger } from '@/common/logger/logger.service'; import { CustomLogger } from '@/common/logger/logger.service';
import { sign, verify } from 'jsonwebtoken'; import { sign, verify } from 'jsonwebtoken';
import { type StringValue } from 'ms'; import { type StringValue } from 'ms';
import { UserGuard } from '@/type/userGuard'; import { UserGuard, UserGuardType } from '@/type/userGuard';
export type SendEmailOptions = { export type SendEmailOptions = {
to: string; to: string;
@ -97,8 +97,42 @@ export class UtilsService {
// 过期时间 // 过期时间
const expiresIn = const expiresIn =
this.configService.get<StringValue>('jwt.accessExpiresIn') || '20m'; this.configService.get<StringValue>('jwt.accessExpiresIn') || '20m';
userGuard.type = UserGuardType.ACCESS;
return sign(userGuard, secret, { expiresIn }); 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 { generateRefreshToken(userGuard: UserGuard): string {
const secret = const secret =
@ -106,6 +140,7 @@ export class UtilsService {
const expiresIn = const expiresIn =
this.configService.get<StringValue>('jwt.refreshExpiresIn') || this.configService.get<StringValue>('jwt.refreshExpiresIn') ||
'14d'; '14d';
userGuard.type = UserGuardType.REFRESH;
return sign(userGuard, secret, { expiresIn }); return sign(userGuard, secret, { expiresIn });
} }

View File

@ -5,7 +5,6 @@ import {
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { RedisService } from '@/service/redis/redis.service';
import { UtilsService } from '@/common/utils/utils.service'; import { UtilsService } from '@/common/utils/utils.service';
import { UserGuard } from '@/type/userGuard'; import { UserGuard } from '@/type/userGuard';
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
@ -21,19 +20,21 @@ declare module 'fastify' {
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor( constructor(
private readonly config: ConfigService, private readonly config: ConfigService,
private readonly redis: RedisService,
private readonly utils: UtilsService, private readonly utils: UtilsService,
) {} ) {}
canActivate(context: ExecutionContext): boolean { canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<FastifyRequest>(); const request = context.switchToHttp().getRequest<FastifyRequest>();
const path = request.url; const path = request.url;
console.log(path);
// 检查白名单路由 // 检查白名单路由
const whitelist = this.config.get<string[]>('jwt.whitelist') || []; const whitelist = this.config.get<string[]>('jwt.whitelist') || [];
console.log(whitelist);
if (whitelist.some((route) => path.startsWith(route))) { if (whitelist.some((route) => path.startsWith(route))) {
return true; return true;
} }
console.log('?', request.headers);
// 获取并验证 token // 获取并验证 token
const authHeader = request.headers.authorization; const authHeader = request.headers.authorization;

View File

@ -50,10 +50,21 @@ async function bootstrap() {
// 配置 Swagger // 配置 Swagger
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Star Tune API') .setTitle('Star Tune API') // 设置API文档标题
.setDescription('Star Tune 项目的 API 文档') .setDescription('Star Tune 项目的 API 文档') // 设置API文档描述
.setVersion('1.0') .setVersion('1.0') // 设置API版本号
.addBearerAuth() .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(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document); SwaggerModule.setup('api/docs', app, document);

View File

@ -28,8 +28,8 @@ export class RequestLoggerMiddleware implements NestMiddleware {
// 只在开发环境下记录请求日志 // 只在开发环境下记录请求日志
if (environment === 'development') { if (environment === 'development') {
this.use = this.useBack; this.use = this.useBack;
for (let i of Object.keys(allColors.text)) { for (const i of Object.keys(allColors.text)) {
for (let j of Object.keys(allColors.bg)) { for (const j of Object.keys(allColors.bg)) {
this.color.push(`${allColors.text[i]}${allColors.bg[j]}`); this.color.push(`${allColors.text[i]}${allColors.bg[j]}`);
} }
} }

View File

@ -28,6 +28,7 @@ import {
} from '@nestjs/swagger'; } from '@nestjs/swagger';
@ApiTags('用户管理') @ApiTags('用户管理')
@ApiBearerAuth('access-token')
@Controller('user') @Controller('user')
export class UserController { export class UserController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}
@ -88,10 +89,12 @@ export class UserController {
return this.userService.register(dto); return this.userService.register(dto);
} }
// @Post('login') @ApiOperation({ summary: '用户登录' })
// login(@Body() dto: LoginDto) { @ApiResponse({ status: 201, description: '登录成功并返回用户信息' })
// return this.userService.login(dto); @Post('login')
// } login(@Body() dto: LoginDto) {
return this.userService.login(dto);
}
// @Post('email-login') // @Post('email-login')
// emailLogin(@Body() dto: EmailLoginDto) { // emailLogin(@Body() dto: EmailLoginDto) {
@ -112,11 +115,14 @@ export class UserController {
// return this.userService.logout(userId); // return this.userService.logout(userId);
// } // }
// @Get('info') @ApiOperation({ summary: '获取用户信息' })
// @UseGuards(AuthGuard) @ApiResponse({ status: 200, description: '获取成功,返回用户信息' })
// getUserInfo(@User('userId') userId: number) { @ApiBearerAuth('access-token')
// return this.userService.getUserInfo(userId); @Get('info')
// } @UseGuards(AuthGuard)
getUserInfo(@User('userId') userId: number) {
return this.userService.getUserInfo(userId);
}
// @Patch() // @Patch()
// @UseGuards(AuthGuard) // @UseGuards(AuthGuard)

View File

@ -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 { RedisService } from '@/service/redis/redis.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { UtilsService } from '@/common/utils/utils.service'; import { UtilsService } from '@/common/utils/utils.service';
@ -24,6 +28,7 @@ import { CustomLogger } from '@/common/logger/logger.service';
import { EmailCodeType } from '@/type/enum'; import { EmailCodeType } from '@/type/enum';
import * as dayjs from 'dayjs'; import * as dayjs from 'dayjs';
import { UserGuard } from '@/type/userGuard'; import { UserGuard } from '@/type/userGuard';
import { SQL } from 'drizzle-orm';
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -163,50 +168,66 @@ export class UserService {
time: dayjs().format('YYYY-MM-DD HH:mm:ss'), time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
}; };
await this.redis.del(codeKey); await this.redis.del(codeKey);
const accessToken = this.utils.generateAccessToken(userGuard); return this.generateTokens(userGuard);
const refreshToken = this.utils.generateRefreshToken(userGuard);
return { accessToken, refreshToken };
} finally { } finally {
await this.redis.unlock(`register:${dto.email}`); await this.redis.unlock(`register:${dto.email}`);
} }
} }
// // 普通登录 // 普通登录
// async login(dto: LoginDto) { async login(dto: LoginDto) {
// const result = await this.database.db // 判断用户是否存在
// .select() const result = await this.database.db
// .from(user) .select({
// .where( userId: user.userId,
// and( username: user.username,
// dto.email email: user.email,
// ? eq(user.email, dto.email) })
// : eq(user.username, dto.username), .from(user)
// eq(user.isDeleted, 0), .where(
// ), and(
// ) dto.email
// .execute(); ? eq(user.email, dto.email)
: eq(user.username, dto.username!),
eq(user.isDeleted, 0),
),
)
.execute();
// if (result.length === 0) { if (result.length === 0) {
// throw new UnauthorizedException('用户不存在'); 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]; // 返回token
// const passwordData = await this.database.db const userGuard: UserGuard = {
// .select() userId: userData.userId,
// .from(userPassword) username: userData.username,
// .where(eq(userPassword.userId, userData.userId)) email: userData.email,
// .execute(); time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
};
// if ( return {
// !passwordData.length || ...this.generateTokens(userGuard),
// !(await bcrypt.compare(dto.password, passwordData[0].passwordHash)) userInfo: await this.getUserInfo(userData.userId),
// ) { };
// throw new UnauthorizedException('密码错误'); }
// }
// return this.generateTokens(userData.userId);
// }
// // 邮箱登录 // // 邮箱登录
// async emailLogin(dto: EmailLoginDto) { // async emailLogin(dto: EmailLoginDto) {
@ -231,22 +252,10 @@ export class UserService {
// return this.generateTokens(result[0].userId); // return this.generateTokens(result[0].userId);
// } // }
; // 生成令牌
private async generateTokens(userGuard: UserGuard) { private generateTokens(userGuard: UserGuard) {
const accessToken = this.utils.generateAccessToken(userGuard); const accessToken = this.utils.generateAccessToken(userGuard);
const refreshToken = this.utils.generateRefreshToken(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 }; return { accessToken, refreshToken };
} }
@ -281,21 +290,21 @@ export class UserService {
// return { message: '退出成功' }; // return { message: '退出成功' };
// } // }
// // 获取用户信息 // 获取用户信息
// async getUserInfo(userId: number) { async getUserInfo(userId: number) {
// const result = await this.database.db const result = await this.database.db
// .select() .select()
// .from(user) .from(user)
// .where(and(eq(user.userId, userId), eq(user.isDeleted, 0))) .where(and(eq(user.userId, userId), eq(user.isDeleted, 0)))
// .execute(); .execute();
// if (result.length === 0) { if (result.length === 0) {
// throw new UnauthorizedException('用户不存在'); throw new UnauthorizedException('用户不存在');
// } }
// const { passwordHash, ...userData } = result[0]; const { ...userData } = result[0];
// return userData; return userData;
// } }
// // 更新用户信息 // // 更新用户信息
// async updateUser(userId: number, dto: UpdateUserDto) { // async updateUser(userId: number, dto: UpdateUserDto) {

View File

@ -1,6 +1,12 @@
export enum UserGuardType {
ACCESS = 'ACCESS',
REFRESH = 'REFRESH',
}
export type UserGuard = { export type UserGuard = {
userId: number; userId: number;
username: string; username: string;
email: string; email: string;
time: string; time: string;
type?: UserGuardType;
}; };