94 lines
3.9 KiB
TypeScript
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();
|