diff --git a/.env.example b/.env.example index 23d9d9c..6503500 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ DB_HOST=localhost DB_PORT=5432 DB_NAME=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 # ----------------------------------------------------------------------------- @@ -18,6 +18,7 @@ DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_N # ----------------------------------------------------------------------------- REDIS_HOST=localhost REDIS_PORT=6379 +REDIS_PASSWORD= REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT} # ----------------------------------------------------------------------------- @@ -26,16 +27,16 @@ REDIS_URL=redis://${REDIS_HOST}:${REDIS_PORT} TYPESENSE_HOST=localhost TYPESENSE_PORT=8108 TYPESENSE_PROTOCOL=http -TYPESENSE_API_KEY=ts_dev_key_change_me +TYPESENSE_API_KEY=CHANGE_ME # ----------------------------------------------------------------------------- # MinIO (S3-compatible Object Storage) # ----------------------------------------------------------------------------- MINIO_ENDPOINT=localhost -MINIO_API_PORT=9000 +MINIO_PORT=9000 MINIO_CONSOLE_PORT=9001 -MINIO_USER=minioadmin -MINIO_PASSWORD=minioadmin_secret +MINIO_ACCESS_KEY=CHANGE_ME +MINIO_SECRET_KEY=CHANGE_ME MINIO_BUCKET=goodgo-media MINIO_USE_SSL=false @@ -43,14 +44,20 @@ MINIO_USE_SSL=false # NestJS API # ----------------------------------------------------------------------------- API_PORT=3000 +PORT=3001 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_REFRESH_SECRET=your_refresh_secret_change_me +JWT_REFRESH_SECRET=CHANGE_ME JWT_REFRESH_EXPIRES_IN=7d # ----------------------------------------------------------------------------- @@ -63,6 +70,7 @@ WEB_PORT=3001 # AI Service (Python/FastAPI) # ----------------------------------------------------------------------------- AI_SERVICE_PORT=8000 +AI_SERVICE_URL=http://localhost:8000 CLAUDE_API_KEY= # ----------------------------------------------------------------------------- @@ -72,12 +80,38 @@ NEXT_PUBLIC_MAPBOX_TOKEN= # ----------------------------------------------------------------------------- # Payment Gateways (VNPay, MoMo, ZaloPay) +# Leave empty if not using payment features # ----------------------------------------------------------------------------- VNPAY_TMN_CODE= 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_ACCESS_KEY= MOMO_SECRET_KEY= +MOMO_ENDPOINT=https://test-payment.momo.vn/v2/gateway/api + ZALOPAY_APP_ID= ZALOPAY_KEY1= 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 diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 4ed6989..be55182 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,12 +1,13 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { LoggerService } from '@modules/shared'; +import { LoggerService, validateEnv } from '@modules/shared'; import cookieParser from 'cookie-parser'; import helmet from 'helmet'; import { AppModule } from './app.module'; async function bootstrap() { + validateEnv(); const app = await NestFactory.create(AppModule, { bufferLogs: true, rawBody: true, @@ -67,7 +68,11 @@ async function bootstrap() { app.use(cookieParser()); // ── 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(',') .map((o) => o.trim()); app.enableCors({ diff --git a/apps/api/src/modules/shared/infrastructure/env-validation.ts b/apps/api/src/modules/shared/infrastructure/env-validation.ts new file mode 100644 index 0000000..d420fef --- /dev/null +++ b/apps/api/src/modules/shared/infrastructure/env-validation.ts @@ -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 = 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')}`, + ); + } +} diff --git a/apps/api/src/modules/shared/infrastructure/index.ts b/apps/api/src/modules/shared/infrastructure/index.ts index 3d7d193..fb5203e 100644 --- a/apps/api/src/modules/shared/infrastructure/index.ts +++ b/apps/api/src/modules/shared/infrastructure/index.ts @@ -11,3 +11,4 @@ export { maskPii } from './pii-masker'; export { ThrottlerBehindProxyGuard } from './guards/throttler-behind-proxy.guard'; export { FileValidationPipe } from './pipes/file-validation.pipe'; export type { FileValidationOptions } from './pipes/file-validation.pipe'; +export { validateEnv } from './env-validation';