feat(api): 开发auth、login等接口,规范格式化
This commit is contained in:
+144
-5
@@ -1,12 +1,151 @@
|
||||
import 'reflect-metadata';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import helmet from 'helmet';
|
||||
|
||||
import { AppModule } from './app.module.js';
|
||||
import { ApiErrorFilter } from './common/api-error.filter.js';
|
||||
import { APP_CONFIG, type AppConfig } from './config/app-config.module.js';
|
||||
import { AppLogger } from './logging/app-logger.js';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.enableCors({ origin: process.env.WEB_ORIGIN ?? 'http://localhost:8100' });
|
||||
await app.listen(Number(process.env.API_PORT ?? 8200));
|
||||
type RequestWithId = Request & {
|
||||
requestId?: string;
|
||||
};
|
||||
|
||||
function openSwaggerInDevelopment(config: AppConfig, logger: AppLogger): void {
|
||||
if (
|
||||
config.NODE_ENV !== 'development' ||
|
||||
!config.swaggerEnabled ||
|
||||
process.platform !== 'darwin'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const swaggerUrl = `http://localhost:${config.API_PORT}/api-docs`;
|
||||
// 只在本机开发时打开浏览器,生产环境和非 macOS 主机不执行外部进程。
|
||||
execFile('open', [swaggerUrl], (error) => {
|
||||
if (error) {
|
||||
logger.warn('swagger_auto_open_failed', {
|
||||
error: error.message,
|
||||
url: swaggerUrl,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
bodyParser: false,
|
||||
// Keep Nest's bootstrap errors visible until AppLogger is installed below.
|
||||
logger: ['error', 'warn', 'log'],
|
||||
});
|
||||
const config = app.get<AppConfig>(APP_CONFIG);
|
||||
const logger = new AppLogger(config.LOG_LEVEL);
|
||||
app.useLogger(logger);
|
||||
|
||||
if (config.trustedProxyCidrs.length > 0) {
|
||||
// Never trust forwarded headers unless the deployment names its proxy networks explicitly.
|
||||
app.getHttpAdapter()
|
||||
.getInstance()
|
||||
.set('trust proxy', config.trustedProxyCidrs);
|
||||
}
|
||||
|
||||
app.enableCors({
|
||||
origin: config.WEB_ORIGIN,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: [
|
||||
'Authorization',
|
||||
'Content-Type',
|
||||
'Idempotency-Key',
|
||||
'If-Match',
|
||||
'X-Atlas-Platform-Id',
|
||||
'X-Request-Id',
|
||||
],
|
||||
exposedHeaders: ['X-Request-Id'],
|
||||
});
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
app.useBodyParser('json', { limit: config.API_BODY_LIMIT_BYTES });
|
||||
app.useBodyParser('urlencoded', {
|
||||
limit: config.API_BODY_LIMIT_BYTES,
|
||||
extended: true,
|
||||
});
|
||||
app.use(
|
||||
(request: RequestWithId, response: Response, next: NextFunction) => {
|
||||
const typedRequest = request;
|
||||
const typedResponse = response;
|
||||
const suppliedId = typedRequest.headers['x-request-id'];
|
||||
const requestId =
|
||||
typeof suppliedId === 'string' &&
|
||||
/^[A-Za-z0-9_-]{8,128}$/.test(suppliedId)
|
||||
? suppliedId
|
||||
: `req_${randomUUID()}`;
|
||||
const startedAt = performance.now();
|
||||
typedRequest.requestId = requestId;
|
||||
typedResponse.setHeader('X-Request-Id', requestId);
|
||||
typedResponse.on('finish', () => {
|
||||
logger.log('http_request_completed', {
|
||||
request_id: requestId,
|
||||
method: typedRequest.method,
|
||||
path: typedRequest.originalUrl,
|
||||
status_code: typedResponse.statusCode,
|
||||
duration_ms: Math.round(performance.now() - startedAt),
|
||||
});
|
||||
});
|
||||
next();
|
||||
},
|
||||
);
|
||||
app.useGlobalFilters(new ApiErrorFilter(logger));
|
||||
app.setGlobalPrefix('api', { exclude: ['health', 'ready'] });
|
||||
app.enableShutdownHooks();
|
||||
|
||||
if (config.swaggerEnabled) {
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Atlas Seller Kit API')
|
||||
.setDescription('Atlas 卖家工作台 HTTP API 文档')
|
||||
.setVersion('0.1.0')
|
||||
.addBearerAuth(
|
||||
{ type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
|
||||
'access-token',
|
||||
)
|
||||
.build();
|
||||
const swaggerDocument = SwaggerModule.createDocument(
|
||||
app,
|
||||
swaggerConfig,
|
||||
);
|
||||
SwaggerModule.setup('api-docs', app, swaggerDocument, {
|
||||
swaggerOptions: { persistAuthorization: true },
|
||||
});
|
||||
}
|
||||
|
||||
await app.listen(config.API_PORT);
|
||||
logger.log('api_started', {
|
||||
port: config.API_PORT,
|
||||
swagger_enabled: config.swaggerEnabled,
|
||||
});
|
||||
openSwaggerInDevelopment(config, logger);
|
||||
}
|
||||
|
||||
void bootstrap().catch((error: unknown) => {
|
||||
const details =
|
||||
error instanceof Error
|
||||
? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error;
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'error',
|
||||
message: 'api_startup_failed',
|
||||
error: details,
|
||||
}),
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user