feat(25-06-03): 用户登录和接口认证
This commit is contained in:
parent
804d14d8fd
commit
ab7badc4ae
@ -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',
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user