import 'reflect-metadata'; import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { Logger } from 'nestjs-pino'; import multipart from '@fastify/multipart'; import { AppModule } from './app.module'; import fastifyStatic from '@fastify/static'; import * as path from 'path'; // @ts-expect-error: allow JSON.stringify(BigInt) BigInt.prototype.toJSON = function () { const value = this as bigint; if (value > BigInt(Number.MAX_SAFE_INTEGER)) { return this.toString(); } return Number(this); }; async function bootstrap() { const fastifyAdapter = new FastifyAdapter() as FastifyAdapter; // multipart for file upload (e.g. OSS, S3) await fastifyAdapter.register(multipart as any, { limits: { fileSize: 30 * 1024 * 1024, // 30 MB }, }); // after creating fastifyAdapter but before NestFactory.create: // Read allowed origin from env (fallback to '*') const corsOrg = process.env.APP_CORS_ORIGIN || '*'; // await fastifyAdapter.register(fastifyStatic as any, { // root: path.resolve(process.env.IMAGE_ROOT_PATH || '/data/box-images'), // prefix: '/images/', // setHeaders: (res: any, pathName: any, stat: any) => { // // CORS // res.setHeader('Access-Control-Allow-Origin', corsOrg); // res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); // res.setHeader( // 'Access-Control-Allow-Headers', // 'Origin, X-Requested-With, Content-Type, Accept, Authorization', // ); // }, // }); const app = await NestFactory.create( AppModule, fastifyAdapter, { bufferLogs: true, snapshot: true, }, ); // nestjs-pino logger app.useLogger(app.get(Logger)); // Global API prefix; mgnt routes stay under /api/v1/mgnt/* app.setGlobalPrefix('api/v1'); app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: false, }), ); const configService = app.get(ConfigService); const host = configService.get('MGNT_HOST') ?? configService.get('MGNT_HOST') ?? '0.0.0.0'; const port = configService.get('MGNT_PORT') ?? Number(process.env.PORT ?? 3300); const corsOrigin = configService.get('MGNT_CORS_ORIGIN') ?? '*'; const corsOriginOption = corsOrigin === '*' ? true : corsOrigin .split(',') .map((o) => o.trim()) .filter(Boolean); app.enableCors({ origin: corsOriginOption, methods: 'GET,PUT,PATCH,POST,DELETE', credentials: true, }); if (process.env.NODE_ENV !== 'production') { const swaggerConfig = new DocumentBuilder() .setTitle('盒子管理系统管理后台 API 文档') .setDescription('盒子管理系统管理后台接口 (api/v1/mgnt/*)') .setVersion('1.0') .addBearerAuth() .addServer(`http://${host}:${port}`, '本地开发环境') .addTag('系统 - 授权', '管理后台授权相关接口 (mgnt/auth)') .addTag('系统 - 用户', '管理后台用户管理接口 (mgnt/users)') .addTag('系统 - 角色', '管理后台角色管理接口 (mgnt/roles)') .addTag('系统 - 菜单', '管理后台菜单管理接口 (mgnt/menus)') .build(); const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig); SwaggerModule.setup('api-docs', app, swaggerDocument, { jsonDocumentUrl: '/api-docs-json', swaggerOptions: { docExpansion: 'none', tagsSorter: 'alpha', operationsSorter: 'alpha', }, }); // Prevent Swagger UI/JSON from being cached (browser + proxies) const fastify = app.getHttpAdapter().getInstance(); fastify.addHook('onSend', (request, reply, payload, done) => { if ( request.url?.startsWith('/api-docs') || request.url === '/api-docs-json' ) { reply.header( 'Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate', ); reply.header('Pragma', 'no-cache'); reply.header('Expires', '0'); } done(null, payload); }); } await app.listen(port, host); const url = `http://${host}:${port}`; console.log(`🚀 box-mgnt-api is running at: ${url}`); console.log(`📚 API Documentation: ${url}/api-docs`); console.log(`📄 Swagger JSON: ${url}/api-docs-json`); console.log(`🔧 Management APIs: ${url}/api/v1/mgnt/*`); } bootstrap();