Complete SaaS self-service onboarding sprint: - Stripe-powered signup flow: pricing page → checkout → provisioning → activation - Refresh token infrastructure: 1h access tokens + 30-day httpOnly cookie refresh - TOTP MFA with QR setup, recovery codes, and login challenge flow - Google + Azure AD SSO (conditional on env vars) with account linking - WebAuthn passkey registration and passwordless login - Guided onboarding checklist with server-side progress tracking - Stubbed email service (console + DB logging, ready for real provider) - Settings page with tabbed security settings (MFA, passkeys, linked accounts) - Login page enhanced with MFA verification, SSO buttons, passkey login - Database migration 015 with all new tables and columns - Version bump to 2026.03.17 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import * as _cluster from 'node:cluster';
|
|
import * as os from 'node:os';
|
|
import { NestFactory } from '@nestjs/core';
|
|
import { ValidationPipe } from '@nestjs/common';
|
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|
import helmet from 'helmet';
|
|
import * as cookieParser from 'cookie-parser';
|
|
import { AppModule } from './app.module';
|
|
|
|
const cluster = _cluster as any; // Cast to 'any' bypasses the missing property errors
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Clustering — fork one worker per CPU core in production
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const WORKERS = isProduction
|
|
? Math.min(os.cpus().length, 4) // cap at 4 workers to stay within DB pool
|
|
: 1; // single process in dev
|
|
|
|
if (WORKERS > 1 && cluster.isPrimary) {
|
|
console.log(`Primary ${process.pid} forking ${WORKERS} workers ...`);
|
|
for (let i = 0; i < WORKERS; i++) {
|
|
cluster.fork();
|
|
}
|
|
cluster.on('exit', (worker: any, code: number) => {
|
|
console.warn(`Worker ${worker.process.pid} exited (code ${code}), restarting ...`);
|
|
cluster.fork();
|
|
});
|
|
} else {
|
|
bootstrap();
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// NestJS bootstrap
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function bootstrap() {
|
|
const app = await NestFactory.create(AppModule, {
|
|
logger: isProduction ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],
|
|
// Enable raw body for Stripe webhook signature verification
|
|
rawBody: true,
|
|
});
|
|
|
|
app.setGlobalPrefix('api');
|
|
|
|
// Cookie parser — needed for refresh token httpOnly cookies
|
|
app.use(cookieParser());
|
|
|
|
// Security headers — Helmet sets CSP, X-Frame-Options, X-Content-Type-Options,
|
|
// Referrer-Policy, Permissions-Policy, and removes X-Powered-By
|
|
app.use(
|
|
helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: ["'self'", "'unsafe-inline'", 'https://chat.hoaledgeriq.com'],
|
|
connectSrc: ["'self'", 'https://chat.hoaledgeriq.com', 'wss://chat.hoaledgeriq.com'],
|
|
imgSrc: ["'self'", 'data:', 'https://chat.hoaledgeriq.com'],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
frameSrc: ["'self'", 'https://chat.hoaledgeriq.com'],
|
|
fontSrc: ["'self'", 'data:'],
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
// Request logging — only in development (too noisy / slow for prod)
|
|
if (!isProduction) {
|
|
app.use((req: any, _res: any, next: any) => {
|
|
console.log(`[REQ] ${req.method} ${req.url} auth=${req.headers.authorization ? 'yes' : 'no'}`);
|
|
next();
|
|
});
|
|
}
|
|
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({
|
|
whitelist: false,
|
|
forbidNonWhitelisted: false,
|
|
transform: true,
|
|
}),
|
|
);
|
|
|
|
// CORS — in production nginx handles this; accept all origins behind the proxy
|
|
app.enableCors({
|
|
origin: isProduction ? true : ['http://localhost', 'http://localhost:5173'],
|
|
credentials: true,
|
|
});
|
|
|
|
// Swagger docs — disabled in production to avoid exposing API surface
|
|
if (!isProduction) {
|
|
const config = new DocumentBuilder()
|
|
.setTitle('HOA LedgerIQ API')
|
|
.setDescription('API for the HOA LedgerIQ')
|
|
.setVersion('2026.3.11')
|
|
.addBearerAuth()
|
|
.build();
|
|
const document = SwaggerModule.createDocument(app, config);
|
|
SwaggerModule.setup('api/docs', app, document);
|
|
}
|
|
|
|
await app.listen(3000);
|
|
console.log(`Backend worker ${process.pid} listening on port 3000`);
|
|
}
|