Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const envValidationSchema = Joi.object({
BACKUP_ENCRYPTION_KEY: Joi.string().length(64).optional(), // 32-byte key as hex
BACKUP_RETENTION_DAYS: Joi.number().integer().min(1).default(30).optional(),
BACKUP_TMP_DIR: Joi.string().optional(),
ALLOWED_ORIGINS: Joi.string().optional(),
});

@Module({
Expand Down Expand Up @@ -149,6 +150,30 @@ const envValidationSchema = Joi.object({
useFactory: (configService: ConfigService) => {
const dbUrl = configService.get<string>('database.url');
const dbHost = configService.get<string>('database.host');
const isProduction = configService.get<string>('NODE_ENV') === 'production';
const redisUrl = configService.get<string>('REDIS_URL');

const poolConfig = {
max: configService.get<number>('DATABASE_POOL_MAX', isProduction ? 30 : 10),
min: configService.get<number>('DATABASE_POOL_MIN', isProduction ? 5 : 2),
idleTimeoutMillis: configService.get<number>('DATABASE_IDLE_TIMEOUT', 30000),
connectionTimeoutMillis: configService.get<number>('DATABASE_CONNECTION_TIMEOUT', 2000),
statement_timeout: 30000,
query_timeout: 30000,
validationQuery: 'SELECT 1',
validateConnection: true,
};

const cacheConfig = redisUrl
? {
type: 'redis' as const,
options: { url: redisUrl },
duration: 30000,
}
: {
type: 'database' as const,
duration: 30000,
};

if (dbUrl) {
// URL-based connection (e.g. DATABASE_URL on cloud platforms)
Expand All @@ -157,6 +182,9 @@ const envValidationSchema = Joi.object({
url: dbUrl,
autoLoadEntities: true,
synchronize: configService.get<string>('NODE_ENV') !== 'production',
extra: poolConfig,
cache: cacheConfig,
maxQueryExecutionTime: 100, // Monitor and log queries exceeding 100ms
};
}

Expand All @@ -176,6 +204,9 @@ const envValidationSchema = Joi.object({
password: configService.get<string>('database.pass'),
autoLoadEntities: true,
synchronize: configService.get<string>('NODE_ENV') !== 'production',
extra: poolConfig,
cache: cacheConfig,
maxQueryExecutionTime: 100, // Monitor and log queries exceeding 100ms
};
},
}),
Expand Down
20 changes: 20 additions & 0 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ async function bootstrap() {
defaultVersion: CURRENT_VERSION,
});

// Configure CORS
const allowedOriginsString = configService.get<string>('ALLOWED_ORIGINS') || '';
const allowedOrigins = allowedOriginsString.split(',').map((origin) => origin.trim()).filter(Boolean);
const isProduction = configService.get<string>('NODE_ENV') === 'production';

app.enableCors({
origin: (origin, callback) => {
// Allow all origins in non-production, or if ALLOWED_ORIGINS contains '*' or is not set
if (!isProduction || !origin || allowedOrigins.includes(origin) || allowedOrigins.includes('*') || allowedOrigins.length === 0) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Version'],
exposedHeaders: ['X-Deprecated-Version', 'X-Sunset-Date'],
});

// Apply security headers middleware
app.use(helmet.default());
app.use(createSecurityHeadersMiddleware());
Expand Down
Loading
Loading