diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..01ab138 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,75 @@ +# Issue #34 — Dependabot configuration. +# Covers every workspace in this monorepo: Backend, Frontend, analytics (npm), +# contracts (cargo), and GitHub Actions (when workflows are added). +# Weekly cadence + grouped minor/patch PRs keeps churn low. +# All opened PRs are tagged with 'dependencies' + 'automation' per spec. +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/Backend" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "automation" + groups: + backend-minor-and-patch: + applies-to: version-updates + patterns: ["*"] + update-types: ["minor", "patch"] + + - package-ecosystem: "npm" + directory: "/Frontend" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "automation" + groups: + frontend-minor-and-patch: + applies-to: version-updates + patterns: ["*"] + update-types: ["minor", "patch"] + + - package-ecosystem: "npm" + directory: "/analytics" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "automation" + groups: + analytics-minor-and-patch: + applies-to: version-updates + patterns: ["*"] + update-types: ["minor", "patch"] + + - package-ecosystem: "cargo" + directory: "/contracts" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "automation" + groups: + contracts-minor-and-patch: + applies-to: version-updates + patterns: ["*"] + update-types: ["minor", "patch"] + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "automation" diff --git a/Backend/package-lock.json b/Backend/package-lock.json index bc96de7..f2d9a3c 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -22,6 +22,7 @@ "cookie-parser": "^1.4.7", "csrf": "^3.1.0", "ethers": "^6.15.0", + "helmet": "^8.2.0", "pg": "^8.13.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -3007,10 +3008,10 @@ } }, "node_modules/@swc/core": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz", - "integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==", - "dev": true, + "version": "1.12.11", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.11.tgz", + "integrity": "sha512-P3GM+0lqjFctcp5HhR9mOcvLSX3SptI9L1aux0Fuvgt8oH4f92rCUrkodAa0U2ktmdjcyIiG37xg2mb/dSCYSA==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3054,7 +3055,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3071,7 +3071,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3088,7 +3087,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3105,7 +3103,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3122,7 +3119,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3173,7 +3169,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3190,7 +3185,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3207,7 +3201,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3224,7 +3217,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3241,7 +3233,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -3255,14 +3246,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", - "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", - "dev": true, + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -7556,6 +7547,18 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", diff --git a/Backend/package.json b/Backend/package.json index 78f20b8..55d4896 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -39,10 +39,10 @@ "cookie-parser": "^1.4.7", "csrf": "^3.1.0", "ethers": "^6.15.0", + "helmet": "^8.2.0", "pg": "^8.13.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "sanitize-html": "^2.13.1", "typeorm": "^0.3.28" }, "devDependencies": { diff --git a/Backend/src/app.module.ts b/Backend/src/app.module.ts index dbd63c6..1896cfc 100644 --- a/Backend/src/app.module.ts +++ b/Backend/src/app.module.ts @@ -17,6 +17,11 @@ import { AppService } from './app.service'; ThrottlerModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], + // Issue #16: NestJS ThrottlerModule tracks by `req.ip` by default, + // giving us per-IP rate limiting out of the box. Global ThrottlerGuard + // (APP_GUARD below) applies the default. Per-route differentiation + // is handled by controllers using @Throttle / @SkipThrottle + // (e.g., GistsController). useFactory: (config: ConfigService) => [ { ttl: config.get('THROTTLE_TTL_MS', 60000), diff --git a/Backend/src/config/configuration.ts b/Backend/src/config/configuration.ts index e2b43f3..73a49f0 100644 --- a/Backend/src/config/configuration.ts +++ b/Backend/src/config/configuration.ts @@ -2,12 +2,22 @@ export default () => ({ port: parseInt(process.env.PORT ?? '3000', 10), nodeEnv: process.env.NODE_ENV ?? 'development', + // Issue #2: do NOT default credentials. They must be provided via env. + // Failing loud here prevents secret leakage via a hardcoded fallback. database: { host: process.env.DATABASE_HOST ?? 'localhost', port: parseInt(process.env.DATABASE_PORT ?? '5432', 10), - user: process.env.DATABASE_USER ?? 'gist', - password: process.env.DATABASE_PASSWORD ?? 'gist', - name: process.env.DATABASE_NAME ?? 'gist', + user: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + name: process.env.DATABASE_NAME, + }, + + // Issue #16: per-IP rate limiting differentiation. + // THROTTLE_TTL_MS / THROTTLE_LIMIT are consumed by AppModule's + // ThrottlerModule.forRootAsync — keep keys flat (process.env flood). + throttle: { + ttlMs: parseInt(process.env.THROTTLE_TTL_MS ?? '60000', 10), + limit: parseInt(process.env.THROTTLE_LIMIT ?? '10', 10), }, soroban: { diff --git a/Backend/src/database/data-source.ts b/Backend/src/database/data-source.ts index 5fe3991..5fea05c 100644 --- a/Backend/src/database/data-source.ts +++ b/Backend/src/database/data-source.ts @@ -8,13 +8,15 @@ import 'dotenv/config'; import { DataSource } from 'typeorm'; import { Gist } from '../gists/entities/gist.entity'; +// Issue #2: removed hardcoded 'gist' default for credentials. +// TypeORM CLI commands will fail loud if these env vars are missing. const AppDataSource = new DataSource({ type: 'postgres', host: process.env.DATABASE_HOST ?? 'localhost', port: Number(process.env.DATABASE_PORT ?? 5432), - username: process.env.DATABASE_USER ?? 'gist', - password: process.env.DATABASE_PASSWORD ?? 'gist', - database: process.env.DATABASE_NAME ?? 'gist', + username: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, entities: [Gist], migrations: [__dirname + '/migrations/*{.ts,.js}'], synchronize: false, diff --git a/Backend/src/database/database.module.ts b/Backend/src/database/database.module.ts index f95834d..5c37be5 100644 --- a/Backend/src/database/database.module.ts +++ b/Backend/src/database/database.module.ts @@ -12,9 +12,11 @@ import { Gist } from '../gists/entities/gist.entity'; type: 'postgres', host: config.get('DATABASE_HOST', 'localhost'), port: config.get('DATABASE_PORT', 5432), - username: config.get('DATABASE_USER', 'gist'), - password: config.get('DATABASE_PASSWORD', 'gist'), - database: config.get('DATABASE_NAME', 'gist'), + // Issue #2: no hardcoded credentials. Missing env fails + // loud at boot via the underlying pg driver. + username: config.get('DATABASE_USER'), + password: config.get('DATABASE_PASSWORD'), + database: config.get('DATABASE_NAME'), entities: [Gist], migrations: [__dirname + '/migrations/*{.ts,.js}'], migrationsRun: false, diff --git a/Backend/src/main.ts b/Backend/src/main.ts index 52d287d..9e20554 100644 --- a/Backend/src/main.ts +++ b/Backend/src/main.ts @@ -2,6 +2,8 @@ import 'reflect-metadata'; import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import helmet from 'helmet'; import { AppModule } from './app.module'; import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; import { @@ -12,7 +14,37 @@ import { import { compressionMiddleware } from './common/middleware/compression.middleware'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + // Issue #2: preflight — required DB env vars must be set; no silent + // hardcoded fallback. Fails loud at boot with an actionable message. + const requiredDbEnvVars = ['DATABASE_USER', 'DATABASE_PASSWORD', 'DATABASE_NAME']; + for (const key of requiredDbEnvVars) { + if (!process.env[key]) { + throw new Error( + `Missing required environment variable: ${key}. ` + + `Set it in Backend/.env or your environment (see Backend/.env.example).`, + ); + } + } + + const app = await NestFactory.create(AppModule); + + // Issue #16 — Trust X-Forwarded-For so per-IP rate limiting works + // behind reverse proxies (ALB, nginx, Cloudflare). Defaults to + // 'loopback' for local/dev; override via TRUST_PROXY env var + // (e.g. 'true', 'false', or a comma-separated list of CIDRs). + // MUST be set before the ThrottlerGuard binds `req.ip`. + const trustProxy = process.env.TRUST_PROXY ?? 'loopback'; + app.set('trust proxy', trustProxy); + + // Issue #11 — Helmet HTTP security headers. + // Applied before any other middleware so every response carries them. + app.use(helmet()); + + // Issue #15 — Request body size limit (default 100kb). + // Override with MAX_BODY_SIZE env var if needed. + const bodyLimit = process.env.MAX_BODY_SIZE ?? '100kb'; + app.useBodyParser('json', { limit: bodyLimit }); + app.useBodyParser('urlencoded', { limit: bodyLimit, extended: true }); // Issue 43 — Response compression. // Registered first so the middleware wraps `res.write` / `res.end` diff --git a/Backend/tsconfig.json b/Backend/tsconfig.json index 8d24017..26d5f83 100644 --- a/Backend/tsconfig.json +++ b/Backend/tsconfig.json @@ -12,11 +12,16 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + // Issue #22: enable strict TS checks (low-risk subset to avoid + // breaking NestJS DI bootstrapping in a single batch PR). + // strictPropertyInitialization stays off for now — many NestJS + // services rely on constructor injection without explicit `!`. + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "strictPropertyInitialization": false }, "ts-node": { "require": ["tsconfig-paths/register"], diff --git a/infrastructure/ci/dependency-updates.yml b/infrastructure/ci/dependency-updates.yml index 25a04ce..db52df6 100644 --- a/infrastructure/ci/dependency-updates.yml +++ b/infrastructure/ci/dependency-updates.yml @@ -1,8 +1,11 @@ -name: Dependency Update Automation +name: Dependency Update Automation (deprecated) +# Issue #34: This workflow is superseded by Dependabot at `.github/dependabot.yml`. +# Both would run on the same weekly cadence, producing duplicate PRs. +# Disabled by default; manual `workflow_dispatch` is retained for emergencies +# (e.g. when Dependabot is paused or fails for a given ecosystem). on: - schedule: - - cron: "0 6 * * 1" + schedule: [] workflow_dispatch: permissions: