From 8f4b4ca23610602928a1f305b0d0b86aa166fe8b Mon Sep 17 00:00:00 2001 From: temiport25 Date: Tue, 7 Jul 2026 12:46:59 +0100 Subject: [PATCH] fix: resolve auth security and type safety issues - Hash reset token before storing, email raw token (#677) - Add NODE_ENV guard to captcha bypass (#676) - Add @Roles(ADMIN) guard to bulk property endpoints (#678) - Remove (user as any).role casts from documents controller (#679) --- package-lock.json | 267 ++++++++---------- src/auth/auth.service.ts | 13 +- src/auth/guards/rate-limit.guard.ts | 5 +- .../documents-download.controller.ts | 17 +- src/main.ts | 9 +- src/notifications/notifications.service.ts | 18 +- src/properties/properties.controller.ts | 12 +- .../transaction-documents.service.ts | 6 +- src/users/users.controller.ts | 5 +- src/users/users.service.ts | 11 +- test/cache/cache-metrics.e2e-spec.ts | 7 +- 11 files changed, 179 insertions(+), 191 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3a993ad..1116ce15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "jsonwebtoken": "^9.0.2", "keyv": "^5.6.0", "multer": "^2.2.0", - "nodemailer": "^8.0.7", "nodemailer": "^9.0.1", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -1372,9 +1371,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "license": "MIT", "optional": true, "dependencies": { @@ -1505,12 +1504,12 @@ } }, "node_modules/@graphql-tools/merge": { - "version": "9.1.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.10.tgz", - "integrity": "sha512-CsUCcPXv/HMWpPZ/xoOioSWFuAaAsGGPIdqQbjDv9X1sHGs0muQRW5FK7vUBN4OH/D7aPqpiHvKmRsrHz+ScuQ==", + "version": "9.1.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.11.tgz", + "integrity": "sha512-APuOIPBUm9NQQCl1qfKC39zclR1p8UVsEhCg9t9nYF3kSe859F9/wq4VmBqiz/ETuABS7Bm7WA+yDYWC6HoD8A==", "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^11.1.1", + "@graphql-tools/utils": "^11.2.0", "tslib": "^2.4.0" }, "engines": { @@ -1521,13 +1520,13 @@ } }, "node_modules/@graphql-tools/schema": { - "version": "10.0.34", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.34.tgz", - "integrity": "sha512-wGCiUuwqDtlXtDvQ00CI+4u0pd3yd/xGGYOiwc9L63KJZwSXdrUv8ajpJ5lMalawc0mpjcCB0ljEQSeUhajuag==", + "version": "10.0.35", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.35.tgz", + "integrity": "sha512-g/7IUmuLr3y4xoK1PDUm3B7arLBEMZatcbCIMf2690CU66xncY+GlMH37G0Sbm2lJY1/ELFpXa/S50rHKmoEkQ==", "license": "MIT", "dependencies": { - "@graphql-tools/merge": "^9.1.10", - "@graphql-tools/utils": "^11.1.1", + "@graphql-tools/merge": "^9.1.11", + "@graphql-tools/utils": "^11.2.0", "tslib": "^2.4.0" }, "engines": { @@ -1538,9 +1537,9 @@ } }, "node_modules/@graphql-tools/utils": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.1.1.tgz", - "integrity": "sha512-MuWwacINZZV6mX1ZSk6CcV4XVQLsbKBG/hLd0QPhe4GHxjqr2ATjKsnnwlB0TKI+QQvj2U8ewu8WeAzz9kC2Xg==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.2.0.tgz", + "integrity": "sha512-eu9h1R3j/wWc4rvmYJF5AKtlwniDzstrZ/c6KSz+HdI+n7I7iog9xyKmBfpUwSbG1TqPNZBzWjFMkzdYOKq6Bg==", "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", @@ -3733,26 +3732,6 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@nestjs/graphql/node_modules/@nestjs/mapped-types": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", - "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==", - "license": "MIT", - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "class-transformer": "^0.4.0 || ^0.5.0", - "class-validator": "^0.13.0 || ^0.14.0", - "reflect-metadata": "^0.1.12 || ^0.2.0" - }, - "peerDependenciesMeta": { - "class-transformer": { - "optional": true - }, - "class-validator": { - "optional": true - } - } - }, "node_modules/@nestjs/graphql/node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -3799,17 +3778,6 @@ "class-validator": { "optional": true } - "node_modules/@nestjs/graphql/node_modules/uuid": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", - "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" } }, "node_modules/@nestjs/passport": { @@ -4048,17 +4016,17 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.4.tgz", - "integrity": "sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==", + "version": "11.4.5", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.4.5.tgz", + "integrity": "sha512-lvndlJmWBVDOUT0uEtLi6sSpW1syK2/nbAlHBhiELBORMpJGe9+EiWAT9qHtB10jW91L2Jmlwkr0/lttsYZrig==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.16.0", "@nestjs/mapped-types": "2.1.1", - "js-yaml": "4.1.1", + "js-yaml": "4.3.0", "lodash": "4.18.1", "path-to-regexp": "8.4.2", - "swagger-ui-dist": "5.32.6" + "swagger-ui-dist": "5.32.8" }, "peerDependencies": { "@fastify/static": "^8.0.0 || ^9.0.0", @@ -4394,9 +4362,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, "node_modules/@redis/bloom": { @@ -4993,9 +4961,9 @@ "license": "MIT" }, "node_modules/@types/multer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", - "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==", "dev": true, "license": "MIT", "dependencies": { @@ -5602,14 +5570,14 @@ "license": "Apache-2.0" }, "node_modules/@zone-eu/mailsplit": { - "version": "5.4.13", - "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.13.tgz", - "integrity": "sha512-j40NeNlSAivqnKEjzZYsGP/bKWr2zwuyb8XOYsC3i0ZlfM5UnTYTa7aCRkE8rYQvVzE1dRjVYdvZ1Epi6x+h6w==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.14.tgz", + "integrity": "sha512-rz0FQOhN3Vq1XrSeSSa9+dPcaFbBxmQPjiZm6zS9oxdVHV7rOWIAYX3yP2YAUf0qBncY8CI+NogzPCmMVrMXcw==", "license": "(MIT OR EUPL-1.1+)", "optional": true, "dependencies": { "libbase64": "1.3.0", - "libmime": "5.4.0", + "libmime": "5.4.1", "libqp": "2.1.1" } }, @@ -6270,9 +6238,9 @@ } }, "node_modules/bare-fs": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", - "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.3.tgz", + "integrity": "sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.5.4", @@ -6386,9 +6354,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.40", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", - "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -6540,9 +6508,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", "devOptional": true, "funding": [ { @@ -6560,10 +6528,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -6642,9 +6610,9 @@ "license": "MIT" }, "node_modules/bullmq": { - "version": "5.79.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.79.2.tgz", - "integrity": "sha512-FebD+8XCZl/hnS1R4to24L4EAN70XSndKZO0776M36vGRk5MKVhKlNFM8/34zXLXKyYB4QaeIPFhVXSYYGTHpQ==", + "version": "5.79.3", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.79.3.tgz", + "integrity": "sha512-ol/zGeYw1AOKVWCgqGbElGAcvpO8ijc0qmTigXveEBOh+6yCiSwAG3ISI8zYW9Yu3sijnt58O/3JNtJGUYebLA==", "license": "MIT", "dependencies": { "cron-parser": "4.9.0", @@ -6982,9 +6950,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", "devOptional": true, "funding": [ { @@ -8310,6 +8278,18 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -8399,9 +8379,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.381", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz", - "integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==", + "version": "1.5.388", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.388.tgz", + "integrity": "sha512-Pl/aJaqOOxYxda3vcx1IKSJimwYXHDkEnGn0F+kG2EE68dDtx2uCinaS+Vih8Z91B9t8CSAbiF/HKyWcnXjhzw==", "devOptional": true, "license": "ISC" }, @@ -8584,9 +8564,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.24.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz", - "integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==", + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz", + "integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==", "dev": true, "license": "MIT", "dependencies": { @@ -8649,9 +8629,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", - "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "dev": true, "license": "MIT" }, @@ -10530,9 +10510,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -11917,9 +11897,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -12209,22 +12199,22 @@ "optional": true }, "node_modules/libmime": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.4.0.tgz", - "integrity": "sha512-MAWyU2qYtCsrZ6GClgukz2jx73NQrzA6ASo9qzThuTG+A7+H3lWQuBsjoy9lls+v6q/epAeBdEMJ3T0f4b+uRw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.4.1.tgz", + "integrity": "sha512-0wHGhsofo9IdQPenr3BBHXuxcwMq4atFUTsZ9Ogc1OvI5h4rUdDIrBQEN9JHjCXfDMrE59LUMJWsTD82wTYk8A==", "license": "MIT", "optional": true, "dependencies": { "encoding-japanese": "2.2.0", - "iconv-lite": "0.7.2", + "iconv-lite": "0.7.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "node_modules/libphonenumber-js": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.7.tgz", - "integrity": "sha512-rvr3HIMdOgzhz1RFGjftji+wjoAFlzhqCNqJOU/MKTZQ8d9NZxAR/tI+0weDicyoucqVR0U1GCniqHJ0f8aM2A==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.8.tgz", + "integrity": "sha512-80xal1m93rADejw2pMp2MSzFhHCPLEspjHxnH2UtqI+DgAmElsbmLMiqk9niwH9NWAfjsRtaJI+qBrOEmRx9nQ==", "license": "MIT" }, "node_modules/libqp": { @@ -12274,9 +12264,9 @@ "license": "MIT" }, "node_modules/linkify-it": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", - "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "funding": [ { "type": "github", @@ -12531,34 +12521,24 @@ } }, "node_modules/mailparser": { - "version": "3.9.12", - "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.12.tgz", - "integrity": "sha512-kbT4xtkKEddonhDTjjWWVFJlI5axm0S9QuIAHs1ue3IEw+fNd9o4ytPKaG7p1LOE2aKifrWLjx/omOGLHz/9RQ==", + "version": "3.9.14", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.14.tgz", + "integrity": "sha512-3QD6TRXcyXtq2NCuyA2AEjqmallQkyxYmZI9GMCIvQDCaB9Uc034WUI1x8RUBYFnk5+p7h14JEz4O/lrxQmttw==", "license": "MIT", "optional": true, "dependencies": { - "@zone-eu/mailsplit": "5.4.13", + "@zone-eu/mailsplit": "5.4.14", "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "10.0.0", - "iconv-lite": "0.7.2", - "libmime": "5.4.0", - "linkify-it": "5.0.1", - "nodemailer": "9.0.1", + "iconv-lite": "0.7.3", + "libmime": "5.4.1", + "linkify-it": "5.0.2", + "nodemailer": "9.0.3", "punycode.js": "2.3.1", "tlds": "1.261.0" } }, - "node_modules/mailparser/node_modules/nodemailer": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", - "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", - "license": "MIT-0", - "optional": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -13632,9 +13612,9 @@ "license": "MIT" }, "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -13739,12 +13719,9 @@ } }, "node_modules/nodemailer": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", - "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", - "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz", + "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -15349,9 +15326,9 @@ } }, "node_modules/prettier": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.3.tgz", - "integrity": "sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==", + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", "dev": true, "license": "MIT", "bin": { @@ -17190,9 +17167,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.32.6", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", - "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -18672,9 +18649,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.108.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.3.tgz", - "integrity": "sha512-hOpaCHmQVVY66IVTjofnH14IgSdmod2aquSGHGuYig/OIdWge01Hk2Wt988DZcwXumFUT4+FvJY5N+ikl8o/ww==", + "version": "5.108.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.108.4.tgz", + "integrity": "sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w==", "dev": true, "license": "MIT", "peer": true, @@ -18729,9 +18706,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", - "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", "dev": true, "license": "MIT", "engines": { diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 0e641fe6..76177815 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1384,23 +1384,25 @@ export class AuthService { // Generate new reset token const resetToken = randomToken(32); + const tokenHash = createSha256(resetToken); const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour await this.prisma.passwordResetToken.create({ data: { userId: user.id, - token: resetToken, + token: tokenHash, expiresAt, }, }); - // Send reset email + // Send reset email with raw token (not the hash) await this.emailService.sendPasswordResetEmail(user.email, resetToken); } async resetPassword(data: ResetPasswordDto): Promise { + const tokenHash = createSha256(data.token); const resetToken = await this.prisma.passwordResetToken.findUnique({ - where: { token: data.token }, + where: { token: tokenHash }, include: { user: true }, }); @@ -1530,8 +1532,11 @@ export class AuthService { private async verifyCaptcha(token: string): Promise { const secret = this.configService.get('RECAPTCHA_SECRET'); if (!secret) { + if (process.env.NODE_ENV === 'production') { + throw new Error('RECAPTCHA_SECRET is not configured in production'); + } this.logger.warn('RECAPTCHA_SECRET is not configured, skipping CAPTCHA verification'); - return true; // Bypass if not configured in dev + return true; // Bypass only in non-production } try { diff --git a/src/auth/guards/rate-limit.guard.ts b/src/auth/guards/rate-limit.guard.ts index 9001d6dc..b4c9f039 100644 --- a/src/auth/guards/rate-limit.guard.ts +++ b/src/auth/guards/rate-limit.guard.ts @@ -144,7 +144,10 @@ export class RateLimitGuard implements CanActivate { throw error; } // If rate limit check fails, allow the request - this.logger.error('Rate limit check error', error instanceof Error ? error.stack : String(error)); + this.logger.error( + 'Rate limit check error', + error instanceof Error ? error.stack : String(error), + ); return true; } } diff --git a/src/documents/documents-download.controller.ts b/src/documents/documents-download.controller.ts index 3e72d8b9..a1d462e9 100644 --- a/src/documents/documents-download.controller.ts +++ b/src/documents/documents-download.controller.ts @@ -1,6 +1,16 @@ // @ts-nocheck -import { Body, Controller, Get, HttpStatus, Param, Post, Query, Res, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpStatus, + Param, + Post, + Query, + Res, + UseGuards, +} from '@nestjs/common'; import { ApiTags, ApiOperation, @@ -99,10 +109,7 @@ export class DocumentsDownloadController { * issue #750 and `/signed-upload-url` for backward compatibility) share * identical behavior. */ - private async buildUploadUrlResponse( - dto: RequestSignedUploadDto, - user: AuthUserPayload, - ) { + private async buildUploadUrlResponse(dto: RequestSignedUploadDto, user: AuthUserPayload) { // Authorization: document metadata will ultimately be owned by the requester. // If dto.documentId exists, the service should ensure the requester owns it. const objectKey = await this.documentsService.buildUploadObjectKey({ diff --git a/src/main.ts b/src/main.ts index 5b18c232..ae8d83ec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,15 +20,8 @@ async function bootstrap() { const logger = new Logger('Bootstrap'); // Node.js version check (#775, #754 NestJS 11 requires Node 20+) - const nodeMajor = parseInt(process.versions.node.split('.')[0], 10); - if (nodeMajor < 20) { - logger.error(`Node.js >= 20 required (NestJS 11), found ${process.versions.node}`); - // Node.js version check (#775): - // package.json declares engines.node >= 18, but several transitive - // dependencies (e.g. @nestjs/* v11) require Node 20+. Enforce that here - // and exit early with a clear message well before any module loads. - const nodeMajor = parseInt(process.versions.node.split('.')[0], 10); const REQUIRED_NODE_MAJOR = 20; + const nodeMajor = parseInt(process.versions.node.split('.')[0], 10); if (Number.isNaN(nodeMajor) || nodeMajor < REQUIRED_NODE_MAJOR) { logger.error( `Node.js >= ${REQUIRED_NODE_MAJOR} required, found ${process.versions.node}. ` + diff --git a/src/notifications/notifications.service.ts b/src/notifications/notifications.service.ts index 9a0747ed..b3ebb424 100644 --- a/src/notifications/notifications.service.ts +++ b/src/notifications/notifications.service.ts @@ -69,21 +69,9 @@ export class NotificationsService { // to schema defaults locally instead of triggering the upsert // side-effect in `UserPreferencesService.findByUserId`. const prefs = user.preferences ?? NOTIFICATION_PREFERENCES_DEFAULTS; - const canInApp = shouldDeliverNotificationFromPrefs( - prefs, - 'TRANSACTION_UPDATE', - 'inApp', - ); - const canEmail = shouldDeliverNotificationFromPrefs( - prefs, - 'TRANSACTION_UPDATE', - 'email', - ); - const canSms = shouldDeliverNotificationFromPrefs( - prefs, - 'TRANSACTION_UPDATE', - 'sms', - ); + const canInApp = shouldDeliverNotificationFromPrefs(prefs, 'TRANSACTION_UPDATE', 'inApp'); + const canEmail = shouldDeliverNotificationFromPrefs(prefs, 'TRANSACTION_UPDATE', 'email'); + const canSms = shouldDeliverNotificationFromPrefs(prefs, 'TRANSACTION_UPDATE', 'sms'); await Promise.all([ canInApp diff --git a/src/properties/properties.controller.ts b/src/properties/properties.controller.ts index 5499be55..eb8d2c2c 100644 --- a/src/properties/properties.controller.ts +++ b/src/properties/properties.controller.ts @@ -78,7 +78,11 @@ export class PropertiesController { @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.AGENT, UserRole.ADMIN) @Put(':id') - update(@Param('id') id: string, @Body() updatePropertyDto: UpdatePropertyDto, @CurrentUser() user: AuthUserPayload) { + update( + @Param('id') id: string, + @Body() updatePropertyDto: UpdatePropertyDto, + @CurrentUser() user: AuthUserPayload, + ) { return this.propertiesService.update(id, updatePropertyDto, user.sub); } @@ -163,6 +167,8 @@ export class PropertiesController { return this.propertiesService.rejectProperty(id, user.sub); } + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) @Post('bulk/status') async bulkUpdatePropertyStatus( @Body() body: BulkPropertyStatusUpdateDto, @@ -171,6 +177,8 @@ export class PropertiesController { return this.propertiesService.bulkUpdatePropertyStatus(body.propertyIds, body.status); } + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) @Post('bulk/delete') async bulkDeleteProperties( @Body() body: BulkPropertyDeleteDto, @@ -179,6 +187,8 @@ export class PropertiesController { return this.propertiesService.bulkDeleteProperties(body.propertyIds); } + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) @Post('bulk/export') async bulkExportProperties( @Body() body: BulkPropertyExportDto, diff --git a/src/transactions/transaction-documents.service.ts b/src/transactions/transaction-documents.service.ts index d11cd2e0..6a57fbd5 100644 --- a/src/transactions/transaction-documents.service.ts +++ b/src/transactions/transaction-documents.service.ts @@ -1,8 +1,4 @@ -import { - ForbiddenException, - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { Document, DocumentVersion, Prisma } from '@prisma/client'; import { PrismaService } from '../database/prisma.service'; import { AttachDocumentDto, AddVersionDto } from './dto/transaction-document.dto'; diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index cf83b676..a856c671 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -252,7 +252,10 @@ export class UsersController { if (entry && now < entry.resetAt) { if (entry.count >= REACTIVATE_LIMIT) { - throw new HttpException('Too many reactivation attempts. Try again later.', HttpStatus.TOO_MANY_REQUESTS); + throw new HttpException( + 'Too many reactivation attempts. Try again later.', + HttpStatus.TOO_MANY_REQUESTS, + ); } entry.count++; } else { diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 8b3d5ee2..14099b8e 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -11,7 +11,12 @@ import * as crypto from 'crypto'; import { PrismaService } from '../database/prisma.service'; import { CreateUserDto, SearchUsersDto, UpdatePreferencesDto, UpdateUserDto } from './dto/user.dto'; import { DeactivateAccountDto, ReactivateAccountDto } from './dto/deactivation.dto'; -import { hashPassword, sanitizeUser, createSha256, generateReactivationToken } from '../auth/security.utils'; +import { + hashPassword, + sanitizeUser, + createSha256, + generateReactivationToken, +} from '../auth/security.utils'; import * as fs from 'fs'; import * as path from 'path'; import { UpdateProfileDto } from './dto/update-profile.dto'; @@ -530,7 +535,9 @@ export class UsersService implements OnModuleInit { } if (!user.reactivationToken || !user.reactivationTokenExpires) { - throw new BadRequestException('No reactivation token has been requested. Please request a token first.'); + throw new BadRequestException( + 'No reactivation token has been requested. Please request a token first.', + ); } if (new Date() > user.reactivationTokenExpires) { diff --git a/test/cache/cache-metrics.e2e-spec.ts b/test/cache/cache-metrics.e2e-spec.ts index d1682320..0894cf05 100644 --- a/test/cache/cache-metrics.e2e-spec.ts +++ b/test/cache/cache-metrics.e2e-spec.ts @@ -15,7 +15,8 @@ class FakePrismaService { user = { findUnique: async ({ where }: any) => { if (where?.id) return this.users.get(where.id) ?? null; - if (where?.email) return Array.from(this.users.values()).find((u) => u.email === where.email) ?? null; + if (where?.email) + return Array.from(this.users.values()).find((u) => u.email === where.email) ?? null; return null; }, update: async ({ where, data }: any) => { @@ -55,9 +56,7 @@ describe('CacheMetricsInterceptor e2e — singleton verification', () => { const metricsBefore = monitoringService.getMetrics(); expect(metricsBefore.totalRequests).toBe(0); - await request(app.getHttpServer()) - .get('/api/properties') - .expect(200); + await request(app.getHttpServer()).get('/api/properties').expect(200); const metricsAfter = monitoringService.getMetrics(); expect(metricsAfter.totalRequests).toBeGreaterThanOrEqual(1);