diff --git a/.gitignore b/.gitignore index de625e8..104d867 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,6 @@ coverage .env .env.local .env.*.local +.env.* /src/generated/prisma diff --git a/docker-compose.yml b/docker-compose.yml index 616055f..57867f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,10 @@ name: coverit services: api: image: ghcr.io/coveritlabs/coverit-api:${API_TAG:-dev} + build: + context: ${API_DIR} + secrets: + - npm_token pull_policy: always container_name: coverit-api ports: @@ -14,9 +18,9 @@ services: environment: - NODE_ENV=${NODE_ENV:-development} - PORT=3000 - - DATABASE_URL=postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@db:5432/${DB_NAME:-coverit}?schema=public - - REDIS_URL=redis://redis:6379 - - JWT_SECRET=${JWT_SECRET:-change-me-in-production} + - DATABASE_URL=${DATABASE_URL:-} + - REDIS_URL=${REDIS_URL:-} + - JWT_SECRET=${JWT_SECRET:-} - JWT_ACCESS_EXPIRY=${JWT_ACCESS_EXPIRY:-15m} - JWT_REFRESH_EXPIRY_SECONDS=${JWT_REFRESH_EXPIRY_SECONDS:-604800} - RESET_TOKEN_TTL_SECONDS=${RESET_TOKEN_TTL_SECONDS:-900} @@ -28,6 +32,7 @@ services: - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-} - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-} - GITHUB_CALLBACK_URL=${GITHUB_CALLBACK_URL:-http://localhost:3000/api/v1/auth/oauth/github/callback} + - NODE_TLS_REJECT_UNAUTHORIZED=0 restart: unless-stopped depends_on: db: @@ -66,6 +71,10 @@ services: retries: 5 restart: unless-stopped +secrets: + npm_token: + environment: GITHUB_TOKEN + volumes: postgres_data: redis_data: diff --git a/jest.config.js b/jest.config.js index 8119fa5..d39db9a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,6 +10,7 @@ module.exports = { testMatch: ['**/__tests__/**/*.test.ts'], moduleFileExtensions: ['ts', 'js', 'json'], moduleNameMapper: { + '^ioredis$': '/src/__tests__/mocks/ioredis.ts', '^@api/(.*)$': '/src/api/$1', '^@config/(.*)$': '/src/config/$1', '^@lib/(.*)$': '/src/lib/$1', diff --git a/overrides/api.cloud.yml b/overrides/api.cloud.yml index 60ee65c..11b4672 100644 --- a/overrides/api.cloud.yml +++ b/overrides/api.cloud.yml @@ -11,9 +11,4 @@ services: api: - environment: - - NODE_ENV=production - - DATABASE_URL=${DATABASE_URL} - - REDIS_URL=${REDIS_URL} - - JWT_SECRET=${JWT_SECRET} depends_on: [] diff --git a/overrides/api.dev.yml b/overrides/api.dev.yml index f999e02..d5072f3 100644 --- a/overrides/api.dev.yml +++ b/overrides/api.dev.yml @@ -10,21 +10,10 @@ services: api: image: coverit-api-dev-local build: - context: ${API_DIR} dockerfile: Dockerfile.dev - secrets: - - npm_token volumes: - ${API_DIR}/src:/app/src - ${API_DIR}/prisma:/app/prisma - ${API_DIR}/prisma.config.ts:/app/prisma.config.ts - ${API_DIR}/nodemon.json:/app/nodemon.json - - ${API_DIR}/tsconfig.json:/app/tsconfig.json - environment: - - NODE_ENV=development - - DATABASE_URL=postgresql://postgres:postgres@db:5432/coverit_dev?schema=public - - JWT_SECRET=dev-secret-do-not-use-in-production - -secrets: - npm_token: - environment: GITHUB_TOKEN + - ${API_DIR}/tsconfig.json:/app/tsconfig.json \ No newline at end of file diff --git a/overrides/api.test.yml b/overrides/api.test.yml index f9c5fa9..3cc4d1d 100644 --- a/overrides/api.test.yml +++ b/overrides/api.test.yml @@ -10,11 +10,4 @@ services: api: image: coverit-api-test-local build: - context: ${API_DIR} - dockerfile: Dockerfile - secrets: - - npm_token - -secrets: - npm_token: - environment: GITHUB_TOKEN + dockerfile: Dockerfile \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 19c4732..ff92d4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,13 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "helmet": "^7.1.0", + "http-status-codes": "^2.3.0", "ioredis": "^5.10.0", "jsonwebtoken": "^9.0.3", "pg": "^8.20.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", + "pino-pretty": "^13.1.3", "prisma": "^7.4.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", @@ -2572,6 +2576,12 @@ "node": ">=10" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3961,6 +3971,15 @@ "node": ">= 4.0.0" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -4794,6 +4813,12 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5289,6 +5314,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5594,6 +5628,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -6168,6 +6211,12 @@ ], "license": "MIT" }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6223,7 +6272,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, "license": "MIT" }, "node_modules/fast-uri": { @@ -6560,7 +6608,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6942,6 +6989,12 @@ "node": ">=16.0.0" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -9286,6 +9339,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9930,7 +9992,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10244,6 +10305,15 @@ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -10739,6 +10809,91 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^10.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -10966,6 +11121,22 @@ } } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -10997,6 +11168,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11070,6 +11251,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -11162,6 +11349,15 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -11363,6 +11559,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11375,6 +11580,22 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -11574,6 +11795,15 @@ "node": ">=8" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11959,6 +12189,18 @@ "node": ">=8" } }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index 88f2f85..6648964 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,14 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", - "http-status-codes": "^2.3.0", "helmet": "^7.1.0", + "http-status-codes": "^2.3.0", "ioredis": "^5.10.0", "jsonwebtoken": "^9.0.3", "pg": "^8.20.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", + "pino-pretty": "^13.1.3", "prisma": "^7.4.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", diff --git a/src/__tests__/middlewares/requireAuth.test.ts b/src/__tests__/middlewares/requireAuth.test.ts index ba1b31f..d94cd84 100644 --- a/src/__tests__/middlewares/requireAuth.test.ts +++ b/src/__tests__/middlewares/requireAuth.test.ts @@ -25,6 +25,7 @@ jest.mock('@config/env', () => ({ import app from '../../app'; import express from 'express'; +import { logger } from '@services/logger.service'; import { requireAuth } from '@api/middlewares/requireAuth'; import { errorHandler } from '@api/middlewares/errorHandler'; @@ -102,14 +103,14 @@ describe('requireAuth middleware', () => { }); describe('errorHandler middleware', () => { - let consoleErrorSpy: jest.SpyInstance; + let loggerErrorSpy: jest.SpyInstance; beforeEach(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); + loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation(() => undefined); }); afterEach(() => { - consoleErrorSpy.mockRestore(); + loggerErrorSpy.mockRestore(); }); it('should return 500 for unknown errors', async () => { @@ -124,6 +125,6 @@ describe('errorHandler middleware', () => { expect(res.status).toBe(500); expect(res.body.message).toBe('Internal server error'); - expect(consoleErrorSpy).toHaveBeenCalledWith('Unhandled error:', expect.any(Error)); + expect(loggerErrorSpy).toHaveBeenCalledWith(expect.any(Error)); }); }); diff --git a/src/__tests__/mocks/ioredis.ts b/src/__tests__/mocks/ioredis.ts new file mode 100644 index 0000000..4378780 --- /dev/null +++ b/src/__tests__/mocks/ioredis.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2026 CoverIt Labs. All Rights Reserved. +// Proprietary and confidential. Unauthorized use is strictly prohibited. +// See LICENSE file in the project root for full license information. + +// Manual mock for the `ioredis` package used by the runtime code. +export default class MockRedis { + constructor(..._args: any[]) {} + set = jest.fn().mockResolvedValue('OK'); + get = jest.fn().mockResolvedValue(null); + del = jest.fn().mockResolvedValue(1); + scan = jest.fn().mockResolvedValue(['0', []]); + ping = jest.fn().mockResolvedValue('PONG'); + on = jest.fn(); + disconnect = jest.fn(); +} diff --git a/src/__tests__/mocks/redis.ts b/src/__tests__/mocks/redis.ts index 6a7d97e..f089ac6 100644 --- a/src/__tests__/mocks/redis.ts +++ b/src/__tests__/mocks/redis.ts @@ -9,6 +9,7 @@ const redis = { scan: jest.fn().mockResolvedValue(['0', []]), ping: jest.fn().mockResolvedValue('PONG'), on: jest.fn(), + disconnect: jest.fn(), }; export async function scanKeys(pattern: string): Promise { diff --git a/src/api/middlewares/errorHandler.ts b/src/api/middlewares/errorHandler.ts index 732b35f..5ee54eb 100644 --- a/src/api/middlewares/errorHandler.ts +++ b/src/api/middlewares/errorHandler.ts @@ -5,6 +5,7 @@ import { Request, Response, NextFunction } from 'express'; import { AppError } from '@utils/errors'; import { StatusCodes } from 'http-status-codes'; +import { logger } from "@services/logger.service"; export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction): void { if (err instanceof AppError) { @@ -12,6 +13,6 @@ export function errorHandler(err: Error, _req: Request, res: Response, _next: Ne return; } - console.error('Unhandled error:', err); + logger.error(err); res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: 'Internal server error' }); } diff --git a/src/api/middlewares/logger.ts b/src/api/middlewares/logger.ts new file mode 100644 index 0000000..d367a3f --- /dev/null +++ b/src/api/middlewares/logger.ts @@ -0,0 +1,26 @@ +// Copyright (c) 2026 CoverIt Labs. All Rights Reserved. +// Proprietary and confidential. Unauthorized use is strictly prohibited. +// See LICENSE file in the project root for full license information. + +import pinoHttp from "pino-http"; +import { logger } from "@services/logger.service"; + +export const httpLogger = pinoHttp({ + logger, + customLogLevel: (req, res, err) => { + if (res.statusCode >= 500 || err) return 'error'; + if (res.statusCode >= 400) return 'warn'; + return 'info'; + }, + serializers: { + req: (req) => ({ + method: req.method, + url: req.url, + headers: req.headers, + body: req.body, + }), + res: (res) => ({ + statusCode: res.statusCode, + }), + }, +}); \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 9101bf4..374739e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { env } from '@config/env'; import { swaggerSpec } from '@config/swagger'; import authRoutes from '@api/routes/auth.routes'; import { errorHandler } from '@api/middlewares/errorHandler'; +import { httpLogger } from '@api/middlewares/logger'; const app: Application = express(); @@ -23,6 +24,7 @@ app.use( ); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +app.use(httpLogger); app.get('/health', (_req: Request, res: Response) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); diff --git a/src/index.ts b/src/index.ts index 804488e..e89ceb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,8 @@ import redis from '@lib/redis'; import { env } from '@config/env'; async function startServer(): Promise { + console.info("Starting server with env:") + console.info(process.env) console.info('Connecting to PostgreSQL…'); try { await prisma.$connect(); diff --git a/src/services/logger.service.ts b/src/services/logger.service.ts new file mode 100644 index 0000000..ebb3537 --- /dev/null +++ b/src/services/logger.service.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2026 CoverIt Labs. All Rights Reserved. +// Proprietary and confidential. Unauthorized use is strictly prohibited. +// See LICENSE file in the project root for full license information. + +import pino from "pino"; +import pretty from "pino-pretty"; + +const stream = pretty({ + levelFirst: true, + colorize: true, + ignore: "time,hostname,pid", +}); + +export const logger = pino( + { + level: process.env.NODE_ENV === "production" ? "info" : "debug", + }, + stream, +);