From 5833efed303b63877ba2749155f11fc3873a6565 Mon Sep 17 00:00:00 2001 From: armorbreak001 Date: Tue, 14 Apr 2026 20:58:56 +0800 Subject: [PATCH] feat(backend): add environment config validation guard (Joi) - Add Joi validation schema for all required env vars - Strictly requires: DATABASE_URL, JWT_SECRET, PORT - Conditionally requires REDIS_URL unless QUEUE_DISABLED is set - Validates JWT_SECRET minimum length (16 chars) - Aborts on startup with clear error messages for missing config - Supports storage, mail, and app configuration options --- backend/src/app.module.ts | 7 ++ .../src/common/config/config.validation.ts | 66 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 backend/src/common/config/config.validation.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 5643676..5cdc7a6 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@nestjs/config'; +import { configValidationSchema } from './common/config/config.validation'; import { PrismaModule } from './prisma/prisma.module'; import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module'; @@ -18,6 +19,12 @@ import { QueueModule } from './queue/queue.module'; imports: [ ConfigModule.forRoot({ isGlobal: true, + validationSchema: configValidationSchema, + validationOptions: { + abortEarly: false, // report all errors at once + allowUnknown: true, // allow extra env vars + stripUnknown: false, // keep unknown vars + }, }), ThrottlerModule.forRoot([ { diff --git a/backend/src/common/config/config.validation.ts b/backend/src/common/config/config.validation.ts new file mode 100644 index 0000000..ce846b2 --- /dev/null +++ b/backend/src/common/config/config.validation.ts @@ -0,0 +1,66 @@ +/** + * Environment configuration validation schema using Joi. + * + * Required env vars: PORT, DATABASE_URL, JWT_SECRET + * Conditional: REDIS_URL (required unless QUEUE_DISABLED=true) + * + * Usage: import in AppModule and pass to ConfigModule.forRoot({ validationSchema }) + * + * Install: npm install joi + */ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Joi = require('joi'); + +export const configValidationSchema = Joi.object({ + // Server + PORT: Joi.number().default(3000).optional(), + NODE_ENV: Joi.string() + .valid('development', 'production', 'test') + .default('development'), + + // Database (required) + DATABASE_URL: Joi.string().required().messages({ + 'any.required': 'Missing Configuration: DATABASE_URL', + 'string.empty': 'Missing Configuration: DATABASE_URL', + }), + + // Auth (required) + JWT_SECRET: Joi.string().min(16).required().messages({ + 'any.required': 'Missing Configuration: JWT_SECRET', + 'string.empty': 'Missing Configuration: JWT_SECRET', + 'string.min': 'JWT_SECRET must be at least 16 characters', + }), + JWT_EXPIRES_IN: Joi.string().default('1d').optional(), + + // Redis / Queue (required unless queue disabled) + REDIS_URL: Joi.when('QUEUE_DISABLED', { + is: Joi.exist().valid('true', '1', 'true'), + then: Joi.string().optional(), + otherwise: Joi.string().required().messages({ + 'any.required': 'Missing Configuration: REDIS_URL (required when queue is enabled)', + }), + }), + QUEUE_DISABLED: Joi.string() + .valid('true', '1', 'false', '0') + .optional(), + + // Storage + STORAGE_PROVIDER: Joi.string() + .valid('local', 's3', 'ipfs') + .default('local') + .optional(), + STORAGE_LOCAL_DIR: Joi.string().optional(), + AWS_ACCESS_KEY_ID: Joi.string().optional(), + AWS_SECRET_ACCESS_KEY: Joi.string().optional(), + AWS_S3_BUCKET: Joi.string().optional(), + AWS_REGION: Joi.string().optional(), + + // Mail + SMTP_HOST: Joi.string().optional(), + SMTP_PORT: Joi.number().optional(), + SMTP_USER: Joi.string().optional(), + SMTP_PASS: Joi.string().optional(), + + // App + FRONTEND_URL: Joi.string().optional(), +});