docs: 基本完成初版数据库设计
This commit is contained in:
@@ -1,6 +1,107 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
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';
|
||||
|
||||
export function configureApplication(app: INestApplication): void {
|
||||
app.setGlobalPrefix('api/v1');
|
||||
/** 在业务路由初始化前安装 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);
|
||||
// 响应完成后记录状态与耗时;只记录 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'],
|
||||
});
|
||||
// 应用工厂已禁用 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',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user