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/register',
'/user/refreshToken',
'/',
// '/',
// '/module',
'/docs*',
'/docs/json',

View File

@ -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<StringValue>('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<StringValue>('jwt.refreshExpiresIn') ||
'14d';
userGuard.type = UserGuardType.REFRESH;
return sign(userGuard, secret, { expiresIn });
}

View File

@ -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<FastifyRequest>();
const path = request.url;
console.log(path);
// 检查白名单路由
const whitelist = this.config.get<string[]>('jwt.whitelist') || [];
console.log(whitelist);
if (whitelist.some((route) => path.startsWith(route))) {
return true;
}
console.log('?', request.headers);
// 获取并验证 token
const authHeader = request.headers.authorization;

View File

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

View File

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

View File

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

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 { 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) {

View File

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