From df0f956e3951a0e77a8310f2300b100ee8bf3df0 Mon Sep 17 00:00:00 2001 From: ChukwuemekaP1 Date: Thu, 18 Jun 2026 21:50:01 +0100 Subject: [PATCH 1/2] Security Hardening Engine: JWT strategy and WebSocket gateway --- package-lock.json | 70 ++++++++++++---------- src/app.module.ts | 2 + src/auth/auth-config.service.ts | 39 ++++++++++++ src/auth/auth.module.ts | 15 +++-- src/auth/jwt-auth.guard.ts | 9 +-- src/auth/jwt.strategy.ts | 6 +- src/common/config/env.validation.spec.ts | 52 ++++++++++++++++ src/common/config/env.validation.ts | 67 +++++++++++++++++++++ src/milestones/milestones.module.ts | 7 +-- src/notifications/notifications.gateway.ts | 9 +-- src/notifications/notifications.module.ts | 12 +--- src/redis/redis.module.ts | 4 +- src/stellar/soroban.service.ts | 14 ++--- src/stellar/stellar.module.ts | 3 +- src/users/users.module.ts | 7 +-- 15 files changed, 233 insertions(+), 83 deletions(-) create mode 100644 src/auth/auth-config.service.ts create mode 100644 src/common/config/env.validation.spec.ts create mode 100644 src/common/config/env.validation.ts diff --git a/package-lock.json b/package-lock.json index 9fbe732..1eebf2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -247,6 +247,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -771,6 +772,7 @@ "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.21.3.tgz", "integrity": "sha512-s/PLBJab8cnoQAGVqjQb0v4oGe0KgB4aQ5G5g93doxzXB/D+wkXNL9P9+zLWLldBJXE57jL4CR99ttDCIiyNHw==", "license": "MIT", + "peer": true, "dependencies": { "@bull-board/api": "6.21.3" } @@ -820,29 +822,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -2276,6 +2255,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", "license": "MIT", + "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "axios": "^1.3.1", @@ -2374,6 +2354,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2544,6 +2525,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.24.tgz", "integrity": "sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -2591,6 +2573,7 @@ "integrity": "sha512-K4bzT+lEdd0Hhcsw3jtk56QAW6s6skK3ViN7hIROSN0kUf4ROwWEAKopJID6yhPQxB45kDtP2wEcjzE8171J3g==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2674,6 +2657,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.24.tgz", "integrity": "sha512-CeMKbRBm05aOBiWhIHWO2xDeHbxynBF9ySQv3gRjObz2N5+uJnYriAYkHvVqvC4JIydmMPmT5VdICFNlNz3qyA==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -2695,6 +2679,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.24.tgz", "integrity": "sha512-ImdR9G8W5Y2Hhcptdci+tNaG6JV/dzDguFTgtXOL5ie/gD9O9ARw8Cd9RzF2+oteyzQ+1sPK/+wgVOPOyYGVCA==", "license": "MIT", + "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -2878,6 +2863,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.24.tgz", "integrity": "sha512-37Z/QYzZ4nPHcGyGGjhjoKVOcpSPMhmRQj5DS1l0RKlRYgq8S0cmgaZ6kQ8PI3259PdchLx41oQibXh22iEUiA==", "license": "MIT", + "peer": true, "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -2930,6 +2916,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2951,6 +2938,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" }, @@ -2963,6 +2951,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, @@ -2987,6 +2976,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", @@ -3392,6 +3382,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" @@ -3417,6 +3408,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", @@ -3443,6 +3435,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -3502,6 +3495,7 @@ "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -4169,6 +4163,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4370,6 +4365,7 @@ "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", @@ -5120,6 +5116,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5190,6 +5187,7 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5408,6 +5406,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", @@ -5767,6 +5766,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5845,6 +5845,7 @@ "version": "4.16.5", "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz", "integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==", + "peer": true, "dependencies": { "cron-parser": "^4.9.0", "get-port": "^5.1.1", @@ -5925,6 +5926,7 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.8.tgz", "integrity": "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==", "license": "MIT", + "peer": true, "dependencies": { "@cacheable/utils": "^2.3.3", "keyv": "^5.5.5" @@ -6138,13 +6140,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -7015,6 +7019,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7075,6 +7080,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8622,6 +8628,7 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -9512,6 +9519,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -10401,6 +10409,7 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", + "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -10750,6 +10759,7 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10809,6 +10819,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.19.3", "@prisma/engines": "6.19.3" @@ -11941,6 +11952,7 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12285,6 +12297,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12469,6 +12482,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12836,7 +12850,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12854,7 +12867,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12867,7 +12879,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12881,7 +12892,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -12890,15 +12900,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true + "dev": true }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/src/app.module.ts b/src/app.module.ts index 36d91f6..ab4d3e8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { validate } from './common/config/env.validation'; import { AdminModule } from './admin/admin.module'; import { ApiKeysModule } from './api-keys/api-keys.module'; import { AppController } from './app.controller'; @@ -23,6 +24,7 @@ import { UsersModule } from './users/users.module'; imports: [ ConfigModule.forRoot({ isGlobal: true, + validate, }), AdminModule, ApiKeysModule, diff --git a/src/auth/auth-config.service.ts b/src/auth/auth-config.service.ts new file mode 100644 index 0000000..3a01540 --- /dev/null +++ b/src/auth/auth-config.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class AuthConfigService { + constructor(private readonly configService: ConfigService) {} + + /** + * Gets the JWT secret from the configuration. + * Since we have validation at startup, we can safely assume it exists and is valid. + */ + get jwtSecret(): string { + const secret = this.configService.get('JWT_SECRET'); + if (!secret) { + throw new Error('JWT_SECRET is not defined in configuration'); + } + return secret; + } + + get jwtExpiresIn(): string { + return this.configService.get('JWT_EXPIRES_IN', '15m'); + } + + get stellarRpcUrl(): string { + return this.configService.get('STELLAR_RPC_URL')!; + } + + get stellarNetworkPassphrase(): string { + return this.configService.get('STELLAR_NETWORK_PASSPHRASE')!; + } + + get redisUrl(): string { + return this.configService.get('REDIS_URL')!; + } + + get databaseUrl(): string { + return this.configService.get('DATABASE_URL')!; + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 6fd5625..05d81a9 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,24 +1,27 @@ import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigModule } from '@nestjs/config'; import { PrismaModule } from '../prisma/prisma.module'; import { AuthChallengeController } from './auth-challenge.controller'; import { AuthVerifyController } from './auth-verify.controller'; +import { AuthConfigService } from './auth-config.service'; +import { JwtStrategy } from './jwt.strategy'; /** Module providing Stellar wallet challenge-response authentication and JWT issuance */ @Module({ imports: [ JwtModule.registerAsync({ imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - secret: config.get('JWT_SECRET', 'orbitchain-default-secret'), - signOptions: { expiresIn: '15m' }, + inject: [AuthConfigService], + useFactory: (authConfig: AuthConfigService) => ({ + secret: authConfig.jwtSecret, + signOptions: { expiresIn: authConfig.jwtExpiresIn }, }), }), PrismaModule, ], controllers: [AuthChallengeController, AuthVerifyController], - exports: [JwtModule], + providers: [AuthConfigService, JwtStrategy], + exports: [JwtModule, AuthConfigService], }) export class AuthModule {} diff --git a/src/auth/jwt-auth.guard.ts b/src/auth/jwt-auth.guard.ts index 2ed6305..ddc4df4 100644 --- a/src/auth/jwt-auth.guard.ts +++ b/src/auth/jwt-auth.guard.ts @@ -5,14 +5,14 @@ import { UnauthorizedException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; import { Request } from 'express'; +import { AuthConfigService } from './auth-config.service'; @Injectable() export class JwtAuthGuard implements CanActivate { constructor( private readonly jwt: JwtService, - private readonly config: ConfigService, + private readonly authConfig: AuthConfigService, ) {} canActivate(context: ExecutionContext): boolean { @@ -32,10 +32,7 @@ export class JwtAuthGuard implements CanActivate { try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const payload = this.jwt.verify(token, { - secret: this.config.get( - 'JWT_SECRET', - 'orbitchain-default-secret', - ), + secret: this.authConfig.jwtSecret, }); request.user = payload as Record; return true; diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index c56ee46..6f11322 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -1,7 +1,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { ConfigService } from '@nestjs/config'; +import { AuthConfigService } from './auth-config.service'; /** * Passport JWT strategy for OrbitChain. @@ -9,11 +9,11 @@ import { ConfigService } from '@nestjs/config'; */ @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(configService: ConfigService) { + constructor(authConfig: AuthConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET', 'default-secret'), + secretOrKey: authConfig.jwtSecret, }); } diff --git a/src/common/config/env.validation.spec.ts b/src/common/config/env.validation.spec.ts new file mode 100644 index 0000000..fa8d03b --- /dev/null +++ b/src/common/config/env.validation.spec.ts @@ -0,0 +1,52 @@ +import 'reflect-metadata'; +import { validate } from './env.validation'; + +describe('ConfigValidation', () => { + it('should throw error if JWT_SECRET is missing', () => { + const config = { + NODE_ENV: 'development', + STELLAR_RPC_URL: 'http://localhost:8000', + STELLAR_NETWORK_PASSPHRASE: 'Test SDF Network ; September 2015', + REDIS_URL: 'redis://localhost:6379', + DATABASE_URL: 'postgresql://user:pass@localhost:5432/db', + }; + expect(() => validate(config)).toThrow(/JWT_SECRET/); + }); + + it('should throw error if JWT_SECRET is too short', () => { + const config = { + NODE_ENV: 'development', + JWT_SECRET: 'short', + STELLAR_RPC_URL: 'http://localhost:8000', + STELLAR_NETWORK_PASSPHRASE: 'Test SDF Network ; September 2015', + REDIS_URL: 'redis://localhost:6379', + DATABASE_URL: 'postgresql://user:pass@localhost:5432/db', + }; + expect(() => validate(config)).toThrow(/at least 32 characters/); + }); + + it('should pass if all config is valid', () => { + const config = { + NODE_ENV: 'development', + JWT_SECRET: 'a'.repeat(32), + STELLAR_RPC_URL: 'http://localhost:8000', + STELLAR_NETWORK_PASSPHRASE: 'Test SDF Network ; September 2015', + REDIS_URL: 'redis://localhost:6379', + DATABASE_URL: 'postgresql://user:pass@localhost:5432/db', + }; + expect(() => validate(config)).not.toThrow(); + const validated = validate(config); + expect(validated.JWT_SECRET).toBe('a'.repeat(32)); + }); + + it('should throw error if other required vars are missing', () => { + const config = { + NODE_ENV: 'development', + JWT_SECRET: 'a'.repeat(32), + }; + expect(() => validate(config)).toThrow(/STELLAR_RPC_URL/); + expect(() => validate(config)).toThrow(/STELLAR_NETWORK_PASSPHRASE/); + expect(() => validate(config)).toThrow(/REDIS_URL/); + expect(() => validate(config)).toThrow(/DATABASE_URL/); + }); +}); diff --git a/src/common/config/env.validation.ts b/src/common/config/env.validation.ts new file mode 100644 index 0000000..51d457e --- /dev/null +++ b/src/common/config/env.validation.ts @@ -0,0 +1,67 @@ +import 'reflect-metadata'; +import { plainToInstance } from 'class-transformer'; +import { + IsEnum, + IsNotEmpty, + IsString, + MinLength, + validateSync, +} from 'class-validator'; + +enum Environment { + Development = 'development', + Production = 'production', + Test = 'test', + Provision = 'provision', +} + +class EnvironmentVariables { + @IsEnum(Environment) + NODE_ENV: Environment; + + @IsString() + @IsNotEmpty() + @MinLength(32, { + message: 'JWT_SECRET must be at least 32 characters long for security', + }) + JWT_SECRET: string; + + @IsString() + @IsNotEmpty() + STELLAR_RPC_URL: string; + + @IsString() + @IsNotEmpty() + STELLAR_NETWORK_PASSPHRASE: string; + + @IsString() + @IsNotEmpty() + REDIS_URL: string; + + @IsString() + @IsNotEmpty() + DATABASE_URL: string; +} + +export function validate(config: Record) { + const validatedConfig = plainToInstance(EnvironmentVariables, config, { + enableImplicitConversion: true, + }); + const errors = validateSync(validatedConfig, { + skipMissingProperties: false, + }); + + if (errors.length > 0) { + const errorMessages = errors + .map((error) => { + const constraints = Object.values(error.constraints || {}); + return `${error.property}: ${constraints.join(', ')}`; + }) + .join('\n'); + + throw new Error( + `\n\nāŒ CONFIGURATION VALIDATION FAILED:\n${errorMessages}\n\nšŸ›‘ Application startup stopped due to insecure or missing configuration.\n`, + ); + } + return validatedConfig; +} diff --git a/src/milestones/milestones.module.ts b/src/milestones/milestones.module.ts index 8f3c818..2ef916c 100644 --- a/src/milestones/milestones.module.ts +++ b/src/milestones/milestones.module.ts @@ -1,18 +1,15 @@ import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; import { MilestonesController } from './milestones.controller'; import { MilestonesService } from './milestones.service'; import { PrismaModule } from '../prisma/prisma.module'; import { JwtAuthGuard } from '../users/guards/jwt-auth.guard'; +import { AuthModule } from '../auth/auth.module'; /** Module providing milestone tracking and fund release request management */ @Module({ imports: [ PrismaModule, - JwtModule.register({ - secret: process.env.JWT_SECRET || 'your-secret-key', - signOptions: { expiresIn: '7d' }, - }), + AuthModule, ], controllers: [MilestonesController], providers: [MilestonesService, JwtAuthGuard], diff --git a/src/notifications/notifications.gateway.ts b/src/notifications/notifications.gateway.ts index a609388..b4c8671 100644 --- a/src/notifications/notifications.gateway.ts +++ b/src/notifications/notifications.gateway.ts @@ -7,8 +7,8 @@ import { } from '@nestjs/websockets'; import { Logger, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { ConfigService } from '@nestjs/config'; import type { Server, Socket } from 'socket.io'; +import { AuthConfigService } from '../auth/auth-config.service'; /** * WebSocket gateway providing real-time notification events. @@ -43,7 +43,7 @@ export class NotificationsGateway constructor( private readonly jwtService: JwtService, - private readonly configService: ConfigService, + private readonly authConfig: AuthConfigService, ) {} afterInit(): void { @@ -65,10 +65,7 @@ export class NotificationsGateway throw new UnauthorizedException('Missing authentication token'); } - const secret = this.configService.get( - 'JWT_SECRET', - 'orbitchain-default-secret', - ); + const secret = this.authConfig.jwtSecret; const payload = this.jwtService.verify(token, { secret }); diff --git a/src/notifications/notifications.module.ts b/src/notifications/notifications.module.ts index 0997da0..6a7e214 100644 --- a/src/notifications/notifications.module.ts +++ b/src/notifications/notifications.module.ts @@ -1,27 +1,19 @@ import { Module } from '@nestjs/common'; import { BullModule } from '@nestjs/bull'; -import { JwtModule } from '@nestjs/jwt'; -import { ConfigModule, ConfigService } from '@nestjs/config'; import { PrismaModule } from '../prisma/prisma.module'; import { QUEUE_EMAIL } from '../queue/queue.constants'; import { NotificationsService } from './notifications.service'; import { EmailService } from './email.service'; import { EmailProcessor } from './email.processor'; import { NotificationsGateway } from './notifications.gateway'; +import { AuthModule } from '../auth/auth.module'; /** Module providing WebSocket gateway, email notifications, and notification preferences */ @Module({ imports: [ PrismaModule, BullModule.registerQueue({ name: QUEUE_EMAIL }), - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (config: ConfigService) => ({ - secret: config.get('JWT_SECRET', 'orbitchain-default-secret'), - signOptions: { expiresIn: '15m' }, - }), - }), + AuthModule, ], providers: [ NotificationsService, diff --git a/src/redis/redis.module.ts b/src/redis/redis.module.ts index e4f1445..b9e4a32 100644 --- a/src/redis/redis.module.ts +++ b/src/redis/redis.module.ts @@ -12,9 +12,7 @@ import KeyvRedis from '@keyv/redis'; inject: [ConfigService], useFactory: (config: ConfigService) => ({ stores: [ - new KeyvRedis( - config.get('REDIS_URL', 'redis://localhost:6379'), - ), + new KeyvRedis(config.get('REDIS_URL')!), ], ttl: 60000, }), diff --git a/src/stellar/soroban.service.ts b/src/stellar/soroban.service.ts index 2974ff6..ed4972a 100644 --- a/src/stellar/soroban.service.ts +++ b/src/stellar/soroban.service.ts @@ -12,6 +12,7 @@ import { Transaction, FeeBumpTransaction, } from '@stellar/stellar-sdk'; +import { AuthConfigService } from '../auth/auth-config.service'; @Injectable() export class SorobanService { @@ -20,14 +21,13 @@ export class SorobanService { private readonly serverKeypair?: Keypair; private readonly feeBumpKeypair?: Keypair; - constructor(private readonly config: ConfigService) { - const rpcUrl = - this.config.get('STELLAR_RPC_URL') || - 'https://soroban-testnet.stellar.org:443'; + constructor( + private readonly config: ConfigService, + private readonly authConfig: AuthConfigService, + ) { + const rpcUrl = this.authConfig.stellarRpcUrl; this.server = new rpc.Server(rpcUrl); - this.networkPassphrase = - this.config.get('STELLAR_NETWORK_PASSPHRASE') || - 'Test SDF Network ; September 2015'; + this.networkPassphrase = this.authConfig.stellarNetworkPassphrase; const serverSecret = this.config.get('STELLAR_SERVER_SECRET'); if (serverSecret) { diff --git a/src/stellar/stellar.module.ts b/src/stellar/stellar.module.ts index 5852079..e99b367 100644 --- a/src/stellar/stellar.module.ts +++ b/src/stellar/stellar.module.ts @@ -4,10 +4,11 @@ import { StellarEventService } from './stellar-event.service'; import { PrismaModule } from '../prisma/prisma.module'; import { QueueModule } from '../queue/queue.module'; import { StellarTransactionsService } from './stellar-transactions.service.js'; +import { AuthModule } from '../auth/auth.module'; /** Module providing Stellar Horizon, Soroban, and transaction services */ @Module({ - imports: [PrismaModule, QueueModule], + imports: [PrismaModule, QueueModule, AuthModule], providers: [SorobanService, StellarEventService, StellarTransactionsService], exports: [SorobanService, StellarEventService, StellarTransactionsService], }) diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 9207d1c..b77fbc9 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { JwtModule } from '@nestjs/jwt'; import { BullModule } from '@nestjs/bull'; import { UsersService } from './users.service'; import { UsersController, AdminUsersController } from './users.controller'; @@ -8,15 +7,13 @@ import { PrismaModule } from '../prisma/prisma.module'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { AdminGuard } from './guards/admin.guard'; import { QUEUE_EXPORT } from '../queue/queue.constants'; +import { AuthModule } from '../auth/auth.module'; /** Module providing user profiles, KYC management, notification prefs, and donation exports */ @Module({ imports: [ PrismaModule, - JwtModule.register({ - secret: process.env.JWT_SECRET || 'your-secret-key', - signOptions: { expiresIn: '7d' }, - }), + AuthModule, BullModule.registerQueue({ name: QUEUE_EXPORT }), ], controllers: [UsersController, AdminUsersController], From 4f01eceffbc4dfda2f7a488a4b84ea4028503cf3 Mon Sep 17 00:00:00 2001 From: ChukwuemekaP1 Date: Fri, 19 Jun 2026 06:02:21 +0100 Subject: [PATCH 2/2] Fixed Security Hardening Engine reslove the pending issue --- .env.example | 6 ++- .gitignore | 1 + package.json | 2 +- prisma/schema.prisma | 96 ++++++++++++++++++++++---------------------- 4 files changed, 55 insertions(+), 50 deletions(-) diff --git a/.env.example b/.env.example index 5ec4b39..adc1037 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,11 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/orbitchain?schema=pu REDIS_URL="redis://localhost:6379" # JWT -JWT_SECRET=change-me-in-production +JWT_SECRET=your-secure-random-32-character-or-longer-secret-key-here-please-change + +# Stellar / Soroban Configuration +STELLAR_RPC_URL=https://soroban-testnet.stellar.org:443 +STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" # Admin wallet allowlist (comma-separated Stellar public keys) # On auth, any wallet in this list will receive role=ADMIN diff --git a/.gitignore b/.gitignore index 259a069..7483274 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ dist # Environment files .env .env.*.local +.env.test !.env.example # IDE diff --git a/package.json b/package.json index e592c1f..345e011 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json", + "test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9024871..e913239 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -159,31 +159,31 @@ model ApiKey { /// Campaign model representing fundraising campaigns model Campaign { - id String @id @default(uuid()) - title String - description String - story String? - goalAmount Decimal @db.Decimal(20, 7) - raisedAmount Decimal @default(0) @db.Decimal(20, 7) - status CampaignStatus @default(DRAFT) - creatorId String - contractId String? + id String @id @default(uuid()) + title String + description String + story String? + goalAmount Decimal @db.Decimal(20, 7) + raisedAmount Decimal @default(0) @db.Decimal(20, 7) + status CampaignStatus @default(DRAFT) + creatorId String + contractId String? acceptedAssets Json? - isFeatured Boolean @default(false) - startDate DateTime? - endDate DateTime? - imageUrl String? - category String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + isFeatured Boolean @default(false) + startDate DateTime? + endDate DateTime? + imageUrl String? + category String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations - creator User @relation("CampaignCreator", fields: [creatorId], references: [id], onDelete: Cascade) + creator User @relation("CampaignCreator", fields: [creatorId], references: [id], onDelete: Cascade) donations Donation[] milestones Milestone[] updates Update[] disputes Dispute[] - contract SmartContract? + contract SmartContract? fundReleases FundRelease[] @@index([creatorId]) @@ -228,16 +228,16 @@ model Donation { /// PlatformTip model representing platform tips sent with donations model PlatformTip { - id String @id @default(uuid()) - amount Decimal @db.Decimal(20, 7) - assetCode String @default("XLM") - txHash String @unique - status TipStatus @default(PENDING) - donorId String - donationId String? @unique - confirmedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + amount Decimal @db.Decimal(20, 7) + assetCode String @default("XLM") + txHash String @unique + status TipStatus @default(PENDING) + donorId String + donationId String? @unique + confirmedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations donor User @relation(fields: [donorId], references: [id], onDelete: Cascade) @@ -265,8 +265,8 @@ model Milestone { updatedAt DateTime @updatedAt // Relations - campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) - fundReleases FundRelease[] + campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) + fundReleases FundRelease[] @@index([campaignId]) @@index([status]) @@ -275,20 +275,20 @@ model Milestone { /// FundRelease model representing requests to release funds for milestones model FundRelease { - id String @id @default(uuid()) - milestoneId String - campaignId String - creatorId String - amount Decimal @db.Decimal(20, 7) - status FundReleaseStatus @default(PENDING) - signaturePayload String? // JSON stringified signature for on-chain verification - txHash String? @unique - releaseReason String? - approvedBy String? // Admin ID who approved (if applicable) - approvedAt DateTime? - releasedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + milestoneId String + campaignId String + creatorId String + amount Decimal @db.Decimal(20, 7) + status FundReleaseStatus @default(PENDING) + signaturePayload String? // JSON stringified signature for on-chain verification + txHash String? @unique + releaseReason String? + approvedBy String? // Admin ID who approved (if applicable) + approvedAt DateTime? + releasedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations milestone Milestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade) @@ -395,10 +395,10 @@ model Newsletter { /// NotificationPreference model storing per-user notification channel toggles model NotificationPreference { - id String @id @default(uuid()) - userId String @unique + id String @id @default(uuid()) + userId String @unique /// JSON object structured as { donationReceived: { email: bool, inApp: bool }, milestoneUnlocked: { email: bool, inApp: bool }, ... } - preferences Json @default("{\"donationReceived\":{\"email\":true,\"inApp\":true},\"milestoneUnlocked\":{\"email\":true,\"inApp\":true},\"campaignUpdate\":{\"email\":true,\"inApp\":true},\"campaignCreated\":{\"email\":true,\"inApp\":true},\"campaignCompleted\":{\"email\":true,\"inApp\":true}}") + preferences Json @default("{\"donationReceived\":{\"email\":true,\"inApp\":true},\"milestoneUnlocked\":{\"email\":true,\"inApp\":true},\"campaignUpdate\":{\"email\":true,\"inApp\":true},\"campaignCreated\":{\"email\":true,\"inApp\":true},\"campaignCompleted\":{\"email\":true,\"inApp\":true}}") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -442,7 +442,7 @@ model SmartContract { updatedAt DateTime @updatedAt // Relations - campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) + campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade) @@index([campaignId]) @@map("contracts")