import { randomUUID } from 'node:crypto'; import { ValidationPipe } from '@nestjs/common'; import type { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import type { NextFunction, Response } from 'express'; import helmet from 'helmet'; import { API_PREFIX, type ServerConfig } from './config/server-config'; import { AppLogger } from './logging/app-logger'; import type { HttpRequest } from './common/http-request'; import { ApiErrorFilter } from './common/api-error.filter'; /** 在业务路由初始化前安装 HTTP 基础设施;生产和测试复用相同处理顺序。 */ export function configureApplication( app: NestExpressApplication, config: ServerConfig, logger: AppLogger, ): void { app.useLogger(logger); const express = app.getHttpAdapter().getInstance(); express.disable('x-powered-by'); // 仅信任部署明确声明的代理地址,默认不采信客户端伪造的转发头。 if (config.trustedProxyCidrs.length) express.set('trust proxy', config.trustedProxyCidrs); // 解析 body 前安装,格式错误和超限响应也携带 requestId。 app.use((request: HttpRequest, response: Response, next: NextFunction) => { const suppliedId = request.headers['x-request-id']; // 合法上游 ID 可以贯穿调用链;其余输入替换为服务端生成的标识。 const requestId = typeof suppliedId === 'string' && /^[A-Za-z0-9_-]{8,128}$/.test(suppliedId) ? suppliedId : `req_${randomUUID()}`; const startedAt = performance.now(); request.requestId = requestId; response.setHeader('X-Request-Id', requestId); // 登录和本人接口的成功/失败均禁止缓存,避免凭证或个人数据进入公共缓存。 if (/^\/api\/v1\/(auth|me)(?:\/|$)/.test(request.path)) response.setHeader('Cache-Control', 'no-store'); // 响应完成后记录状态与耗时;只记录 path,排除 URL 查询中的登录 code/token。 response.once('finish', () => logger.log('http_request_completed', { request_id: requestId, method: request.method, path: request.path, status_code: response.statusCode, duration_ms: Math.round(performance.now() - startedAt), }), ); next(); }); // 开启 Swagger 时兼容其页面脚本;关闭文档时恢复 Helmet 默认 CSP。 app.use( helmet(config.swaggerEnabled ? { contentSecurityPolicy: false } : {}), ); // 小程序/服务端请求通常不带 Origin;浏览器请求按精确白名单开放。 // 使用 Bearer token,不启用 Cookie 凭证;CORS 不代替服务端鉴权。 app.enableCors({ origin: (origin, callback) => callback(null, !origin || config.corsOrigins.includes(origin)), credentials: false, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Authorization', 'Content-Type', 'X-Request-Id'], exposedHeaders: ['X-Request-Id', 'Retry-After'], }); // 应用工厂已禁用 Nest 默认解析器,此处统一限制 JSON 和表单请求大小。 // 表单不解析嵌套对象,上传文件另由后续 assets 模块处理。 app.useBodyParser('json', { limit: config.bodyLimitBytes }); app.useBodyParser('urlencoded', { limit: config.bodyLimitBytes, extended: false, }); // DTO 拒绝未声明字段;不隐式转换类型,也不将原始参数值放进校验错误。 app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true, validationError: { target: false, value: false }, transformOptions: { enableImplicitConversion: false }, }), ); // 统一错误格式;成功响应继续由控制器遵循公共契约,避免重复包装。 app.useGlobalFilters(new ApiErrorFilter(logger)); app.setGlobalPrefix(API_PREFIX); app.enableShutdownHooks(); // 文档只包含当前实际注册的路由。它直接挂载在 Express 上,不经过 JWT 守卫。 if (config.swaggerEnabled) { const options = new DocumentBuilder() .setTitle('Doodle Store API') .setDescription( '统一业务 API;已接入微信静默登录、本人资料、账户鉴权与分类查询/管理', ) .setVersion('0.1.0') .addBearerAuth( { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, 'access-token', ) // 默认声明 Bearer 鉴权,公开操作在各自 Swagger 元数据中显式覆盖。 .addSecurityRequirements('access-token') .build(); SwaggerModule.setup( 'api-docs', app, SwaggerModule.createDocument(app, options), { // 刷新页面后不保留 token,避免共享浏览器残留授权凭证。 swaggerOptions: { persistAuthorization: false }, jsonDocumentUrl: 'api-docs-json', }, ); } }