feat: 完成 NestJS 后端核心底座开发 (M1-M6) 和 Ant Design Vue 前端迁移
主要更新: 1. 后端核心底座完成 (M1-M6): - 健康检查、指标监控、分布式锁 - 事件总线、队列系统、事务管理 - 安全守卫、多租户隔离、存储适配器 - 审计日志、配置管理、多语言支持 2. 前端迁移到 Ant Design Vue: - 从 Element Plus 迁移到 Ant Design Vue - 完善 system 模块 (role/menu/dept) - 修复依赖和配置问题 3. 文档完善: - AI 开发工作流文档 - 架构约束和开发规范 - 项目进度跟踪 4. 其他改进: - 修复编译错误和类型问题 - 完善测试用例 - 优化项目结构
This commit is contained in:
@@ -1,46 +1,30 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { Module, forwardRef, Global } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AuthToken } from './entities/AuthToken';
|
||||
import { AuthService } from './services/AuthService';
|
||||
import { AuthController } from './controllers/AuthController';
|
||||
import { JwtAuthGuard } from './guards/JwtAuthGuard';
|
||||
import { RolesGuard } from './guards/RolesGuard';
|
||||
import { JwtGlobalModule } from './jwt.module';
|
||||
|
||||
// 导入Admin和Member模块
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { MemberModule } from '../member/member.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule,
|
||||
TypeOrmModule.forFeature([AuthToken]),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET', 'change_me'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get('JWT_EXPIRES_IN', '7d'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
JwtGlobalModule,
|
||||
// 导入Admin和Member模块以使用其服务
|
||||
forwardRef(() => AdminModule),
|
||||
forwardRef(() => MemberModule),
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtAuthGuard,
|
||||
RolesGuard,
|
||||
],
|
||||
providers: [AuthService, JwtAuthGuard, RolesGuard],
|
||||
controllers: [AuthController],
|
||||
exports: [
|
||||
AuthService,
|
||||
JwtAuthGuard,
|
||||
RolesGuard,
|
||||
],
|
||||
exports: [AuthService, JwtAuthGuard, RolesGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Req,
|
||||
HttpCode,
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Req,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Get
|
||||
Get,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import type { Request } from 'express';
|
||||
import { AuthService } from '../services/AuthService';
|
||||
import { LoginDto, RefreshTokenDto, LogoutDto } from '../dto/AuthDto';
|
||||
@@ -25,13 +30,10 @@ export class AuthController {
|
||||
@ApiResponse({ status: 200, description: '登录成功' })
|
||||
@ApiResponse({ status: 401, description: '用户名或密码错误' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async adminLogin(
|
||||
@Body() loginDto: LoginDto,
|
||||
@Req() req: Request
|
||||
) {
|
||||
async adminLogin(@Body() loginDto: LoginDto, @Req() req: Request) {
|
||||
const ipAddress = req.ip || req.connection.remoteAddress || 'unknown';
|
||||
const userAgent = req.headers['user-agent'] || 'unknown';
|
||||
|
||||
|
||||
return await this.authService.adminLogin(loginDto, ipAddress, userAgent);
|
||||
}
|
||||
|
||||
@@ -40,13 +42,10 @@ export class AuthController {
|
||||
@ApiResponse({ status: 200, description: '登录成功' })
|
||||
@ApiResponse({ status: 401, description: '用户名或密码错误' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async memberLogin(
|
||||
@Body() loginDto: LoginDto,
|
||||
@Req() req: Request
|
||||
) {
|
||||
async memberLogin(@Body() loginDto: LoginDto, @Req() req: Request) {
|
||||
const ipAddress = req.ip || req.connection.remoteAddress || 'unknown';
|
||||
const userAgent = req.headers['user-agent'] || 'unknown';
|
||||
|
||||
|
||||
return await this.authService.memberLogin(loginDto, ipAddress, userAgent);
|
||||
}
|
||||
|
||||
@@ -112,4 +111,4 @@ export class AuthController {
|
||||
}
|
||||
return { message: '登出成功' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
|
||||
4
wwjcloud/src/common/auth/decorators/public.decorator.ts
Normal file
4
wwjcloud/src/common/auth/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNumber, IsOptional, MinLength, MaxLength } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ description: '用户名', example: 'admin' })
|
||||
@@ -21,13 +27,19 @@ export class LoginDto {
|
||||
}
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty({ description: '刷新Token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
|
||||
@ApiProperty({
|
||||
description: '刷新Token',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export class LogoutDto {
|
||||
@ApiProperty({ description: '访问Token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
|
||||
@ApiProperty({
|
||||
description: '访问Token',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
@IsString()
|
||||
token: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('auth_token')
|
||||
@Index(['token'], { unique: true })
|
||||
@@ -22,7 +29,12 @@ export class AuthToken {
|
||||
@Column({ name: 'expires_at', type: 'datetime' })
|
||||
expiresAt: Date;
|
||||
|
||||
@Column({ name: 'refresh_token', type: 'varchar', length: 500, nullable: true })
|
||||
@Column({
|
||||
name: 'refresh_token',
|
||||
type: 'varchar',
|
||||
length: 500,
|
||||
nullable: true,
|
||||
})
|
||||
refreshToken?: string;
|
||||
|
||||
@Column({ name: 'refresh_expires_at', type: 'datetime', nullable: true })
|
||||
@@ -43,7 +55,12 @@ export class AuthToken {
|
||||
@Column({ name: 'revoked_at', type: 'datetime', nullable: true })
|
||||
revokedAt?: Date;
|
||||
|
||||
@Column({ name: 'revoked_reason', type: 'varchar', length: 200, nullable: true })
|
||||
@Column({
|
||||
name: 'revoked_reason',
|
||||
type: 'varchar',
|
||||
length: 200,
|
||||
nullable: true,
|
||||
})
|
||||
revokedReason?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
@@ -56,10 +73,10 @@ export class AuthToken {
|
||||
getDeviceTypeText(): string {
|
||||
if (this.deviceType === undefined || this.deviceType === '') return '';
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'web': '网页',
|
||||
'mobile': '手机',
|
||||
'app': '应用',
|
||||
'wechat': '微信'
|
||||
web: '网页',
|
||||
mobile: '手机',
|
||||
app: '应用',
|
||||
wechat: '微信',
|
||||
};
|
||||
return typeMap[this.deviceType] || '未知';
|
||||
}
|
||||
@@ -80,4 +97,4 @@ export class AuthToken {
|
||||
isValid(): boolean {
|
||||
return !this.isRevoked && !this.isExpired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from './JwtAuthGuard';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalAuthGuard implements CanActivate {
|
||||
@@ -11,7 +12,7 @@ export class GlobalAuthGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 检查是否有 @Public() 装饰器
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
@@ -22,12 +23,12 @@ export class GlobalAuthGuard implements CanActivate {
|
||||
|
||||
// 对于需要认证的接口,使用 JWT 认证
|
||||
const result = this.jwtAuthGuard.canActivate(context);
|
||||
|
||||
|
||||
// 处理 Promise 类型
|
||||
if (result instanceof Promise) {
|
||||
return await result;
|
||||
}
|
||||
|
||||
|
||||
return result as boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { AuthService } from '../services/AuthService';
|
||||
@@ -13,7 +18,7 @@ export class JwtAuthGuard implements CanActivate {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('未提供访问令牌');
|
||||
}
|
||||
@@ -21,7 +26,7 @@ export class JwtAuthGuard implements CanActivate {
|
||||
try {
|
||||
// 验证Token
|
||||
const payload = await this.authService.validateToken(token);
|
||||
|
||||
|
||||
if (!payload) {
|
||||
throw new UnauthorizedException('访问令牌无效或已过期');
|
||||
}
|
||||
@@ -38,4 +43,4 @@ export class JwtAuthGuard implements CanActivate {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
|
||||
@@ -29,10 +34,10 @@ export class RolesGuard implements CanActivate {
|
||||
}
|
||||
|
||||
// 检查具体角色权限
|
||||
if (user.roles && requiredRoles.some(role => user.roles.includes(role))) {
|
||||
if (user.roles && requiredRoles.some((role) => user.roles.includes(role))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ForbiddenException('权限不足');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ export interface User {
|
||||
|
||||
export interface RequestWithUser extends Request {
|
||||
user: User;
|
||||
}
|
||||
}
|
||||
|
||||
21
wwjcloud/src/common/auth/jwt.module.ts
Normal file
21
wwjcloud/src/common/auth/jwt.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET', 'change_me'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get('JWT_EXPIRES_IN', '7d'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
exports: [JwtModule],
|
||||
})
|
||||
export class JwtGlobalModule {}
|
||||
@@ -30,7 +30,7 @@ export class AuthService {
|
||||
|
||||
// 调用AdminService验证用户名密码
|
||||
const adminUser = await this.validateAdminUser(username, password, siteId);
|
||||
|
||||
|
||||
if (!adminUser) {
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
@@ -53,8 +53,11 @@ export class AuthService {
|
||||
|
||||
// 计算过期时间
|
||||
const expiresIn = this.configService.get('JWT_EXPIRES_IN', '7d');
|
||||
const refreshExpiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d');
|
||||
|
||||
const refreshExpiresIn = this.configService.get(
|
||||
'JWT_REFRESH_EXPIRES_IN',
|
||||
'30d',
|
||||
);
|
||||
|
||||
const expiresAt = this.calculateExpiryDate(expiresIn);
|
||||
const refreshExpiresAt = this.calculateExpiryDate(refreshExpiresIn);
|
||||
|
||||
@@ -99,8 +102,12 @@ export class AuthService {
|
||||
const { username, password, siteId = 0 } = loginDto;
|
||||
|
||||
// 调用MemberService验证用户名密码
|
||||
const memberUser = await this.validateMemberUser(username, password, siteId);
|
||||
|
||||
const memberUser = await this.validateMemberUser(
|
||||
username,
|
||||
password,
|
||||
siteId,
|
||||
);
|
||||
|
||||
if (!memberUser) {
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
@@ -123,8 +130,11 @@ export class AuthService {
|
||||
|
||||
// 计算过期时间
|
||||
const expiresIn = this.configService.get('JWT_EXPIRES_IN', '7d');
|
||||
const refreshExpiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d');
|
||||
|
||||
const refreshExpiresIn = this.configService.get(
|
||||
'JWT_REFRESH_EXPIRES_IN',
|
||||
'30d',
|
||||
);
|
||||
|
||||
const expiresAt = this.calculateExpiryDate(expiresIn);
|
||||
const refreshExpiresAt = this.calculateExpiryDate(refreshExpiresIn);
|
||||
|
||||
@@ -175,7 +185,7 @@ export class AuthService {
|
||||
try {
|
||||
// 验证刷新Token
|
||||
const payload = this.jwtService.verify(refreshToken);
|
||||
|
||||
|
||||
// 检查数据库中的Token记录
|
||||
const tokenRecord = await this.authTokenRepository.findOne({
|
||||
where: { refreshToken, isRevoked: 0 },
|
||||
@@ -199,7 +209,9 @@ export class AuthService {
|
||||
|
||||
// 更新数据库中的Token
|
||||
tokenRecord.token = newAccessToken;
|
||||
tokenRecord.expiresAt = this.calculateExpiryDate(this.configService.get('JWT_EXPIRES_IN', '7d'));
|
||||
tokenRecord.expiresAt = this.calculateExpiryDate(
|
||||
this.configService.get('JWT_EXPIRES_IN', '7d'),
|
||||
);
|
||||
await this.authTokenRepository.save(tokenRecord);
|
||||
|
||||
return {
|
||||
@@ -239,7 +251,7 @@ export class AuthService {
|
||||
try {
|
||||
// 验证JWT Token
|
||||
const payload = this.jwtService.verify(token);
|
||||
|
||||
|
||||
// 检查数据库中的Token记录
|
||||
const tokenRecord = await this.authTokenRepository.findOne({
|
||||
where: { token, isRevoked: 0 },
|
||||
@@ -268,7 +280,12 @@ export class AuthService {
|
||||
/**
|
||||
* 撤销用户所有Token
|
||||
*/
|
||||
async revokeUserTokens(userId: number, userType: string, siteId: number = 0, reason: string = '管理员撤销') {
|
||||
async revokeUserTokens(
|
||||
userId: number,
|
||||
userType: string,
|
||||
siteId: number = 0,
|
||||
reason: string = '管理员撤销',
|
||||
) {
|
||||
const tokens = await this.authTokenRepository.find({
|
||||
where: { userId, userType, siteId, isRevoked: 0 },
|
||||
});
|
||||
@@ -344,7 +361,11 @@ export class AuthService {
|
||||
/**
|
||||
* 验证管理员用户
|
||||
*/
|
||||
private async validateAdminUser(username: string, password: string, siteId: number): Promise<any> {
|
||||
private async validateAdminUser(
|
||||
username: string,
|
||||
password: string,
|
||||
siteId: number,
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 根据用户名查找管理员
|
||||
const admin = await this.adminService.getAdminByUsername(username);
|
||||
@@ -353,7 +374,10 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await this.adminService.validatePassword(admin.uid, password);
|
||||
const isValidPassword = await this.adminService.validatePassword(
|
||||
admin.uid,
|
||||
password,
|
||||
);
|
||||
if (!isValidPassword) {
|
||||
return null;
|
||||
}
|
||||
@@ -372,11 +396,15 @@ export class AuthService {
|
||||
/**
|
||||
* 验证会员用户
|
||||
*/
|
||||
private async validateMemberUser(username: string, password: string, siteId: number): Promise<any> {
|
||||
private async validateMemberUser(
|
||||
username: string,
|
||||
password: string,
|
||||
siteId: number,
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 根据用户名查找会员
|
||||
let member = await this.memberService.findByUsername(username);
|
||||
|
||||
|
||||
// 如果用户名没找到,尝试用手机号或邮箱查找
|
||||
if (!member) {
|
||||
member = await this.memberService.findByMobile(username);
|
||||
@@ -405,4 +433,4 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user