123 lines
4.4 KiB
TypeScript
123 lines
4.4 KiB
TypeScript
import { NestFactory } from '@nestjs/core';
|
||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||
import { ValidationPipe } from '@nestjs/common';
|
||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||
import helmet from 'helmet';
|
||
import * as cookieParserModule from 'cookie-parser';
|
||
import { json } from 'express';
|
||
import { join } from 'path';
|
||
import { AppModule } from './app.module';
|
||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||
|
||
async function bootstrap() {
|
||
const app = await NestFactory.create<NestExpressApplication>(AppModule, { bodyParser: false });
|
||
|
||
// Replace Express's JSON parser with one that stringifies BigInt.
|
||
// Express's default `json()` throws "Do not know how to serialize a BigInt".
|
||
app.use(
|
||
json({
|
||
limit: '2mb',
|
||
reviver: (_key: string, value: unknown) => {
|
||
// If a numeric string would overflow the JS number range we keep
|
||
// it as a string; the JWT strategy / services expect bigints.
|
||
if (typeof value === 'string' && /^-?\d{16,}$/.test(value)) {
|
||
// Leave as string — services parse with BigInt().
|
||
return value;
|
||
}
|
||
return value;
|
||
},
|
||
}),
|
||
);
|
||
|
||
// Security headers (X-Content-Type-Options, X-Frame-Options, HSTS, ...)
|
||
// Static assets (/uploads, /assets) are embedded cross-origin by other
|
||
// sites, so CORP must allow cross-origin reads. CSP 去掉默认的
|
||
// upgrade-insecure-requests:Swagger UI 经纯 http 访问时该指令会把
|
||
// 自身子资源升级为 https,导致 ERR_SSL_PROTOCOL_ERROR。
|
||
app.use(
|
||
helmet({
|
||
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
||
contentSecurityPolicy: {
|
||
useDefaults: true,
|
||
directives: {
|
||
'upgrade-insecure-requests': null,
|
||
},
|
||
},
|
||
}),
|
||
);
|
||
|
||
// Parse auth cookies (HttpOnly access/refresh tokens). Resolve both the
|
||
// namespace and its `default` interop shape so it works regardless of
|
||
// the compiled module interop mode.
|
||
const cookieParser = (
|
||
cookieParserModule as unknown as {
|
||
default?: typeof cookieParserModule;
|
||
}
|
||
).default ?? cookieParserModule;
|
||
app.use(cookieParser());
|
||
|
||
// CORS: only origins listed in CORS_ORIGINS (comma-separated) are
|
||
// allowed. Credentials are enabled because the session lives in
|
||
// HttpOnly cookies. "*" disables the allowlist and reflects any origin
|
||
// (reflected origins are required when credentials are enabled).
|
||
const corsOrigins = (process.env.CORS_ORIGINS ?? '')
|
||
.split(',')
|
||
.map((o) => o.trim())
|
||
.filter(Boolean);
|
||
const origin = corsOrigins.includes('*') ? true : corsOrigins;
|
||
app.enableCors(corsOrigins.length > 0 ? { origin, credentials: true } : undefined);
|
||
|
||
// Serve uploaded files. nosniff prevents browsers from sniffing a
|
||
// non-image content type out of an uploaded file.
|
||
app.useStaticAssets(join(process.cwd(), 'uploads'), {
|
||
prefix: '/uploads/',
|
||
setHeaders: (res) => {
|
||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||
},
|
||
});
|
||
app.useStaticAssets(join(process.cwd(), 'public'), {
|
||
prefix: '/assets/',
|
||
});
|
||
|
||
// Global pipes
|
||
app.useGlobalPipes(
|
||
new ValidationPipe({
|
||
whitelist: true,
|
||
transform: true,
|
||
forbidNonWhitelisted: true,
|
||
}),
|
||
);
|
||
|
||
// Global filters and interceptors
|
||
app.useGlobalFilters(new HttpExceptionFilter());
|
||
app.useGlobalInterceptors(new TransformInterceptor());
|
||
|
||
// Swagger is only exposed outside production to avoid leaking the
|
||
// full admin API surface.
|
||
if (process.env.NODE_ENV !== 'production') {
|
||
const config = new DocumentBuilder()
|
||
.setTitle('InkReach Product Center API')
|
||
.setDescription('Backend API for InkReach Product Center')
|
||
.setVersion('1.0')
|
||
.addBearerAuth()
|
||
.build();
|
||
|
||
const document = SwaggerModule.createDocument(app, config);
|
||
SwaggerModule.setup('api/docs', app, document);
|
||
}
|
||
|
||
const port = process.env.PORT ?? 3001;
|
||
await app.listen(port, '0.0.0.0');
|
||
console.log(`🚀 Application is running on: http://0.0.0.0:${port}`);
|
||
}
|
||
|
||
// Make JSON.stringify aware of BigInt so outgoing responses containing
|
||
// primary keys (`BigInt` columns) don't blow up. BigInts are serialized
|
||
// as their decimal string — clients should parse them with BigInt().
|
||
(BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function () {
|
||
return this.toString();
|
||
};
|
||
|
||
bootstrap();
|