feat: 重构业务模块架构

- 新增auth模块处理认证逻辑
- 新增security模块处理安全相关功能
- 新增user-mgmt模块管理用户相关操作
- 新增shared模块存放共享组件
- 重构admin模块,添加DTO和Guards
- 为admin模块添加测试文件结构
This commit is contained in:
moyin
2025-12-24 18:04:30 +08:00
parent 85d488a508
commit 47a738067a
35 changed files with 3667 additions and 227 deletions

View File

@@ -0,0 +1,224 @@
/**
* 内容类型检查中间件
*
* 功能描述:
* - 检查POST/PUT请求的Content-Type头
* - 确保API接口接收正确的数据格式
* - 提供友好的错误提示信息
*
* 使用场景:
* - API接口数据格式验证
* - 防止错误的请求格式
* - 提升API接口的健壮性
*
* @author kiro-ai
* @version 1.0.0
* @since 2025-12-24
*/
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { Logger } from '@nestjs/common';
/**
* 不支持的媒体类型响应接口
*/
interface UnsupportedMediaTypeResponse {
/** 请求是否成功 */
success: boolean;
/** 响应消息 */
message: string;
/** 错误代码 */
error_code: string;
/** 支持的媒体类型 */
supported_types: string[];
/** 接收到的媒体类型 */
received_type?: string;
}
@Injectable()
export class ContentTypeMiddleware implements NestMiddleware {
private readonly logger = new Logger(ContentTypeMiddleware.name);
/**
* 需要检查Content-Type的HTTP方法
*/
private readonly methodsToCheck = ['POST', 'PUT', 'PATCH'];
/**
* 支持的Content-Type列表
*/
private readonly supportedTypes = [
'application/json',
'application/json; charset=utf-8'
];
/**
* 不需要检查Content-Type的路径正则表达式
*/
private readonly excludePaths = [
/^\/api-docs/, // Swagger文档
/^\/health/, // 健康检查
/^\/admin\/logs\/archive/, // 文件下载
/\/upload/, // 文件上传
];
/**
* 中间件处理函数
*
* 业务逻辑:
* 1. 检查是否需要验证Content-Type
* 2. 获取请求的Content-Type头
* 3. 验证Content-Type是否支持
* 4. 记录不支持的请求类型
*
* @param req HTTP请求对象
* @param res HTTP响应对象
* @param next 下一个中间件函数
*/
use(req: Request, res: Response, next: NextFunction) {
// 1. 检查是否需要验证Content-Type
if (!this.shouldCheckContentType(req)) {
next();
return;
}
// 2. 获取请求的Content-Type
const contentType = req.get('Content-Type');
// 3. 检查Content-Type是否存在
if (!contentType) {
this.logger.warn('请求缺少Content-Type头', {
operation: 'content_type_check',
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
timestamp: new Date().toISOString()
});
const response: UnsupportedMediaTypeResponse = {
success: false,
message: '请求缺少Content-Type头请设置为application/json',
error_code: 'MISSING_CONTENT_TYPE',
supported_types: this.supportedTypes,
received_type: undefined
};
res.status(415).json(response);
return;
}
// 4. 验证Content-Type是否支持
const normalizedContentType = this.normalizeContentType(contentType);
if (!this.isSupportedContentType(normalizedContentType)) {
this.logger.warn('不支持的Content-Type', {
operation: 'content_type_check',
method: req.method,
url: req.url,
contentType: contentType,
normalizedContentType: normalizedContentType,
userAgent: req.get('User-Agent'),
ip: req.ip,
timestamp: new Date().toISOString()
});
const response: UnsupportedMediaTypeResponse = {
success: false,
message: `不支持的Content-Type: ${contentType}请使用application/json`,
error_code: 'UNSUPPORTED_MEDIA_TYPE',
supported_types: this.supportedTypes,
received_type: contentType
};
res.status(415).json(response);
return;
}
// 5. Content-Type验证通过继续处理
next();
}
/**
* 检查是否需要验证Content-Type
*
* @param req HTTP请求对象
* @returns 是否需要验证
*/
private shouldCheckContentType(req: Request): boolean {
// 1. 检查HTTP方法
if (!this.methodsToCheck.includes(req.method)) {
return false;
}
// 2. 检查是否在排除路径中
const url = req.url;
for (const excludePattern of this.excludePaths) {
if (excludePattern.test(url)) {
return false;
}
}
// 3. 检查Content-Length如果为0则不需要验证
const contentLength = req.get('Content-Length');
if (contentLength === '0') {
return false;
}
return true;
}
/**
* 标准化Content-Type
*
* @param contentType 原始Content-Type
* @returns 标准化后的Content-Type
*/
private normalizeContentType(contentType: string): string {
// 移除空格并转换为小写
return contentType.toLowerCase().trim();
}
/**
* 检查Content-Type是否支持
*
* @param contentType 标准化的Content-Type
* @returns 是否支持
*/
private isSupportedContentType(contentType: string): boolean {
// 检查是否以支持的类型开头
return this.supportedTypes.some(supportedType =>
contentType.startsWith(supportedType.toLowerCase())
);
}
/**
* 获取支持的Content-Type列表
*
* @returns 支持的类型列表
*/
getSupportedTypes(): string[] {
return [...this.supportedTypes];
}
/**
* 添加支持的Content-Type
*
* @param contentType 要添加的Content-Type
*/
addSupportedType(contentType: string): void {
if (!this.supportedTypes.includes(contentType)) {
this.supportedTypes.push(contentType);
}
}
/**
* 添加排除路径
*
* @param pattern 路径正则表达式
*/
addExcludePath(pattern: RegExp): void {
this.excludePaths.push(pattern);
}
}

