fix(security): add production env validation and sanitize .env.example
- Add startup env validation that fails fast in production if critical vars (JWT_SECRET, JWT_REFRESH_SECRET, DATABASE_URL, CORS_ORIGINS, REDIS_HOST) are missing - Fix CORS_ORIGINS to throw in production instead of defaulting to localhost - Replace hardcoded dev passwords in .env.example with CHANGE_ME placeholders - Add missing vars to .env.example (CORS_ORIGINS, SMTP_*, FIREBASE, LOG_LEVEL) - Warn on missing optional payment/storage vars at startup Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
50
.env.example
50
.env.example
@@ -10,7 +10,7 @@ DB_HOST=localhost
|
|||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_NAME=goodgo
|
DB_NAME=goodgo
|
||||||
DB_USER=goodgo
|
DB_USER=goodgo
|
||||||
DB_PASSWORD=goodgo_secret
|
DB_PASSWORD=CHANGE_ME
|
||||||
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=public
|
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=public
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -18,6 +18,7 @@ DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_N
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
|
REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -26,16 +27,16 @@ REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT}
|
|||||||
TYPESENSE_HOST=localhost
|
TYPESENSE_HOST=localhost
|
||||||
TYPESENSE_PORT=8108
|
TYPESENSE_PORT=8108
|
||||||
TYPESENSE_PROTOCOL=http
|
TYPESENSE_PROTOCOL=http
|
||||||
TYPESENSE_API_KEY=ts_dev_key_change_me
|
TYPESENSE_API_KEY=CHANGE_ME
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# MinIO (S3-compatible Object Storage)
|
# MinIO (S3-compatible Object Storage)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
MINIO_ENDPOINT=localhost
|
MINIO_ENDPOINT=localhost
|
||||||
MINIO_API_PORT=9000
|
MINIO_PORT=9000
|
||||||
MINIO_CONSOLE_PORT=9001
|
MINIO_CONSOLE_PORT=9001
|
||||||
MINIO_USER=minioadmin
|
MINIO_ACCESS_KEY=CHANGE_ME
|
||||||
MINIO_PASSWORD=minioadmin_secret
|
MINIO_SECRET_KEY=CHANGE_ME
|
||||||
MINIO_BUCKET=goodgo-media
|
MINIO_BUCKET=goodgo-media
|
||||||
MINIO_USE_SSL=false
|
MINIO_USE_SSL=false
|
||||||
|
|
||||||
@@ -43,14 +44,20 @@ MINIO_USE_SSL=false
|
|||||||
# NestJS API
|
# NestJS API
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
API_PORT=3000
|
API_PORT=3000
|
||||||
|
PORT=3001
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# JWT / Auth (REQUIRED — app will not start without JWT_SECRET)
|
# CORS — comma-separated allowed origins (REQUIRED in production)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
JWT_SECRET=your_jwt_secret_change_me
|
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# JWT / Auth (REQUIRED — app will not start without these)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
JWT_SECRET=CHANGE_ME
|
||||||
JWT_EXPIRES_IN=15m
|
JWT_EXPIRES_IN=15m
|
||||||
JWT_REFRESH_SECRET=your_refresh_secret_change_me
|
JWT_REFRESH_SECRET=CHANGE_ME
|
||||||
JWT_REFRESH_EXPIRES_IN=7d
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -63,6 +70,7 @@ WEB_PORT=3001
|
|||||||
# AI Service (Python/FastAPI)
|
# AI Service (Python/FastAPI)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
AI_SERVICE_PORT=8000
|
AI_SERVICE_PORT=8000
|
||||||
|
AI_SERVICE_URL=http://localhost:8000
|
||||||
CLAUDE_API_KEY=
|
CLAUDE_API_KEY=
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -72,12 +80,38 @@ NEXT_PUBLIC_MAPBOX_TOKEN=
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Payment Gateways (VNPay, MoMo, ZaloPay)
|
# Payment Gateways (VNPay, MoMo, ZaloPay)
|
||||||
|
# Leave empty if not using payment features
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
VNPAY_TMN_CODE=
|
VNPAY_TMN_CODE=
|
||||||
VNPAY_HASH_SECRET=
|
VNPAY_HASH_SECRET=
|
||||||
|
VNPAY_BASE_URL=https://sandbox.vnpayment.vn/paymentv2/vpcpay.html
|
||||||
|
VNPAY_API_URL=https://sandbox.vnpayment.vn/merchant_webapi/api/transaction
|
||||||
|
|
||||||
MOMO_PARTNER_CODE=
|
MOMO_PARTNER_CODE=
|
||||||
MOMO_ACCESS_KEY=
|
MOMO_ACCESS_KEY=
|
||||||
MOMO_SECRET_KEY=
|
MOMO_SECRET_KEY=
|
||||||
|
MOMO_ENDPOINT=https://test-payment.momo.vn/v2/gateway/api
|
||||||
|
|
||||||
ZALOPAY_APP_ID=
|
ZALOPAY_APP_ID=
|
||||||
ZALOPAY_KEY1=
|
ZALOPAY_KEY1=
|
||||||
ZALOPAY_KEY2=
|
ZALOPAY_KEY2=
|
||||||
|
ZALOPAY_ENDPOINT=https://sb-openapi.zalopay.vn/v2
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Email / SMTP
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
SMTP_HOST=localhost
|
||||||
|
SMTP_PORT=1025
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASS=
|
||||||
|
SMTP_FROM=noreply@goodgo.vn
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Firebase Cloud Messaging (optional)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
FIREBASE_SERVICE_ACCOUNT=
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import { LoggerService } from '@modules/shared';
|
import { LoggerService, validateEnv } from '@modules/shared';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
validateEnv();
|
||||||
const app = await NestFactory.create(AppModule, {
|
const app = await NestFactory.create(AppModule, {
|
||||||
bufferLogs: true,
|
bufferLogs: true,
|
||||||
rawBody: true,
|
rawBody: true,
|
||||||
@@ -67,7 +68,11 @@ async function bootstrap() {
|
|||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
// ── CORS ──
|
// ── CORS ──
|
||||||
const allowedOrigins = (process.env['CORS_ORIGINS'] ?? 'http://localhost:3000')
|
const corsOrigins = process.env['CORS_ORIGINS'];
|
||||||
|
if (!corsOrigins && process.env['NODE_ENV'] === 'production') {
|
||||||
|
throw new Error('CORS_ORIGINS must be set in production');
|
||||||
|
}
|
||||||
|
const allowedOrigins = (corsOrigins ?? 'http://localhost:3000')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((o) => o.trim());
|
.map((o) => o.trim());
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
|
|||||||
62
apps/api/src/modules/shared/infrastructure/env-validation.ts
Normal file
62
apps/api/src/modules/shared/infrastructure/env-validation.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Validates that critical environment variables are set in production.
|
||||||
|
* Call this at application startup before any module initialization.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const REQUIRED_IN_PRODUCTION: readonly string[] = [
|
||||||
|
'JWT_SECRET',
|
||||||
|
'JWT_REFRESH_SECRET',
|
||||||
|
'DATABASE_URL',
|
||||||
|
'CORS_ORIGINS',
|
||||||
|
'REDIS_HOST',
|
||||||
|
];
|
||||||
|
|
||||||
|
const REQUIRED_WHEN_USED: ReadonlyMap<string, string> = new Map([
|
||||||
|
['VNPAY_TMN_CODE', 'VNPay payments'],
|
||||||
|
['VNPAY_HASH_SECRET', 'VNPay payments'],
|
||||||
|
['MOMO_PARTNER_CODE', 'MoMo payments'],
|
||||||
|
['MOMO_ACCESS_KEY', 'MoMo payments'],
|
||||||
|
['MOMO_SECRET_KEY', 'MoMo payments'],
|
||||||
|
['ZALOPAY_APP_ID', 'ZaloPay payments'],
|
||||||
|
['ZALOPAY_KEY1', 'ZaloPay payments'],
|
||||||
|
['ZALOPAY_KEY2', 'ZaloPay payments'],
|
||||||
|
['MINIO_ACCESS_KEY', 'Media storage'],
|
||||||
|
['MINIO_SECRET_KEY', 'Media storage'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function validateEnv(): void {
|
||||||
|
const isProduction = process.env['NODE_ENV'] === 'production';
|
||||||
|
|
||||||
|
if (!isProduction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const missing: string[] = [];
|
||||||
|
|
||||||
|
for (const key of REQUIRED_IN_PRODUCTION) {
|
||||||
|
if (!process.env[key]) {
|
||||||
|
missing.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing required environment variables in production:\n ${missing.join('\n ')}\n` +
|
||||||
|
'See .env.example for the full list of variables.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn about payment/storage vars that are empty — services will fail at
|
||||||
|
// runtime when invoked, but we surface the warning early.
|
||||||
|
const warnings: string[] = [];
|
||||||
|
for (const [key, feature] of REQUIRED_WHEN_USED) {
|
||||||
|
if (!process.env[key]) {
|
||||||
|
warnings.push(` ${key} (${feature})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[env-validation] The following optional vars are unset — related features will fail at runtime:\n${warnings.join('\n')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,3 +11,4 @@ export { maskPii } from './pii-masker';
|
|||||||
export { ThrottlerBehindProxyGuard } from './guards/throttler-behind-proxy.guard';
|
export { ThrottlerBehindProxyGuard } from './guards/throttler-behind-proxy.guard';
|
||||||
export { FileValidationPipe } from './pipes/file-validation.pipe';
|
export { FileValidationPipe } from './pipes/file-validation.pipe';
|
||||||
export type { FileValidationOptions } from './pipes/file-validation.pipe';
|
export type { FileValidationOptions } from './pipes/file-validation.pipe';
|
||||||
|
export { validateEnv } from './env-validation';
|
||||||
|
|||||||
Reference in New Issue
Block a user