From 9c93feee6846c28a528ef9d4da136a84acc17f29 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 8 Dec 2025 22:04:01 +0330 Subject: [PATCH] notification --- package-lock.json | 1295 ++++++++++++++++- package.json | 7 +- src/app.module.ts | 23 +- .../dto/create-notification.dto.ts | 41 + .../dto/create-preference.dto.ts | 35 + .../dto/send-notification.dto.ts | 38 + .../notification-preference.entity.ts | 24 + .../entities/notification.entity.ts | 50 + .../events/notification.events.ts | 27 + .../notification-queue.interface.ts | 19 + .../listeners/notification.listeners.ts | 84 ++ .../notifications/notifications.controller.ts | 152 ++ .../notifications/notifications.module.ts | 58 + .../processors/notification.processor.ts | 248 ++++ .../notification-preference.service.ts | 93 ++ .../services/notification-queue.service.ts | 63 + .../services/notification.service.ts | 190 +++ .../services/push-notification.service.ts | 148 ++ .../services/sms-notification.service.ts | 96 ++ 19 files changed, 2603 insertions(+), 88 deletions(-) create mode 100644 src/modules/notifications/dto/create-notification.dto.ts create mode 100644 src/modules/notifications/dto/create-preference.dto.ts create mode 100644 src/modules/notifications/dto/send-notification.dto.ts create mode 100644 src/modules/notifications/entities/notification-preference.entity.ts create mode 100644 src/modules/notifications/entities/notification.entity.ts create mode 100644 src/modules/notifications/events/notification.events.ts create mode 100644 src/modules/notifications/interfaces/notification-queue.interface.ts create mode 100644 src/modules/notifications/listeners/notification.listeners.ts create mode 100644 src/modules/notifications/notifications.controller.ts create mode 100644 src/modules/notifications/notifications.module.ts create mode 100644 src/modules/notifications/processors/notification.processor.ts create mode 100644 src/modules/notifications/services/notification-preference.service.ts create mode 100644 src/modules/notifications/services/notification-queue.service.ts create mode 100644 src/modules/notifications/services/notification.service.ts create mode 100644 src/modules/notifications/services/push-notification.service.ts create mode 100644 src/modules/notifications/services/sms-notification.service.ts diff --git a/package-lock.json b/package-lock.json index 57e7dfc..818720a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.1", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", @@ -34,14 +35,17 @@ "@nestjs/swagger": "^11.2.1", "@nestjs/throttler": "^6.4.0", "axios": "^1.13.1", + "bullmq": "^5.65.1", "cache-manager": "^7.2.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "dayjs": "^1.11.19", "fastify": "^5.6.1", + "firebase-admin": "^13.6.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "ulid": "^3.0.1" + "ulid": "^3.0.1", + "uuid": "^13.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -55,6 +59,7 @@ "@types/jest": "^30.0.0", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2", @@ -306,6 +311,19 @@ "node": "20 || >=22" } }, + "node_modules/@apollo/server/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/@apollo/usage-reporting-protobuf": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz", @@ -1416,6 +1434,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2473,6 +2492,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@fastify/accept-negotiator": "^2.0.0", "@fastify/send": "^4.0.0", @@ -2494,6 +2514,240 @@ "node": ">= 0.6" } }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", + "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0", + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.18.0.tgz", + "integrity": "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@graphql-tools/merge": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.1.tgz", @@ -2555,6 +2809,72 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.2.tgz", + "integrity": "sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2961,8 +3281,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", @@ -3673,6 +3992,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@keyv/redis": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-5.1.3.tgz", @@ -3762,6 +4092,7 @@ "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.5.8.tgz", "integrity": "sha512-0jym5Pd7AwjpzsgPvs5c1WhrqqsfD2K32UcDmsN0CVU0uXCgKKEfXWnAHsesvUBghg0pKzFhTonI2G6I1GYYUA==", "license": "MIT", + "peer": true, "dependencies": { "dataloader": "2.2.3", "dotenv": "17.2.3", @@ -3913,8 +4244,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { "version": "3.0.3", @@ -3927,8 +4257,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { "version": "3.0.3", @@ -3941,8 +4270,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { "version": "3.0.3", @@ -3955,8 +4283,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { "version": "3.0.3", @@ -3969,8 +4296,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { "version": "3.0.3", @@ -3983,8 +4309,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", @@ -4128,6 +4453,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4312,6 +4638,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.7.tgz", "integrity": "sha512-lwlObwGgIlpXSXYOTpfzdCepUyWomz6bv9qzGzzvpgspUxkj0Uz0fUJcvD44V8Ps7QhKW3lZBoYbXrH25UZrbA==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -4371,6 +4698,7 @@ "integrity": "sha512-TyXFOwjhHv/goSgJ8i20K78jwTM0iSpk9GBcC2h3mf4MxNy+znI8m7nWjfoACjTkb89cTwDQetfTHtSfGLLaiA==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -4406,6 +4734,19 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.1.tgz", + "integrity": "sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==", + "license": "MIT", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/jwt": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz", @@ -4444,6 +4785,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.7.tgz", "integrity": "sha512-5T+GLdvTiGPKB4/P4PM9ftKUKNHJy8ThEFhZA3vQnXVL7Vf0rDr07TfVTySVu+XTh85m1lpFVuyFM6u6wLNsRA==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.5", "express": "5.1.0", @@ -4465,6 +4807,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-11.1.8.tgz", "integrity": "sha512-4XiiTiTkF9UbVDAuHDyVIzgr43L2sI3vLs9A52ov2lrOJcqyKwTYL/NiCQd4dtUQm1L6M0jOrJhzpYXobX5uMw==", "license": "MIT", + "peer": true, "dependencies": { "@fastify/cors": "11.1.0", "@fastify/formbody": "8.0.2", @@ -4741,6 +5084,16 @@ "npm": ">=5.10.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", @@ -5647,6 +6000,16 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -5735,18 +6098,23 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", + "optional": true + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5765,6 +6133,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5817,7 +6186,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -5898,7 +6266,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -5912,6 +6279,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5920,21 +6288,72 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, "license": "MIT" }, + "node_modules/@types/request": { + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/request/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/request/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@types/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz", "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5944,7 +6363,6 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz", "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -5956,7 +6374,6 @@ "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -5994,6 +6411,20 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.4", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.4.tgz", @@ -6063,6 +6494,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -6738,6 +7170,19 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -6763,6 +7208,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6806,12 +7252,22 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6997,6 +7453,16 @@ "node": ">=8" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -7058,6 +7524,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -7174,7 +7641,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -7201,6 +7667,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -7294,6 +7769,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -7369,9 +7845,9 @@ "license": "MIT" }, "node_modules/bullmq": { - "version": "5.64.1", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.64.1.tgz", - "integrity": "sha512-Tg4ORit8bQ1xLwcQrEfcDpG50pS30Onuz1ZA4rPLbL9QEsOaBbvFQMSEvXSgMnvRZRqDggJoSBGz9tck+1PixQ==", + "version": "5.65.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.65.1.tgz", + "integrity": "sha512-QgDAzX1G9L5IRy4Orva5CfQTXZT+5K+OfO/kbPrAqN+pmL9LJekCzxijXehlm/u2eXfWPfWvIdJJIqiuz3WJSg==", "license": "MIT", "peer": true, "dependencies": { @@ -7384,6 +7860,19 @@ "uuid": "^11.1.0" } }, + "node_modules/bullmq/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -7409,6 +7898,7 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.4.tgz", "integrity": "sha512-skmhkqXjPCBmrb70ctEx4zwFk7vb0RdFXlVGYWnFZ8pKvkzdFrFFKSJ1IaKduGfkryHOJvb7q2PkGmonmL+UGw==", "license": "MIT", + "peer": true, "dependencies": { "@cacheable/utils": "^2.1.0", "keyv": "^5.5.3" @@ -7542,6 +8032,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -7589,13 +8080,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -7658,7 +8151,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -7673,7 +8166,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -7683,7 +8176,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7696,7 +8189,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7974,7 +8467,6 @@ "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", "license": "MIT", - "peer": true, "dependencies": { "luxon": "^3.2.1" }, @@ -8113,7 +8605,6 @@ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=0.10" } @@ -8142,7 +8633,6 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -8252,6 +8742,19 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8308,6 +8811,16 @@ "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", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -8418,6 +8931,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8478,6 +8992,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8644,6 +9159,22 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -8755,6 +9286,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -8926,6 +9472,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", @@ -9052,6 +9599,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -9185,6 +9744,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase-admin": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.0.tgz", + "integrity": "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "@types/node": "^22.8.7", + "farmhash-modern": "^1.1.0", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^9.14.2", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^11.0.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.11.0", + "@google-cloud/storage": "^7.14.0" + } + }, + "node_modules/firebase-admin/node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/firebase-admin/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -9421,6 +10025,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -9435,7 +10089,7 @@ "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, + "devOptional": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -9597,6 +10251,91 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-gax": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", + "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -9632,6 +10371,40 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -9731,6 +10504,23 @@ "integrity": "sha512-aokUX1VdTpI0DUsndvW+OiwmBpKCu/NgRsSSkuSY0zq8PY6Q6a+lmOfAFDXAAOtBqJELvcWY9L1EVtzjbQcMdg==", "license": "MIT" }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -9763,6 +10553,53 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -9897,7 +10734,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", @@ -10029,7 +10865,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10179,6 +11014,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10948,6 +11784,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10980,6 +11825,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -11092,6 +11946,47 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwks-rsa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", + "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.20", + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -11107,6 +12002,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -11279,6 +12175,11 @@ ], "license": "MIT" }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11341,12 +12242,24 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -11358,8 +12271,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", @@ -11463,6 +12375,34 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -11716,7 +12656,6 @@ "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", "license": "MIT", - "peer": true, "optionalDependencies": { "msgpackr-extract": "^3.0.2" } @@ -11728,7 +12667,6 @@ "hasInstallScript": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, @@ -11870,13 +12808,41 @@ "lodash": "^4.17.21" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "detect-libc": "^2.0.1" }, @@ -11932,6 +12898,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -12059,7 +13035,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -12564,6 +13540,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12631,6 +13608,51 @@ ], "license": "MIT" }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -12813,7 +13835,6 @@ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -12823,7 +13844,6 @@ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", "license": "MIT", - "peer": true, "dependencies": { "redis-errors": "^1.0.0" }, @@ -12841,7 +13861,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12948,6 +13968,21 @@ "node": ">= 4" } }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -13008,6 +14043,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13415,8 +14451,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -13427,6 +14462,23 @@ "node": ">= 0.8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -13650,6 +14702,13 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, "node_modules/superagent": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", @@ -13768,6 +14827,64 @@ "node": ">=8.0.0" } }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/terser": { "version": "5.44.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", @@ -13828,6 +14945,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14065,6 +15183,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -14181,6 +15305,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14342,6 +15467,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14528,16 +15654,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -14614,6 +15740,12 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.103.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", @@ -14708,7 +15840,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -14727,7 +15858,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -14741,7 +15871,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -14756,7 +15885,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -14766,8 +15894,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", @@ -14775,7 +15902,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -14786,7 +15912,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -14800,7 +15925,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -14815,6 +15939,29 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", @@ -14824,6 +15971,16 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14987,7 +16144,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -15004,7 +16161,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -15023,7 +16180,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=12" @@ -15043,7 +16200,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 0f0c6c2..c6a5bed 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/jwt": "^11.0.1", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", @@ -49,14 +50,17 @@ "@nestjs/swagger": "^11.2.1", "@nestjs/throttler": "^6.4.0", "axios": "^1.13.1", + "bullmq": "^5.65.1", "cache-manager": "^7.2.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "dayjs": "^1.11.19", "fastify": "^5.6.1", + "firebase-admin": "^13.6.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "ulid": "^3.0.1" + "ulid": "^3.0.1", + "uuid": "^13.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -70,6 +74,7 @@ "@types/jest": "^30.0.0", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2", diff --git a/src/app.module.ts b/src/app.module.ts index 5676a11..7066ba0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,18 +1,12 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { Module } from '@nestjs/common'; import dataBaseConfig from './config/mikro-orm.config'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ConfigModule } from '@nestjs/config'; import { UserModule } from './modules/users/user.module'; -// import { CacheModule } from '@nestjs/cache-manager'; -// import { cacheConfig } from './config/cache.config'; import { UtilsModule } from './modules/utils/utils.module'; -// import { HttpModule } from '@nestjs/axios'; -// import { httpConfig } from './config/http.config'; import { AuthModule } from './modules/auth/auth.module'; -// import { JwtModule } from '@nestjs/jwt'; import { UploaderModule } from './modules/uploader/uploader.module'; import { AdminModule } from './modules/admin/admin.module'; -// import { CacheService } from './modules/utils/cache.service'; import { ThrottlerModule } from '@nestjs/throttler'; import { ScheduleModule } from '@nestjs/schedule'; import { RestaurantsModule } from './modules/restaurants/restaurants.module'; @@ -24,22 +18,13 @@ import { DeliveryModule } from './modules/delivery/delivery.module'; import { OrdersModule } from './modules/orders/orders.module'; import { CouponModule } from './modules/coupons/coupon.module'; import { ReviewModule } from './modules/review/review.module'; +import { NotificationsModule } from './modules/notifications/notifications.module'; +import { EventEmitterModule } from '@nestjs/event-emitter'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, cache: true }), MikroOrmModule.forRootAsync(dataBaseConfig), - // CacheModule.registerAsync(cacheConfig()), - // JwtModule.registerAsync({ - // useFactory: (configService: ConfigService) => ({ - // global: true, - // secret: configService.getOrThrow('JWT_SECRET'), - // // signOptions: { - // // expiresIn: configService.getOrThrow('JWT_EXPIRATION_TIME'), - // // }, - // }), - // inject: [ConfigService], - // }), UserModule, UtilsModule, AuthModule, @@ -61,6 +46,8 @@ import { ReviewModule } from './modules/review/review.module'; OrdersModule, CouponModule, ReviewModule, + NotificationsModule, + EventEmitterModule.forRoot(), ], controllers: [], providers: [], diff --git a/src/modules/notifications/dto/create-notification.dto.ts b/src/modules/notifications/dto/create-notification.dto.ts new file mode 100644 index 0000000..25b7b2d --- /dev/null +++ b/src/modules/notifications/dto/create-notification.dto.ts @@ -0,0 +1,41 @@ +import { IsString, IsNotEmpty, IsOptional, IsArray, IsObject } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateNotificationDto { + @ApiProperty({ description: 'Notification title' }) + @IsString() + @IsNotEmpty() + title: string; + + @ApiProperty({ description: 'Notification message' }) + @IsString() + @IsNotEmpty() + message: string; + + @ApiProperty({ description: 'Notification type' }) + @IsString() + @IsNotEmpty() + type: string; + + @ApiPropertyOptional({ description: 'User ID' }) + @IsString() + @IsOptional() + userId?: string; + + @ApiPropertyOptional({ description: 'Phone number for SMS' }) + @IsString() + @IsOptional() + phoneNumber?: string; + + @ApiPropertyOptional({ description: 'Push notification tokens', type: [String] }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + pushTokens?: string[]; + + @ApiPropertyOptional({ description: 'Additional data' }) + @IsObject() + @IsOptional() + data?: Record; +} + diff --git a/src/modules/notifications/dto/create-preference.dto.ts b/src/modules/notifications/dto/create-preference.dto.ts new file mode 100644 index 0000000..a31edb7 --- /dev/null +++ b/src/modules/notifications/dto/create-preference.dto.ts @@ -0,0 +1,35 @@ +import { IsString, IsNotEmpty, IsBoolean, IsObject, IsOptional, ValidateNested } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +class ChannelsDto { + @ApiPropertyOptional({ description: 'Enable SMS channel' }) + @IsBoolean() + @IsOptional() + sms?: boolean; + + @ApiPropertyOptional({ description: 'Enable push notification channel' }) + @IsBoolean() + @IsOptional() + push?: boolean; +} + +export class CreateNotificationPreferenceDto { + @ApiProperty({ description: 'Notification type (e.g., ORDER_CONFIRMED, PAYMENT_FAILED)' }) + @IsString() + @IsNotEmpty() + notificationType: string; + + @ApiProperty({ description: 'Notification channels configuration', type: ChannelsDto }) + @ValidateNested() + @Type(() => ChannelsDto) + @IsObject() + @IsNotEmpty() + channels: ChannelsDto; + + @ApiPropertyOptional({ description: 'Whether the preference is enabled', default: true }) + @IsBoolean() + @IsOptional() + enabled?: boolean; +} + diff --git a/src/modules/notifications/dto/send-notification.dto.ts b/src/modules/notifications/dto/send-notification.dto.ts new file mode 100644 index 0000000..c306319 --- /dev/null +++ b/src/modules/notifications/dto/send-notification.dto.ts @@ -0,0 +1,38 @@ +import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class SendNotificationDto { + @ApiProperty({ description: 'Restaurant ID (ULID)' }) + @IsString() + @IsNotEmpty() + restaurantId: string; + + @ApiPropertyOptional({ description: 'User ID (ULID)' }) + @IsString() + @IsOptional() + userId?: string; + + @ApiProperty({ description: 'Notification type (e.g., ORDER_CONFIRMED, PAYMENT_FAILED)' }) + @IsString() + @IsNotEmpty() + notificationType: string; + + @ApiProperty({ description: 'Notification payload' }) + @IsObject() + @IsNotEmpty() + payload: { + title?: string; + message: string; + body?: string; + phoneNumber?: string; + pushToken?: string; + data?: Record; + templateId?: string; + parameters?: Array<{ name: string; value: string }>; + }; + + @ApiPropertyOptional({ description: 'Idempotency key to prevent duplicates' }) + @IsString() + @IsOptional() + idempotencyKey?: string; +} diff --git a/src/modules/notifications/entities/notification-preference.entity.ts b/src/modules/notifications/entities/notification-preference.entity.ts new file mode 100644 index 0000000..1c29373 --- /dev/null +++ b/src/modules/notifications/entities/notification-preference.entity.ts @@ -0,0 +1,24 @@ +import { Entity, Property, ManyToOne, Unique } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; + +export interface NotificationChannels { + sms?: boolean; + push?: boolean; +} + +@Entity({ tableName: 'notification_preferences' }) +@Unique({ properties: ['restaurant', 'notificationType'] }) +export class NotificationPreference extends BaseEntity { + @ManyToOne(() => Restaurant) + restaurant!: Restaurant; + + @Property() + notificationType!: string; // e.g., ORDER_CONFIRMED, PAYMENT_FAILED + + @Property({ type: 'json' }) + channels!: NotificationChannels; // { sms: true, push: true } + + @Property({ default: true }) + enabled: boolean = true; +} diff --git a/src/modules/notifications/entities/notification.entity.ts b/src/modules/notifications/entities/notification.entity.ts new file mode 100644 index 0000000..2c54ec2 --- /dev/null +++ b/src/modules/notifications/entities/notification.entity.ts @@ -0,0 +1,50 @@ +import { Entity, Property, ManyToOne, Enum } from '@mikro-orm/core'; +import { BaseEntity } from '../../../common/entities/base.entity'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { User } from '../../users/entities/user.entity'; + +export enum NotificationStatus { + PENDING = 'pending', + SENT = 'sent', + FAILED = 'failed', + SKIPPED = 'skipped', +} + +export enum NotificationChannel { + SMS = 'sms', + PUSH = 'push', +} + +@Entity({ tableName: 'notifications' }) +export class Notification extends BaseEntity { + @ManyToOne(() => Restaurant) + restaurant!: Restaurant; + + @ManyToOne(() => User, { nullable: true }) + user?: User; + + + + @Property() + notificationType!: string; // e.g., ORDER_CONFIRMED, PAYMENT_FAILED + + @Enum(() => NotificationChannel) + channel!: NotificationChannel; + + @Property({ type: 'json' }) + payload!: Record; // raw outbound payload + + @Enum(() => NotificationStatus) + status: NotificationStatus = NotificationStatus.PENDING; + + + + @Property({ type: 'int', default: 0 }) + attemptCount: number = 0; + + @Property({ nullable: true, unique: true }) + idempotencyKey?: string; + + @Property({ nullable: true }) + sentAt?: Date; +} diff --git a/src/modules/notifications/events/notification.events.ts b/src/modules/notifications/events/notification.events.ts new file mode 100644 index 0000000..560b116 --- /dev/null +++ b/src/modules/notifications/events/notification.events.ts @@ -0,0 +1,27 @@ +export class OrderCreatedEvent { + constructor( + public readonly restaurantId: string, + public readonly userId: string, + public readonly orderId: string, + public readonly orderData: any, + ) {} +} + +export class ReviewCreatedEvent { + constructor( + public readonly restaurantId: string, + public readonly userId: string, + public readonly reviewId: string, + public readonly reviewData: any, + ) {} +} + +export class OrderStatusChangedEvent { + constructor( + public readonly restaurantId: string, + public readonly userId: string, + public readonly orderId: string, + public readonly oldStatus: string, + public readonly newStatus: string, + ) {} +} diff --git a/src/modules/notifications/interfaces/notification-queue.interface.ts b/src/modules/notifications/interfaces/notification-queue.interface.ts new file mode 100644 index 0000000..5418b42 --- /dev/null +++ b/src/modules/notifications/interfaces/notification-queue.interface.ts @@ -0,0 +1,19 @@ +import { NotificationChannel } from '../entities/notification.entity'; + +export interface NotificationQueueJob { + restaurantId: string; + userId?: string; + notificationType: string; + channel: NotificationChannel; + payload: Record; + idempotencyKey?: string; + notificationId?: string; // For retries +} + +export interface NotificationQueueJobResult { + success: boolean; + notificationId: string; + providerResponse?: Record; + error?: string; +} + diff --git a/src/modules/notifications/listeners/notification.listeners.ts b/src/modules/notifications/listeners/notification.listeners.ts new file mode 100644 index 0000000..57440e9 --- /dev/null +++ b/src/modules/notifications/listeners/notification.listeners.ts @@ -0,0 +1,84 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { NotificationService } from '../services/notification.service'; +import { OrderCreatedEvent, ReviewCreatedEvent, OrderStatusChangedEvent } from '../events/notification.events'; + +@Injectable() +export class NotificationListeners { + private readonly logger = new Logger(NotificationListeners.name); + + constructor(private readonly notificationService: NotificationService) {} + + @OnEvent('order.created') + async handleOrderCreated(event: OrderCreatedEvent) { + try { + this.logger.log(`Order created event received: ${event.orderId}`); + + await this.notificationService.sendNotification({ + restaurantId: event.restaurantId, + userId: event.userId, + notificationType: 'order.created', + payload: { + title: 'Order Confirmed', + message: `Your order #${event.orderId} has been confirmed`, + data: { orderId: event.orderId }, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for order.created event: ${event.orderId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } + + @OnEvent('review.created') + async handleReviewCreated(event: ReviewCreatedEvent) { + try { + this.logger.log(`Review created event received: ${event.reviewId}`); + + await this.notificationService.sendNotification({ + restaurantId: event.restaurantId, + userId: event.userId, + notificationType: 'review.created', + payload: { + title: 'Review Received', + message: 'Thank you for your review!', + data: { reviewId: event.reviewId }, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for review.created event: ${event.reviewId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } + + @OnEvent('order.status.changed') + async handleOrderStatusChanged(event: OrderStatusChangedEvent) { + try { + this.logger.log(`Order status changed: ${event.orderId} from ${event.oldStatus} to ${event.newStatus}`); + + await this.notificationService.sendNotification({ + restaurantId: event.restaurantId, + userId: event.userId, + notificationType: 'order.status.changed', + payload: { + title: 'Order Status Updated', + message: `Your order #${event.orderId} is now ${event.newStatus}`, + data: { + orderId: event.orderId, + oldStatus: event.oldStatus, + newStatus: event.newStatus, + }, + }, + }); + } catch (error) { + this.logger.error( + `Failed to send notification for order.status.changed event: ${event.orderId}`, + error instanceof Error ? error.stack : String(error), + ); + } + } +} diff --git a/src/modules/notifications/notifications.controller.ts b/src/modules/notifications/notifications.controller.ts new file mode 100644 index 0000000..069703d --- /dev/null +++ b/src/modules/notifications/notifications.controller.ts @@ -0,0 +1,152 @@ +import { Controller, Post, Get, Body, Param, UseGuards, Query, Patch, Delete } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { NotificationService } from './services/notification.service'; +import { NotificationPreferenceService } from './services/notification-preference.service'; +import { SendNotificationDto } from './dto/send-notification.dto'; +import { CreateNotificationPreferenceDto } from './dto/create-preference.dto'; +import { AuthGuard } from '../auth/guards/auth.guard'; +import { UserId } from '../../common/decorators/user-id.decorator'; +import { AdminAuthGuard } from '../auth/guards/adminAuth.guard'; +import { RestId } from '../../common/decorators/rest-id.decorator'; + +@ApiTags('notifications') +@Controller() +export class NotificationsController { + constructor( + private readonly notificationService: NotificationService, + private readonly preferenceService: NotificationPreferenceService, + ) {} + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Post('admin/notifications/send') + @ApiOperation({ summary: 'Send a notification (queued)' }) + async sendNotification(@Body() dto: SendNotificationDto) { + return this.notificationService.sendNotification(dto); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Get('admin/notifications') + @ApiOperation({ summary: 'Get notifications for a restaurant' }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'notificationType', required: false, type: String }) + async getRestaurantNotifications( + @RestId() restaurantId: string, + @Query('limit') limit?: number, + @Query('notificationType') notificationType?: string, + ) { + if (notificationType) { + return this.notificationService.findByRestaurantAndType( + restaurantId, + notificationType, + limit ? parseInt(limit.toString(), 10) : 50, + ); + } + return this.notificationService.findByRestaurant(restaurantId, limit ? parseInt(limit.toString(), 10) : 50); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Get('admin/notifications/:id') + @ApiOperation({ summary: 'Get a notification by ID' }) + @ApiParam({ name: 'id', description: 'Notification ID' }) + async getNotification(@Param('id') id: string) { + return this.notificationService.findOne(id); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Post('admin/notifications/:id/retry') + @ApiOperation({ summary: 'Retry a failed notification' }) + @ApiParam({ name: 'id', description: 'Notification ID' }) + async retryNotification(@Param('id') id: string) { + await this.notificationService.retryFailedNotification(id); + return { message: 'Notification queued for retry' }; + } + + @UseGuards(AuthGuard) + @ApiBearerAuth() + @Get('public/notifications') + @ApiOperation({ summary: 'Get user notifications' }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + async getUserNotifications(@UserId() userId: string, @Query('limit') limit?: number) { + return this.notificationService.findByUserId(userId, limit ? parseInt(limit.toString(), 10) : 50); + } + + @UseGuards(AuthGuard) + @ApiBearerAuth() + @Get('public/notifications/:id') + @ApiOperation({ summary: 'Get a notification by ID' }) + @ApiParam({ name: 'id', description: 'Notification ID' }) + async getUserNotification(@Param('id') id: string) { + return this.notificationService.findOne(id); + } + + // Preference endpoints + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Post('admin/notification-preferences') + @ApiOperation({ summary: 'Create or update notification preference' }) + async createPreference(@RestId() restaurantId: string, @Body() dto: CreateNotificationPreferenceDto) { + return this.preferenceService.createOrUpdate( + restaurantId, + dto.notificationType, + dto.channels, + dto.enabled ?? true, + ); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Get('admin/notification-preferences') + @ApiOperation({ summary: 'Get all notification preferences for a restaurant' }) + async getPreferences(@RestId() restaurantId: string) { + return this.preferenceService.findByRestaurant(restaurantId); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Get('admin/notification-preferences/:type') + @ApiOperation({ summary: 'Get a specific notification preference' }) + @ApiParam({ name: 'type', description: 'Notification type' }) + async getPreference(@RestId() restaurantId: string, @Param('type') notificationType: string) { + return this.preferenceService.findByRestaurantAndType(restaurantId, notificationType); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Patch('admin/notification-preferences/:type/enabled') + @ApiOperation({ summary: 'Enable or disable a notification preference' }) + @ApiParam({ name: 'type', description: 'Notification type' }) + async updatePreferenceEnabled( + @RestId() restaurantId: string, + @Param('type') notificationType: string, + @Body('enabled') enabled: boolean, + ) { + return this.preferenceService.updateEnabled(restaurantId, notificationType, enabled); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Patch('admin/notification-preferences/:type/channels') + @ApiOperation({ summary: 'Update notification channels' }) + @ApiParam({ name: 'type', description: 'Notification type' }) + async updatePreferenceChannels( + @RestId() restaurantId: string, + @Param('type') notificationType: string, + @Body('channels') channels: { sms?: boolean; push?: boolean }, + ) { + return this.preferenceService.updateChannels(restaurantId, notificationType, channels); + } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Delete('admin/notification-preferences/:type') + @ApiOperation({ summary: 'Delete a notification preference' }) + @ApiParam({ name: 'type', description: 'Notification type' }) + async deletePreference(@RestId() restaurantId: string, @Param('type') notificationType: string) { + await this.preferenceService.delete(restaurantId, notificationType); + return { message: 'Preference deleted successfully' }; + } +} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts new file mode 100644 index 0000000..34629a7 --- /dev/null +++ b/src/modules/notifications/notifications.module.ts @@ -0,0 +1,58 @@ +import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { BullModule } from '@nestjs/bullmq'; +import { JwtModule } from '@nestjs/jwt'; +import { Notification } from './entities/notification.entity'; +import { NotificationPreference } from './entities/notification-preference.entity'; +import { NotificationService } from './services/notification.service'; +import { NotificationPreferenceService } from './services/notification-preference.service'; +import { NotificationQueueService } from './services/notification-queue.service'; +import { PushNotificationService } from './services/push-notification.service'; +import { SmsNotificationService } from './services/sms-notification.service'; +import { NotificationProcessor } from './processors/notification.processor'; +import { NotificationsController } from './notifications.controller'; +import { User } from '../users/entities/user.entity'; +import { Restaurant } from '../restaurants/entities/restaurant.entity'; +import { NotificationListeners } from './listeners/notification.listeners'; +import { AuthModule } from '../auth/auth.module'; +import { ConfigService } from '@nestjs/config'; + +@Module({ + imports: [ + AuthModule, + JwtModule, + MikroOrmModule.forFeature([Notification, NotificationPreference, User, Restaurant]), + BullModule.registerQueue({ + name: 'notification-queue', + }), + + BullModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + connection: { + host: config.get('REDIS_HOST') || 'captain.dev.danakcorp.com', + port: config.get('REDIS_PORT') || 50601, + password: config.get('REDIS_PASSWORD'), // Pull from .env + }, + }), + }), + ], + controllers: [NotificationsController], + providers: [ + NotificationService, + NotificationPreferenceService, + NotificationQueueService, + PushNotificationService, + SmsNotificationService, + NotificationProcessor, + NotificationListeners, + ], + exports: [ + NotificationService, + NotificationPreferenceService, + NotificationQueueService, + PushNotificationService, + SmsNotificationService, + ], +}) +export class NotificationsModule {} diff --git a/src/modules/notifications/processors/notification.processor.ts b/src/modules/notifications/processors/notification.processor.ts new file mode 100644 index 0000000..fbc78bf --- /dev/null +++ b/src/modules/notifications/processors/notification.processor.ts @@ -0,0 +1,248 @@ +import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq'; +import { Logger } from '@nestjs/common'; +import { Job } from 'bullmq'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import { EntityRepository, EntityManager } from '@mikro-orm/postgresql'; +import { Notification, NotificationStatus, NotificationChannel } from '../entities/notification.entity'; +import { NotificationQueueJob, NotificationQueueJobResult } from '../interfaces/notification-queue.interface'; +import { PushNotificationService } from '../services/push-notification.service'; +import { SmsNotificationService } from '../services/sms-notification.service'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { User } from '../../users/entities/user.entity'; + +@Processor('notification-queue') +export class NotificationProcessor extends WorkerHost { + private readonly logger = new Logger(NotificationProcessor.name); + + constructor( + @InjectRepository(Notification) + private readonly notificationRepository: EntityRepository, + private readonly em: EntityManager, + private readonly pushNotificationService: PushNotificationService, + private readonly smsNotificationService: SmsNotificationService, + ) { + super(); + } + + async process(job: Job): Promise { + const { restaurantId, userId, notificationType, channel, payload, idempotencyKey, notificationId } = job.data; + + this.logger.log( + `Processing notification job: ${job.id} - Type: ${notificationType}, Channel: ${channel}, Restaurant: ${restaurantId}`, + ); + + try { + // Find or create notification record + let notification: Notification | null = null; + + if (notificationId) { + // Retry case - find existing notification + notification = await this.notificationRepository.findOne({ id: notificationId }); + if (!notification) { + throw new Error(`Notification ${notificationId} not found for retry`); + } + } else { + // Check for duplicate using idempotency key + if (idempotencyKey) { + const existing = await this.notificationRepository.findOne({ idempotencyKey }); + if (existing && existing.status === NotificationStatus.SENT) { + this.logger.warn(`Duplicate notification skipped: ${idempotencyKey}`); + return { + success: true, + notificationId: existing.id, + }; + } + if (existing) { + notification = existing; + } + } + + // Create new notification if not found + if (!notification) { + const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); + if (!restaurant) { + throw new Error(`Restaurant ${restaurantId} not found`); + } + const user = userId ? await this.em.findOne(User, { id: userId }) : undefined; + + notification = this.em.create(Notification, { + restaurant, + user, + notificationType, + channel, + payload, + idempotencyKey, + status: NotificationStatus.PENDING, + attemptCount: 0, + }); + await this.em.persistAndFlush(notification); + } + } + + // Increment attempt count + notification.attemptCount += 1; + await this.em.persistAndFlush(notification); + + // Send notification based on channel + let providerResponse: Record | undefined; + let success = false; + + switch (channel) { + case NotificationChannel.PUSH: { + const result = await this.sendPushNotification(notification); + success = result.success; + providerResponse = result.providerResponse; + break; + } + case NotificationChannel.SMS: { + const result = await this.sendSmsNotification(notification); + success = result.success; + providerResponse = result.providerResponse; + break; + } + default: + throw new Error(`Unsupported notification channel: ${channel}`); + } + + // Update notification status + if (success) { + notification.status = NotificationStatus.SENT; + notification.sentAt = new Date(); + this.logger.log(`Notification ${notification.id} sent successfully`); + } else { + notification.status = NotificationStatus.FAILED; + this.logger.error(`Notification ${notification.id} failed to send`); + } + + await this.em.persistAndFlush(notification); + + return { + success, + notificationId: notification.id, + providerResponse, + }; + } catch (error) { + this.logger.error(`Error processing notification job ${job.id}:`, error); + + // Update notification status if it exists + if (notificationId) { + const notification = await this.notificationRepository.findOne({ id: notificationId }); + if (notification) { + notification.status = NotificationStatus.FAILED; + + await this.em.persistAndFlush(notification); + } + } + + throw error; + } + } + + private async sendPushNotification(notification: Notification): Promise<{ + success: boolean; + providerResponse?: Record; + }> { + try { + const pushToken = notification.payload.pushToken; + if (!pushToken) { + this.logger.warn(`No push token in payload for notification ${notification.id}`); + return { + success: false, + providerResponse: { error: 'No push token provided' }, + }; + } + + const response = await this.pushNotificationService.sendToToken({ + title: notification.payload.title || 'Notification', + body: notification.payload.message || notification.payload.body || '', + token: pushToken, + data: notification.payload.data, + }); + + if (response.success) { + return { + success: true, + providerResponse: { + messageId: response.messageId, + success: true, + }, + }; + } else { + return { + success: false, + providerResponse: { + error: response.error, + success: false, + }, + }; + } + } catch (error) { + return { + success: false, + providerResponse: { + error: error instanceof Error ? error.message : 'Unknown error', + success: false, + }, + }; + } + } + + private async sendSmsNotification(notification: Notification): Promise<{ + success: boolean; + providerResponse?: Record; + }> { + try { + const phoneNumber = notification.payload.phoneNumber; + if (!phoneNumber) { + this.logger.warn(`No phone number in payload for notification ${notification.id}`); + return { + success: false, + providerResponse: { error: 'No phone number provided' }, + }; + } + + const response = await this.smsNotificationService.send({ + phoneNumber, + message: notification.payload.message || notification.payload.body || '', + templateId: notification.payload.templateId, + parameters: notification.payload.parameters, + }); + + if (response.success) { + return { + success: true, + providerResponse: { + messageId: response.messageId, + success: true, + }, + }; + } else { + return { + success: false, + providerResponse: { + error: response.error, + success: false, + }, + }; + } + } catch (error) { + return { + success: false, + providerResponse: { + error: error instanceof Error ? error.message : 'Unknown error', + success: false, + }, + }; + } + } + + @OnWorkerEvent('completed') + onCompleted(job: Job) { + this.logger.log(`Notification job ${job.id} completed`); + } + + @OnWorkerEvent('failed') + onFailed(job: Job, error: Error) { + this.logger.error(`Notification job ${job.id} failed:`, error); + } +} diff --git a/src/modules/notifications/services/notification-preference.service.ts b/src/modules/notifications/services/notification-preference.service.ts new file mode 100644 index 0000000..017d938 --- /dev/null +++ b/src/modules/notifications/services/notification-preference.service.ts @@ -0,0 +1,93 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { NotificationPreference, NotificationChannels } from '../entities/notification-preference.entity'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; + +@Injectable() +export class NotificationPreferenceService { + constructor(private readonly em: EntityManager) {} + + async createOrUpdate( + restaurantId: string, + notificationType: string, + channels: NotificationChannels, + enabled: boolean = true, + ): Promise { + const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); + if (!restaurant) { + throw new NotFoundException('Restaurant not found'); + } + + let preference = await this.em.findOne(NotificationPreference, { + restaurant: { id: restaurantId }, + notificationType, + }); + + if (preference) { + preference.channels = channels; + preference.enabled = enabled; + } else { + preference = this.em.create(NotificationPreference, { + restaurant, + notificationType, + channels, + enabled, + }); + } + + await this.em.persistAndFlush(preference); + return preference; + } + + async findByRestaurant(restaurantId: string): Promise { + return this.em.find(NotificationPreference, { + restaurant: { id: restaurantId }, + }); + } + + async findByRestaurantAndType( + restaurantId: string, + notificationType: string, + ): Promise { + return this.em.findOne(NotificationPreference, { + restaurant: { id: restaurantId }, + notificationType, + }); + } + + async updateEnabled(restaurantId: string, notificationType: string, enabled: boolean): Promise { + const preference = await this.findByRestaurantAndType(restaurantId, notificationType); + if (!preference) { + throw new NotFoundException('Notification preference not found'); + } + + preference.enabled = enabled; + await this.em.persistAndFlush(preference); + return preference; + } + + async updateChannels( + restaurantId: string, + notificationType: string, + channels: NotificationChannels, + ): Promise { + const preference = await this.findByRestaurantAndType(restaurantId, notificationType); + if (!preference) { + throw new NotFoundException('Notification preference not found'); + } + + preference.channels = channels; + await this.em.persistAndFlush(preference); + return preference; + } + + async delete(restaurantId: string, notificationType: string): Promise { + const preference = await this.findByRestaurantAndType(restaurantId, notificationType); + if (!preference) { + throw new NotFoundException('Notification preference not found'); + } + + await this.em.removeAndFlush(preference); + } +} + diff --git a/src/modules/notifications/services/notification-queue.service.ts b/src/modules/notifications/services/notification-queue.service.ts new file mode 100644 index 0000000..80e0cfe --- /dev/null +++ b/src/modules/notifications/services/notification-queue.service.ts @@ -0,0 +1,63 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Queue } from 'bullmq'; +import { NotificationQueueJob } from '../interfaces/notification-queue.interface'; +import { NotificationChannel } from '../entities/notification.entity'; + +@Injectable() +export class NotificationQueueService { + private readonly logger = new Logger(NotificationQueueService.name); + + constructor(@InjectQueue('notification-queue') private readonly notificationQueue: Queue) {} + + async addNotification(job: NotificationQueueJob): Promise { + try { + const jobId = job.idempotencyKey || `${job.restaurantId}-${job.notificationType}-${job.channel}-${Date.now()}`; + + await this.notificationQueue.add('send-notification', job, { + jobId, + attempts: 3, + backoff: { + type: 'exponential', + delay: 2000, + }, + removeOnComplete: { + age: 24 * 3600, // Keep completed jobs for 24 hours + count: 1000, + }, + removeOnFail: { + age: 7 * 24 * 3600, // Keep failed jobs for 7 days + }, + }); + + this.logger.log(`Notification job added to queue: ${jobId}`); + } catch (error) { + this.logger.error(`Failed to add notification to queue:`, error); + throw error; + } + } + + async addBulkNotifications(jobs: NotificationQueueJob[]): Promise { + try { + const queueJobs = jobs.map(job => ({ + name: 'send-notification', + data: job, + opts: { + jobId: job.idempotencyKey || `${job.restaurantId}-${job.notificationType}-${job.channel}-${Date.now()}`, + attempts: 3, + backoff: { + type: 'exponential', + delay: 2000, + }, + }, + })); + + await this.notificationQueue.addBulk(queueJobs); + this.logger.log(`Added ${jobs.length} notification jobs to queue`); + } catch (error) { + this.logger.error(`Failed to add bulk notifications to queue:`, error); + throw error; + } + } +} + diff --git a/src/modules/notifications/services/notification.service.ts b/src/modules/notifications/services/notification.service.ts new file mode 100644 index 0000000..e5b6ecd --- /dev/null +++ b/src/modules/notifications/services/notification.service.ts @@ -0,0 +1,190 @@ +import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common'; +import { EntityManager } from '@mikro-orm/postgresql'; +import { Notification, NotificationStatus, NotificationChannel } from '../entities/notification.entity'; +import { NotificationPreferenceService } from './notification-preference.service'; +import { NotificationQueueService } from './notification-queue.service'; +import { NotificationQueueJob } from '../interfaces/notification-queue.interface'; +import { Restaurant } from '../../restaurants/entities/restaurant.entity'; +import { User } from '../../users/entities/user.entity'; + +export interface SendNotificationParams { + restaurantId: string; + userId?: string; + notificationType: string; + payload: { + title?: string; + message: string; + body?: string; + phoneNumber?: string; + pushToken?: string; + data?: Record; + templateId?: string; + parameters?: Array<{ name: string; value: string }>; + }; + idempotencyKey?: string; +} + +@Injectable() +export class NotificationService { + private readonly logger = new Logger(NotificationService.name); + + constructor( + private readonly em: EntityManager, + private readonly preferenceService: NotificationPreferenceService, + private readonly queueService: NotificationQueueService, + ) {} + + async sendNotification(params: SendNotificationParams): Promise { + const { restaurantId, userId, notificationType, payload, idempotencyKey } = params; + + // Verify restaurant exists + const restaurant = await this.em.findOne(Restaurant, { id: restaurantId }); + if (!restaurant) { + throw new NotFoundException('Restaurant not found'); + } + + // Get notification preferences + const preference = await this.preferenceService.findByRestaurantAndType(restaurantId, notificationType); + + if (!preference || !preference.enabled) { + this.logger.warn( + `Notification preference not found or disabled for restaurant ${restaurantId}, type ${notificationType}`, + ); + return []; + } + + // Determine which channels to use + const channels: NotificationChannel[] = []; + if (preference.channels.sms && payload.phoneNumber) { + channels.push(NotificationChannel.SMS); + } + if (preference.channels.push && payload.pushToken) { + channels.push(NotificationChannel.PUSH); + } + + if (channels.length === 0) { + this.logger.warn(`No enabled channels for notification type ${notificationType}`); + return []; + } + + // Create notification records and queue jobs + const notifications: Notification[] = []; + const jobs: NotificationQueueJob[] = []; + + for (const channel of channels) { + const channelIdempotencyKey = idempotencyKey + ? `${idempotencyKey}-${channel}` + : `${restaurantId}-${notificationType}-${channel}-${Date.now()}`; + + // Create notification record + const user = userId ? await this.em.findOne(User, { id: userId }) : undefined; + const notification = this.em.create(Notification, { + restaurant, + user, + notificationType, + channel, + payload, + status: NotificationStatus.PENDING, + idempotencyKey: channelIdempotencyKey, + attemptCount: 0, + }); + + await this.em.persistAndFlush(notification); + notifications.push(notification); + + // Add to queue + jobs.push({ + restaurantId, + userId, + notificationType, + channel, + payload, + idempotencyKey: channelIdempotencyKey, + notificationId: notification.id, + }); + } + + // Add all jobs to queue + if (jobs.length === 1) { + await this.queueService.addNotification(jobs[0]); + } else { + await this.queueService.addBulkNotifications(jobs); + } + + this.logger.log( + `Queued ${notifications.length} notification(s) for restaurant ${restaurantId}, type ${notificationType}`, + ); + + return notifications; + } + + async findOne(id: string): Promise { + const notification = await this.em.findOne(Notification, { id }, { populate: ['restaurant', 'user'] }); + if (!notification) { + throw new NotFoundException('Notification not found'); + } + return notification; + } + + async findByRestaurant(restaurantId: string, limit = 50): Promise { + return this.em.find( + Notification, + { restaurant: { id: restaurantId } }, + { + orderBy: { createdAt: 'DESC' }, + limit, + populate: ['user'], + }, + ); + } + + async findByUserId(userId: string, limit = 50): Promise { + return this.em.find( + Notification, + { user: { id: userId } }, + { + orderBy: { createdAt: 'DESC' }, + limit, + populate: ['restaurant'], + }, + ); + } + + async findByRestaurantAndType(restaurantId: string, notificationType: string, limit = 50): Promise { + return this.em.find( + Notification, + { + restaurant: { id: restaurantId }, + notificationType, + }, + { + orderBy: { createdAt: 'DESC' }, + limit, + populate: ['user'], + }, + ); + } + + async retryFailedNotification(notificationId: string): Promise { + const notification = await this.findOne(notificationId); + + if (notification.status !== NotificationStatus.FAILED) { + throw new BadRequestException('Only failed notifications can be retried'); + } + + // Reset status and add to queue + notification.status = NotificationStatus.PENDING; + await this.em.persistAndFlush(notification); + + await this.queueService.addNotification({ + restaurantId: notification.restaurant.id, + notificationType: notification.notificationType, + channel: notification.channel, + payload: notification.payload, + idempotencyKey: notification.idempotencyKey, + notificationId: notification.id, + }); + + this.logger.log(`Retry queued for notification ${notificationId}`); + } +} diff --git a/src/modules/notifications/services/push-notification.service.ts b/src/modules/notifications/services/push-notification.service.ts new file mode 100644 index 0000000..3260078 --- /dev/null +++ b/src/modules/notifications/services/push-notification.service.ts @@ -0,0 +1,148 @@ +import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as admin from 'firebase-admin'; + +export interface PushNotificationPayload { + title: string; + body: string; + data?: Record; + token?: string; + tokens?: string[]; +} + +export interface PushNotificationResponse { + success: boolean; + messageId?: string; + error?: string; +} + +@Injectable() +export class PushNotificationService { + private readonly logger = new Logger(PushNotificationService.name); + private firebaseApp: admin.app.App | null = null; + + constructor(private readonly configService: ConfigService) { + this.initializeFirebase(); + } + + private initializeFirebase() { + try { + const firebaseConfig = this.configService.get('FIREBASE_SERVICE_ACCOUNT'); + + if (!firebaseConfig) { + this.logger.warn('Firebase service account not configured. Push notifications will be disabled.'); + return; + } + + // Parse the service account JSON + const serviceAccount = JSON.parse(firebaseConfig); + + if (!this.firebaseApp) { + this.firebaseApp = admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); + this.logger.log('Firebase initialized successfully'); + } + } catch (error) { + this.logger.error('Failed to initialize Firebase:', error); + this.logger.warn('Push notifications will be disabled'); + } + } + + async sendToToken(payload: PushNotificationPayload): Promise { + if (!this.firebaseApp || !payload.token) { + throw new HttpException('Push notification service not configured or token missing', HttpStatus.SERVICE_UNAVAILABLE); + } + + try { + const message: admin.messaging.Message = { + notification: { + title: payload.title, + body: payload.body, + }, + data: this.convertDataToString(payload.data || {}), + token: payload.token, + android: { + priority: 'high', + }, + apns: { + headers: { + 'apns-priority': '10', + }, + }, + }; + + const response = await admin.messaging().send(message); + this.logger.log(`Push notification sent successfully: ${response}`); + + return { + success: true, + messageId: response, + }; + } catch (error) { + this.logger.error(`Failed to send push notification: ${error}`); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + async sendToMultipleTokens(payload: PushNotificationPayload): Promise { + if (!this.firebaseApp || !payload.tokens || payload.tokens.length === 0) { + throw new HttpException( + 'Push notification service not configured or tokens missing', + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + try { + const message: admin.messaging.MulticastMessage = { + notification: { + title: payload.title, + body: payload.body, + }, + data: this.convertDataToString(payload.data || {}), + tokens: payload.tokens, + android: { + priority: 'high', + }, + apns: { + headers: { + 'apns-priority': '10', + }, + }, + }; + + const response = await admin.messaging().sendEachForMulticast(message); + this.logger.log( + `Push notifications sent: ${response.successCount} successful, ${response.failureCount} failed`, + ); + + return { + success: response.successCount > 0, + messageId: response.responses[0]?.messageId, + error: response.failureCount > 0 ? `${response.failureCount} notifications failed` : undefined, + }; + } catch (error) { + this.logger.error(`Failed to send push notifications: ${error}`); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private convertDataToString(data: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(data)) { + result[key] = typeof value === 'string' ? value : JSON.stringify(value); + } + return result; + } + + isConfigured(): boolean { + return this.firebaseApp !== null; + } +} + diff --git a/src/modules/notifications/services/sms-notification.service.ts b/src/modules/notifications/services/sms-notification.service.ts new file mode 100644 index 0000000..1a187ed --- /dev/null +++ b/src/modules/notifications/services/sms-notification.service.ts @@ -0,0 +1,96 @@ +import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; + +export interface SmsNotificationPayload { + phoneNumber: string; + message: string; + templateId?: string; + parameters?: Array<{ name: string; value: string }>; +} + +export interface SmsNotificationResponse { + success: boolean; + messageId?: string; + error?: string; +} + +@Injectable() +export class SmsNotificationService { + private readonly logger = new Logger(SmsNotificationService.name); + private readonly baseURL: string; + private readonly apiKey: string; + private readonly defaultTemplateId?: string; + + constructor(private readonly configService: ConfigService) { + this.baseURL = this.configService.getOrThrow('SMS_BASE_URL'); + this.apiKey = this.configService.getOrThrow('SMS_API_KEY'); + this.defaultTemplateId = this.configService.get('SMS_PATTERN_OTP'); + } + + async send(payload: SmsNotificationPayload): Promise { + try { + // If templateId is provided, use template-based SMS + if (payload.templateId || this.defaultTemplateId) { + return await this.sendTemplateSms(payload); + } + + // Otherwise, send simple SMS + return await this.sendSimpleSms(payload); + } catch (error) { + this.logger.error(`Failed to send SMS: ${error}`); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } + + private async sendTemplateSms(payload: SmsNotificationPayload): Promise { + const templateId = payload.templateId || this.defaultTemplateId; + + if (!templateId) { + throw new HttpException('Template ID is required for template SMS', HttpStatus.BAD_REQUEST); + } + + const smsData = { + Parameters: payload.parameters || [{ name: 'message', value: payload.message }], + Mobile: payload.phoneNumber, + TemplateId: templateId, + }; + + const url = `${this.baseURL}/send/verify`; + + try { + const { data } = await axios.post<{ status: number; message: string }>(url, smsData, { + headers: { + 'Content-Type': 'application/json', + 'X-API-KEY': this.apiKey, + }, + }); + + if (data.status !== 1) { + throw new HttpException(data.message || 'SMS sending failed', HttpStatus.BAD_GATEWAY); + } + + this.logger.log(`SMS sent successfully to ${payload.phoneNumber}`); + return { + success: true, + messageId: data.message, + }; + } catch (error) { + this.logger.error(`SMS sending failed: ${JSON.stringify(error)}`); + throw new HttpException('The SMS was not sent. Please try again later.', HttpStatus.BAD_GATEWAY); + } + } + + private async sendSimpleSms(payload: SmsNotificationPayload): Promise { + // If your SMS provider supports simple text messages, implement it here + // For now, we'll use the template method as fallback + return this.sendTemplateSms({ + ...payload, + parameters: [{ name: 'message', value: payload.message }], + }); + } +} +