2025-10-02 16:33:25 +09:00
|
|
|
import { type INestApplication } from "@nestjs/common";
|
2025-09-18 15:18:12 +09:00
|
|
|
import { ConfigService } from "@nestjs/config";
|
|
|
|
|
import { NestFactory } from "@nestjs/core";
|
|
|
|
|
import { Logger } from "nestjs-pino";
|
|
|
|
|
import helmet from "helmet";
|
|
|
|
|
import cookieParser from "cookie-parser";
|
|
|
|
|
import * as express from "express";
|
2025-09-25 17:42:36 +09:00
|
|
|
import type { CookieOptions, Response, NextFunction, Request } from "express";
|
2025-09-25 11:44:10 +09:00
|
|
|
|
|
|
|
|
/* eslint-disable @typescript-eslint/no-namespace */
|
|
|
|
|
declare global {
|
|
|
|
|
namespace Express {
|
|
|
|
|
interface Response {
|
|
|
|
|
setSecureCookie: (name: string, value: string, options?: CookieOptions) => void;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/* eslint-enable @typescript-eslint/no-namespace */
|
2025-09-18 15:18:12 +09:00
|
|
|
|
2025-11-26 16:36:06 +09:00
|
|
|
import { UnifiedExceptionFilter } from "../core/http/exception.filter";
|
2025-09-18 15:18:12 +09:00
|
|
|
|
|
|
|
|
import { AppModule } from "../app.module";
|
|
|
|
|
|
|
|
|
|
export async function bootstrap(): Promise<INestApplication> {
|
|
|
|
|
const app = await NestFactory.create(AppModule, {
|
|
|
|
|
bufferLogs: true,
|
|
|
|
|
// bodyParser is enabled by default in NestJS
|
|
|
|
|
rawBody: true, // Enable raw body access for debugging
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Set Pino as the logger
|
|
|
|
|
app.useLogger(app.get(Logger));
|
|
|
|
|
|
|
|
|
|
const configService = app.get(ConfigService);
|
|
|
|
|
const logger = app.get(Logger);
|
|
|
|
|
|
|
|
|
|
// Enhanced Security Headers
|
|
|
|
|
app.use(
|
|
|
|
|
helmet({
|
|
|
|
|
contentSecurityPolicy: {
|
|
|
|
|
directives: {
|
|
|
|
|
defaultSrc: ["'self'"],
|
|
|
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
|
|
|
scriptSrc: ["'self'"],
|
|
|
|
|
imgSrc: ["'self'", "data:", "https:"],
|
|
|
|
|
connectSrc: ["'self'"],
|
|
|
|
|
fontSrc: ["'self'"],
|
|
|
|
|
objectSrc: ["'none'"],
|
|
|
|
|
mediaSrc: ["'self'"],
|
|
|
|
|
frameSrc: ["'none'"],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
crossOriginEmbedderPolicy: false,
|
|
|
|
|
crossOriginResourcePolicy: { policy: "cross-origin" },
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Disable x-powered-by header
|
|
|
|
|
const expressInstance = app.getHttpAdapter().getInstance() as {
|
|
|
|
|
disable?: (name: string) => void;
|
|
|
|
|
};
|
|
|
|
|
if (typeof expressInstance?.disable === "function") {
|
|
|
|
|
expressInstance.disable("x-powered-by");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure JSON body parser with proper limits
|
|
|
|
|
app.use(express.json({ limit: "10mb" }));
|
|
|
|
|
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
|
|
|
|
|
|
|
|
// Enhanced cookie parser with security options
|
|
|
|
|
app.use(cookieParser());
|
2025-09-25 11:44:10 +09:00
|
|
|
|
|
|
|
|
// Provide helper for secure cookie handling without mutating Express response methods
|
|
|
|
|
const secureCookieDefaults: CookieOptions = {
|
|
|
|
|
httpOnly: true,
|
|
|
|
|
sameSite: "strict",
|
|
|
|
|
secure: configService.get("NODE_ENV") === "production",
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-25 17:42:36 +09:00
|
|
|
app.use((_req: Request, res: Response, next: NextFunction) => {
|
2025-09-25 11:44:10 +09:00
|
|
|
res.setSecureCookie = (name: string, value: string, options: CookieOptions = {}) => {
|
|
|
|
|
res.cookie(name, value, { ...secureCookieDefaults, ...options });
|
|
|
|
|
};
|
2025-09-20 11:35:40 +09:00
|
|
|
next();
|
|
|
|
|
});
|
2025-09-18 15:18:12 +09:00
|
|
|
|
|
|
|
|
// Trust proxy configuration for reverse proxies
|
|
|
|
|
if (configService.get("TRUST_PROXY", "false") === "true") {
|
|
|
|
|
const httpAdapter = app.getHttpAdapter();
|
|
|
|
|
const instance = httpAdapter.getInstance() as {
|
|
|
|
|
set?: (key: string, value: unknown) => void;
|
|
|
|
|
};
|
|
|
|
|
if (typeof instance?.set === "function") {
|
|
|
|
|
instance.set("trust proxy", 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enhanced CORS configuration
|
|
|
|
|
const corsOrigin = configService.get<string | undefined>("CORS_ORIGIN");
|
|
|
|
|
app.enableCors({
|
|
|
|
|
origin: corsOrigin ? [corsOrigin] : false,
|
|
|
|
|
credentials: true,
|
|
|
|
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
|
|
|
allowedHeaders: [
|
|
|
|
|
"Origin",
|
|
|
|
|
"X-Requested-With",
|
|
|
|
|
"Content-Type",
|
|
|
|
|
"Accept",
|
|
|
|
|
"Authorization",
|
|
|
|
|
"X-API-Key",
|
2025-09-27 16:59:25 +09:00
|
|
|
"X-CSRF-Token",
|
2025-09-18 15:18:12 +09:00
|
|
|
],
|
|
|
|
|
exposedHeaders: ["X-Total-Count", "X-Page-Count"],
|
|
|
|
|
maxAge: 86400, // 24 hours
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-26 16:36:06 +09:00
|
|
|
// Global exception filter - single unified filter for all errors
|
|
|
|
|
app.useGlobalFilters(new UnifiedExceptionFilter(app.get(Logger), app.get(ConfigService)));
|
2025-09-18 15:18:12 +09:00
|
|
|
|
|
|
|
|
// Global authentication guard will be registered via APP_GUARD provider in AuthModule
|
|
|
|
|
|
|
|
|
|
// Rely on Nest's built-in shutdown hooks. External orchestrator will send signals.
|
|
|
|
|
app.enableShutdownHooks();
|
|
|
|
|
|
|
|
|
|
// API routing prefix is applied via RouterModule in AppModule for clarity and modern routing.
|
|
|
|
|
|
|
|
|
|
const port = Number(configService.get("BFF_PORT", 4000));
|
|
|
|
|
|
|
|
|
|
await app.listen(port, "0.0.0.0");
|
|
|
|
|
|
|
|
|
|
// Enhanced startup information
|
|
|
|
|
logger.log(`🚀 BFF API running on: http://localhost:${port}/api`);
|
|
|
|
|
logger.log(`🌐 Frontend Portal: http://localhost:${configService.get("NEXT_PORT", 3000)}`);
|
2025-09-25 11:44:10 +09:00
|
|
|
if (configService.get("DATABASE_URL")) {
|
|
|
|
|
logger.log("🗄️ Database connection configured");
|
|
|
|
|
}
|
2025-09-18 15:18:12 +09:00
|
|
|
logger.log(`🔗 Prisma Studio: http://localhost:5555`);
|
2025-09-25 11:44:10 +09:00
|
|
|
if (configService.get("REDIS_URL")) {
|
|
|
|
|
logger.log("🔴 Redis connection configured");
|
|
|
|
|
}
|
2025-09-18 15:18:12 +09:00
|
|
|
|
|
|
|
|
return app;
|
|
|
|
|
}
|