diff --git a/package-lock.json b/package-lock.json index 189f53bd..60074ec9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "graphql": "^16.12.0", "graphql-subscriptions": "^3.0.0", "handlebars": "^4.7.8", + "helmet": "^8.0.0", "ioredis": "^5.9.3", "joi": "^17.13.3", "multer": "^2.0.1", @@ -2653,7 +2654,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -2666,7 +2667,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -3135,9 +3136,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3154,9 +3152,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3173,9 +3168,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3192,9 +3184,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3211,9 +3200,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3230,9 +3216,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3249,9 +3232,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3268,9 +3248,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3287,9 +3264,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3312,9 +3286,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3337,9 +3308,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3362,9 +3330,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3387,9 +3352,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3412,9 +3374,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3437,9 +3396,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3462,9 +3418,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4321,7 +4274,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -4342,7 +4295,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -6700,83 +6653,6 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@redis/bloom": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", - "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" - } - }, - "node_modules/@redis/client": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", - "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", - "license": "MIT", - "peer": true, - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@node-rs/xxhash": "^1.1.0", - "@opentelemetry/api": ">=1 <2" - }, - "peerDependenciesMeta": { - "@node-rs/xxhash": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - } - } - }, - "node_modules/@redis/json": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", - "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" - } - }, - "node_modules/@redis/search": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", - "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" - } - }, - "node_modules/@redis/time-series": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", - "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.19.0" - }, - "peerDependencies": { - "@redis/client": "^5.12.1" - } - }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -7593,28 +7469,28 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/babel__core": { @@ -8658,20 +8534,6 @@ "acorn": "^8" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -8686,7 +8548,7 @@ "version": "8.3.5", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -8903,7 +8765,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -10606,7 +10468,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cron": { @@ -10868,7 +10730,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -11196,14 +11058,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -13005,6 +12859,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -15839,7 +15702,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/makeerror": { @@ -17474,23 +17337,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/redis": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", - "integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==", - "license": "MIT", - "peer": true, - "dependencies": { - "@redis/bloom": "5.12.1", - "@redis/client": "5.12.1", - "@redis/json": "5.12.1", - "@redis/search": "5.12.1", - "@redis/time-series": "5.12.1" - }, - "engines": { - "node": ">= 18.19.0" - } - }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -19383,7 +19229,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -19797,7 +19643,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -19985,7 +19831,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -20070,55 +19916,6 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, - "node_modules/webpack": { - "version": "5.106.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", - "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.1", - "mime-db": "^1.54.0", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, "node_modules/webpack-node-externals": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", @@ -20139,53 +19936,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "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, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -20485,7 +20235,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index a9567df7..96f1e39e 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,8 @@ "stripe": "^18.3.0", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.28", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "helmet": "^8.0.0" }, "devDependencies": { "@commitlint/cli": "^19.0.0", diff --git a/src/email-marketing/analytics/email-analytics.service.ts b/src/email-marketing/analytics/email-analytics.service.ts index bf787284..e7861092 100644 --- a/src/email-marketing/analytics/email-analytics.service.ts +++ b/src/email-marketing/analytics/email-analytics.service.ts @@ -187,11 +187,24 @@ export class EmailAnalyticsService { const totalCampaigns = campaigns.length; const totalEmailsSent = campaigns.reduce((sum, c) => sum + (c.totalRecipients || 0), 0); + const campaignIds = campaigns.map((c) => c.id); + const metricsMap = await this.getBatchMetrics(campaignIds); + let totalOpenRate = 0; let totalClickRate = 0; for (const campaign of campaigns) { - const metrics = await this.getCampaignMetrics(campaign.id); + const metrics = metricsMap.get(campaign.id) || { + sent: campaign.totalRecipients || 0, + delivered: 0, + opened: 0, + clicked: 0, + bounced: 0, + unsubscribed: 0, + openRate: 0, + clickRate: 0, + bounceRate: 0, + }; totalOpenRate += metrics.openRate; totalClickRate += metrics.clickRate; } @@ -204,6 +217,95 @@ export class EmailAnalyticsService { }; } + /** + * Batch fetch metrics for multiple campaigns to prevent N+1 queries + */ + private async getBatchMetrics(campaignIds: string[]): Promise> { + if (!campaignIds.length) return new Map(); + + // Query 1: Regular counts (delivered, bounced, unsubscribed) + const regularCounts = await this.eventRepository + .createQueryBuilder('event') + .select('event.campaignId', 'campaignId') + .addSelect('event.eventType', 'eventType') + .addSelect('COUNT(*)', 'count') + .where('event.campaignId IN (:...campaignIds)', { campaignIds }) + .andWhere('event.eventType IN (:...eventTypes)', { + eventTypes: [EmailEventType.DELIVERED, EmailEventType.BOUNCED, EmailEventType.UNSUBSCRIBED], + }) + .groupBy('event.campaignId') + .addGroupBy('event.eventType') + .getRawMany(); + + // Query 2: Unique recipient counts (opened, clicked) + const uniqueCounts = await this.eventRepository + .createQueryBuilder('event') + .select('event.campaignId', 'campaignId') + .addSelect('event.eventType', 'eventType') + .addSelect('COUNT(DISTINCT event.recipientId)', 'count') + .where('event.campaignId IN (:...campaignIds)', { campaignIds }) + .andWhere('event.eventType IN (:...eventTypes)', { + eventTypes: [EmailEventType.OPENED, EmailEventType.CLICKED], + }) + .groupBy('event.campaignId') + .addGroupBy('event.eventType') + .getRawMany(); + + // Fetch campaigns to get totalRecipients + const campaigns = await this.campaignRepository.findByIds(campaignIds); + const campaignMap = new Map(campaigns.map((c) => [c.id, c])); + + const metricsMap = new Map(); + + const getMetrics = (id: string): CampaignMetrics => { + let metrics = metricsMap.get(id); + if (!metrics) { + const campaign = campaignMap.get(id); + const sent = campaign?.totalRecipients || 0; + metrics = { + sent, + delivered: 0, + opened: 0, + clicked: 0, + bounced: 0, + unsubscribed: 0, + openRate: 0, + clickRate: 0, + bounceRate: 0, + }; + metricsMap.set(id, metrics); + } + return metrics; + }; + + // Process regular counts + for (const row of regularCounts) { + const metrics = getMetrics(row.campaignId); + const count = parseInt(row.count, 10); + if (row.eventType === EmailEventType.DELIVERED) metrics.delivered = count; + if (row.eventType === EmailEventType.BOUNCED) metrics.bounced = count; + if (row.eventType === EmailEventType.UNSUBSCRIBED) metrics.unsubscribed = count; + } + + // Process unique counts + for (const row of uniqueCounts) { + const metrics = getMetrics(row.campaignId); + const count = parseInt(row.count, 10); + if (row.eventType === EmailEventType.OPENED) metrics.opened = count; + if (row.eventType === EmailEventType.CLICKED) metrics.clicked = count; + } + + // Calculate rates + for (const metrics of metricsMap.values()) { + const sent = metrics.sent; + metrics.openRate = sent > 0 ? (metrics.opened / sent) * 100 : 0; + metrics.clickRate = metrics.opened > 0 ? (metrics.clicked / metrics.opened) * 100 : 0; + metrics.bounceRate = sent > 0 ? (metrics.bounced / sent) * 100 : 0; + } + + return metricsMap; + } + // Helper methods private async countEvents(campaignId: string, eventType: EmailEventType): Promise { return this.eventRepository.count({ where: { campaignId, eventType } }); diff --git a/src/email-marketing/email-marketing.service.ts b/src/email-marketing/email-marketing.service.ts index d5779294..e6b19ac8 100644 --- a/src/email-marketing/email-marketing.service.ts +++ b/src/email-marketing/email-marketing.service.ts @@ -42,8 +42,9 @@ export class EmailMarketingService { // Validate segments exist if (createCampaignDto.segmentIds?.length) { - for (const segmentId of createCampaignDto.segmentIds) { - await this.segmentationService.findOne(segmentId); + const segments = await this.segmentationService.findByIds(createCampaignDto.segmentIds); + if (segments.length !== createCampaignDto.segmentIds.length) { + throw new NotFoundException('One or more segments not found'); } } diff --git a/src/email-marketing/segmentation/segmentation.service.ts b/src/email-marketing/segmentation/segmentation.service.ts index c8c44999..60aec6e3 100644 --- a/src/email-marketing/segmentation/segmentation.service.ts +++ b/src/email-marketing/segmentation/segmentation.service.ts @@ -76,9 +76,9 @@ export class SegmentationService { relations: ['rules'], }); - // Calculate member count for each segment + // Calculate member count for each segment using the segment object already loaded with rules for (const segment of segments) { - segment.memberCount = await this.calculateMemberCount(segment.id); + segment.memberCount = await this.calculateMemberCountForSegment(segment); } return { @@ -102,11 +102,21 @@ export class SegmentationService { throw new NotFoundException(`Segment with ID ${id} not found`); } - // Calculate member count without recursive call segment.memberCount = await this.calculateMemberCountForSegment(segment); return segment; } + /** + * Find multiple segments by IDs + */ + async findByIds(ids: string[]): Promise { + if (!ids.length) return []; + return this.segmentRepository.find({ + where: { id: In(ids) }, + relations: ['rules'], + }); + } + /** * Update a segment */ @@ -259,8 +269,8 @@ export class SegmentationService { /** * Check if a user belongs to a segment */ - async isUserInSegment(userId: string, segmentId: string): Promise { - const members = await this.getSegmentMembers(segmentId); + async isUserInSegment(userId: string, segmentOrId: Segment | string): Promise { + const members = await this.getSegmentMembers(segmentOrId); return members.some((member) => member.id === userId); } @@ -275,7 +285,7 @@ export class SegmentationService { const userSegments: Segment[] = []; for (const segment of allSegments) { - if (await this.isUserInSegment(userId, segment.id)) { + if (await this.isUserInSegment(userId, segment)) { userSegments.push(segment); } } diff --git a/src/main.ts b/src/main.ts index 028e8780..4b014843 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; import { correlationMiddleware } from './common/utils/correlation.utils'; import { sessionConfig } from './config/cache.config'; import { SESSION_REDIS_CLIENT } from './session/session.constants'; +import helmet from 'helmet'; async function bootstrapWorker() { const logger = new Logger('Bootstrap'); @@ -21,6 +22,17 @@ async function bootstrapWorker() { // Create the application with dynamic module loading const app = await NestFactory.create(await AppModule.forRoot(), { rawBody: true }); + // ─── Security Headers ───────────────────────────────────────────────────── + app.use( + helmet({ + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true, + }, + }), + ); + const redisClient = app.get(SESSION_REDIS_CLIENT); if (sessionConfig.trustProxy) { diff --git a/src/tenancy/tenancy.service.ts b/src/tenancy/tenancy.service.ts index 5981742d..9d5ef007 100644 --- a/src/tenancy/tenancy.service.ts +++ b/src/tenancy/tenancy.service.ts @@ -219,10 +219,12 @@ export class TenancyService { billing: TenantBilling; customization: TenantCustomization; }> { - const tenant = await this.findOne(tenantId); - const config = await this.getConfig(tenantId); - const billing = await this.billingService.getBillingInfo(tenantId); - const customization = await this.customizationService.getCustomization(tenantId); + const [tenant, config, billing, customization] = await Promise.all([ + this.findOne(tenantId), + this.getConfig(tenantId), + this.billingService.getBillingInfo(tenantId), + this.customizationService.getCustomization(tenantId), + ]); return { tenant,