feat(25-06-03): 用户登录和接口认证
This commit is contained in:
parent
804d14d8fd
commit
ab7badc4ae
@ -32,7 +32,7 @@ export default () => {
|
||||
'/user/login',
|
||||
'/user/register',
|
||||
'/user/refreshToken',
|
||||
'/',
|
||||
// '/',
|
||||
// '/module',
|
||||
'/docs*',
|
||||
'/docs/json',
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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]}`);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -1,6 +1,12 @@
|
||||
export enum UserGuardType {
|
||||
ACCESS = 'ACCESS',
|
||||
REFRESH = 'REFRESH',
|
||||
}
|
||||
|
||||
export type UserGuard = {
|
||||
userId: number;
|
||||
username: string;
|
||||
email: string;
|
||||
time: string;
|
||||
type?: UserGuardType;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user