94 lines
3.9 KiB
TypeScript

import "dotenv/config";
import * as Sentry from "@sentry/node";
import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { Logger } from "nestjs-pino";
import helmet from "helmet";
import { AppModule } from "./app.module";
import { SentryExceptionFilter } from "./common/sentry.filter";
async function bootstrap() {
const isProduction = process.env.NODE_ENV === "production";
// ─── Sentry initialization (before app creation) ──────────────────────────
if (process.env.SENTRY_DSN) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV ?? "development",
tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE ?? 0),
});
}
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
rawBody: true, // Required for Stripe webhook signature verification
});
// ─── Security headers ─────────────────────────────────────────────────────
const scriptSrc = isProduction ? ["'self'"] : ["'self'", "'unsafe-inline'"];
const styleSrc = isProduction ? ["'self'"] : ["'self'", "'unsafe-inline'"];
app.use(
helmet({
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc,
styleSrc,
imgSrc: ["'self'", "data:", "https:"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"],
},
},
}),
);
// ─── CORS ─────────────────────────────────────────────────────────────────
const corsOrigin = process.env.CORS_ORIGIN ?? "http://localhost:3052";
app.enableCors({
origin: corsOrigin.split(",").map((o) => o.trim()),
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
});
// ─── Global prefix ────────────────────────────────────────────────────────
app.setGlobalPrefix("api");
// ─── Global validation pipe ───────────────────────────────────────────────
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
// ─── Swagger / OpenAPI ────────────────────────────────────────────────────
if (process.env.NODE_ENV !== "production") {
const config = new DocumentBuilder()
.setTitle("LedgerOne API")
.setDescription("Personal finance & bookkeeping SaaS API")
.setVersion("1.0")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("api/docs", app, document);
}
// ─── Global exception filter (Sentry + structured error response) ─────────
app.useGlobalFilters(new SentryExceptionFilter());
// ─── Use Pino as the logger ────────────────────────────────────────────────
app.useLogger(app.get(Logger));
const port = process.env.PORT ?? 3051;
await app.listen(port);
app.get(Logger).log(`LedgerOne backend running on port ${port}`, "Bootstrap");
}
bootstrap();