From 3beed740f32d3b1a4e7559514f2808fc6f48f1f9 Mon Sep 17 00:00:00 2001 From: armorbreak001 Date: Tue, 14 Apr 2026 19:16:56 +0800 Subject: [PATCH] feat: implement environment config validation guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add joi dependency for schema validation - Add validationSchema to ConfigModule.forRoot in AppModule - Required: PORT (default 3000), DATABASE_URL, JWT_SECRET (min 16 chars) - Optional: REDIS_HOST, REDIS_PORT, REDIS_URL - On missing/invalid env vars → NestJS hard crash with clear error message - abortEarly: false to show all validation errors at once Fixes #292 --- backend/package-lock.json | 73 +++++++++++++++++++++++++++++++++++++++ backend/package.json | 5 ++- backend/src/app.module.ts | 19 ++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 8160aa0..00e3810 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,6 +25,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "ethers": "^6.16.0", + "joi": "^18.1.2", "nodemailer": "^6.9.3", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", @@ -994,6 +995,54 @@ "npm": ">=10" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "http://mirrors.tencentyun.com/npm/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2912,6 +2961,12 @@ "text-hex": "1.0.x" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "http://mirrors.tencentyun.com/npm/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -7852,6 +7907,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "18.1.2", + "resolved": "http://mirrors.tencentyun.com/npm/joi/-/joi-18.1.2.tgz", + "integrity": "sha512-rF5MAmps5esSlhCA+N1b6IYHDw9j/btzGaqfgie522jS02Ju/HXBxamlXVlKEHAxoMKQL77HWI8jlqWsFuekZA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index fe5b60a..b4a9542 100644 --- a/backend/package.json +++ b/backend/package.json @@ -44,6 +44,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "ethers": "^6.16.0", + "joi": "^18.1.2", "nodemailer": "^6.9.3", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", @@ -89,7 +90,9 @@ "ts" ], "rootDir": "src", - "setupFiles": ["dotenv/config"], + "setupFiles": [ + "dotenv/config" + ], "testRegex": ".*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 5643676..79aa5c5 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 * as Joi from 'joi'; import { PrismaModule } from './prisma/prisma.module'; import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module'; @@ -18,6 +19,24 @@ import { QueueModule } from './queue/queue.module'; imports: [ ConfigModule.forRoot({ isGlobal: true, + validationSchema: Joi.object({ + PORT: Joi.number().default(3000), + DATABASE_URL: Joi.string().required(), + JWT_SECRET: Joi.string() + .min(16) + .required() + .messages({ + 'string.min': 'JWT_SECRET must be at least 16 characters', + 'any.required': 'Missing Configuration: JWT_SECRET', + }), + REDIS_HOST: Joi.string().optional(), + REDIS_PORT: Joi.number().optional(), + REDIS_URL: Joi.string().optional(), + }), + validationOptions: { + abortEarly: false, + allowUnknown: true, + }, }), ThrottlerModule.forRoot([ {