View File

@@ -0,0 +1,137 @@
/**
* 维护模式中间件
*
* 功能描述:
* - 检查系统是否处于维护模式
* - 在维护期间阻止用户访问API
* - 提供维护状态和预计恢复时间信息
*
* 使用场景:
* - 系统升级维护
* - 数据库迁移
* - 紧急故障修复
* - 定期维护窗口
*
* @author kiro-ai
* @version 1.0.0
* @since 2025-12-24
*/
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config';
import { Logger } from '@nestjs/common';
/**
* 维护模式响应接口
*/
interface MaintenanceResponse {
/** 请求是否成功 */
success: boolean;
/** 响应消息 */
message: string;
/** 错误代码 */
error_code: string;
/** 维护信息 */
maintenance_info?: {
/** 维护开始时间 */
start_time: string;
/** 预计结束时间 */
estimated_end_time?: string;
/** 重试间隔(秒) */
retry_after: number;
/** 维护原因 */
reason?: string;
};
}
@Injectable()
export class MaintenanceMiddleware implements NestMiddleware {
private readonly logger = new Logger(MaintenanceMiddleware.name);
constructor(private readonly configService: ConfigService) {}
/**
* 中间件处理函数
*
* 业务逻辑:
* 1. 检查维护模式环境变量
* 2. 如果处于维护模式返回503状态码
* 3. 提供维护信息和重试建议
* 4. 记录维护期间的访问尝试
*
* @param req HTTP请求对象
* @param res HTTP响应对象
* @param next 下一个中间件函数
*/
use(req: Request, res: Response, next: NextFunction) {
// 1. 检查维护模式状态
const isMaintenanceMode = this.configService.get<string>('MAINTENANCE_MODE') === 'true';
if (!isMaintenanceMode) {
// 非维护模式,继续处理请求
next();
return;
}
// 2. 记录维护期间的访问尝试
this.logger.warn('维护模式:拒绝访问请求', {
operation: 'maintenance_check',
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
timestamp: new Date().toISOString()
});
// 3. 获取维护配置信息
const maintenanceStartTime = this.configService.get<string>('MAINTENANCE_START_TIME') || new Date().toISOString();
const maintenanceEndTime = this.configService.get<string>('MAINTENANCE_END_TIME');
const maintenanceReason = this.configService.get<string>('MAINTENANCE_REASON') || '系统维护升级';
const retryAfter = this.configService.get<number>('MAINTENANCE_RETRY_AFTER') || 1800; // 默认30分钟
// 4. 构建维护模式响应
const maintenanceResponse: MaintenanceResponse = {
success: false,
message: '系统正在维护中,请稍后再试',
error_code: 'SERVICE_UNAVAILABLE',
maintenance_info: {
start_time: maintenanceStartTime,
estimated_end_time: maintenanceEndTime,
retry_after: retryAfter,
reason: maintenanceReason
}
};
// 5. 设置HTTP响应头
res.setHeader('Retry-After', retryAfter.toString());
res.setHeader('Content-Type', 'application/json');
// 6. 返回503服务不可用状态
res.status(503).json(maintenanceResponse);
}
/**
* 检查维护模式是否启用
*
* @returns 是否处于维护模式
*/
isMaintenanceEnabled(): boolean {
return this.configService.get<string>('MAINTENANCE_MODE') === 'true';
}
/**
* 获取维护信息
*
* @returns 维护配置信息
*/
getMaintenanceInfo() {
return {
enabled: this.isMaintenanceEnabled(),
startTime: this.configService.get<string>('MAINTENANCE_START_TIME'),
endTime: this.configService.get<string>('MAINTENANCE_END_TIME'),
reason: this.configService.get<string>('MAINTENANCE_REASON'),
retryAfter: this.configService.get<number>('MAINTENANCE_RETRY_AFTER')
};
}
}