From 8a09ee092f5d20083358a596fd096e931001ba7c Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 16 Aug 2025 15:02:00 +1000 Subject: [PATCH 01/11] add notification.model.js --- .idea/workspace.xml | 14 +++++++-- .../HMI/ui/model/notification.model.js | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 src/Components/HMI/ui/model/notification.model.js diff --git a/.idea/workspace.xml b/.idea/workspace.xml index edf108f31..f9caecd7a 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,8 +5,9 @@ - - + + + + @@ -43,12 +49,14 @@ Date: Sat, 16 Aug 2025 15:06:11 +1000 Subject: [PATCH 02/11] add backend notification APIs --- .idea/workspace.xml | 18 ++++++--- src/Components/HMI/ui/server.js | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index f9caecd7a..b00ebbe63 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,10 +4,9 @@ - - + - + @@ -109,7 +116,8 @@ - diff --git a/src/Components/HMI/ui/server.js b/src/Components/HMI/ui/server.js index 95f59120e..6e8af55f7 100644 --- a/src/Components/HMI/ui/server.js +++ b/src/Components/HMI/ui/server.js @@ -737,6 +737,72 @@ app.get("/map", async(req,res) => { res.sendFile(path.join(__dirname, 'public/index.html')) }) +// Get all notifications for current user +app.get('/api/notifications', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const notifications = await Notification.find({ userId: decoded.id }) + .sort({ createdAt: -1 }) + .limit(50); + + res.json(notifications); + } catch (error) { + res.status(500).json({ error: 'Error fetching notifications' }); + } +}); + +// Mark notification as read +app.patch('/api/notifications/:id/read', async (req, res) => { + try { + await Notification.findByIdAndUpdate(req.params.id, { read: true }); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: 'Error updating notification' }); + } +}); + +// Mark all notifications as read +app.patch('/api/notifications/read-all', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + await Notification.updateMany( + { userId: decoded.id, read: false }, + { $set: { read: true } } + ); + + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: 'Error updating notifications' }); + } +}); + +// Create a notification (helper function) +async function createNotification(userId, message, type, link = null, icon = null) { + return await Notification.create({ + userId, + message, + type, + link, + icon: icon || getDefaultIcon(type) + }); +} + +function getDefaultIcon(type) { + const icons = { + donation: 'ti ti-receipt-2', + request: 'ti ti-alert-circle', + user: 'ti ti-user', + system: 'ti ti-bell' + }; + return icons[type] || 'ti ti-bell'; +} + // start the server app.listen(port, () => { From 1a397c8974624f5da455115fb8e2e5b2f02e3f35 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 16 Aug 2025 17:28:55 +1000 Subject: [PATCH 03/11] add backend notification --- .idea/workspace.xml | 37 +- src/Components/HMI/ui/model/donation.model.js | 31 + .../HMI/ui/model/notification.model.js | 44 +- .../HMI/ui/node_modules/.package-lock.json | 495 +++++- src/Components/HMI/ui/package-lock.json | 832 +++++++++- src/Components/HMI/ui/package.json | 5 +- .../HMI/ui/public/admin/css/notifications.css | 95 ++ .../HMI/ui/public/admin/js/notifications.js | 252 ++- .../HMI/ui/public/admin/notifications.html | 21 +- src/Components/HMI/ui/public/sw.js | 53 + src/Components/HMI/ui/server.js | 155 +- .../HMI/ui/services/dispatchService.js | 52 + .../HMI/ui/services/notificationService.js | 151 ++ src/Components/docker-compose.yml | 5 + src/Components/package-lock.json | 1400 ++++++++++++++++- src/Components/package.json | 3 +- 16 files changed, 3319 insertions(+), 312 deletions(-) create mode 100644 src/Components/HMI/ui/model/donation.model.js create mode 100644 src/Components/HMI/ui/public/sw.js create mode 100644 src/Components/HMI/ui/services/dispatchService.js create mode 100644 src/Components/HMI/ui/services/notificationService.js diff --git a/.idea/workspace.xml b/.idea/workspace.xml index b00ebbe63..e1c589ea0 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,9 +4,23 @@ - + + + + + + + + + + + + + + + + + @@ -63,6 +81,7 @@ "node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.tslint": "(autodetect)", "nodejs_package_manager_path": "npm", + "ts.external.directory.path": "C:\\Program Files\\JetBrains\\WebStorm 2024.3.5\\plugins\\javascript-plugin\\jsLanguageServicesImpl\\external", "vue.rearranger.settings.migration": "true" } }]]> @@ -102,7 +121,15 @@ @@ -113,11 +140,15 @@ + + - diff --git a/src/Components/HMI/ui/model/donation.model.js b/src/Components/HMI/ui/model/donation.model.js new file mode 100644 index 000000000..01294331a --- /dev/null +++ b/src/Components/HMI/ui/model/donation.model.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose'); +const donationSchema = new mongoose.Schema({ + amount: { + type: Number, + required: true + }, + status: { + type: String, + enum: ['succeeded', 'pending', 'failed'], + required: true + }, + billing_details: { + email: { + type: String, + required: true + } + }, + created: { + type: Date, + default: Date.now + }, + type: { + type: String, + enum: ['One-Time', 'Recurring'], + required: true + } +}, { + timestamps: true +}); + +module.exports = mongoose.model('Donation', donationSchema); diff --git a/src/Components/HMI/ui/model/notification.model.js b/src/Components/HMI/ui/model/notification.model.js index e9fd2064e..075613ef0 100644 --- a/src/Components/HMI/ui/model/notification.model.js +++ b/src/Components/HMI/ui/model/notification.model.js @@ -6,26 +6,54 @@ const NotificationSchema = new mongoose.Schema({ ref: "User", required: true }, + title: { + type: String, + required: true + }, message: { type: String, required: true }, type: { type: String, - enum: ['donation', 'request', 'user', 'system'], + enum: ['donation', 'request', 'user', 'system', 'alert'], required: true }, - read: { - type: Boolean, - default: false + status: { + type: String, + enum: ['unread', 'read', 'archived'], + default: 'unread' }, link: String, icon: String, - metadata: mongoose.Schema.Types.Mixed // For any additional data + metadata: mongoose.Schema.Types.Mixed, + expiresAt: Date }, { timestamps: true, - toJSON: { virtuals: true }, - toObject: { virtuals: true } + index: { expiresAt: 1, expireAfterSeconds: 0 } // For auto-deleting expired notifications +}); + +const UserNotificationPreferenceSchema = new mongoose.Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + unique: true + }, + preferences: { + inApp: { type: Boolean, default: true }, + email: { type: Boolean, default: false }, + push: { type: Boolean, default: false }, + sms: { type: Boolean, default: false } + }, + doNotDisturb: { + enabled: { type: Boolean, default: false }, + startTime: String, // "22:00" + endTime: String // "08:00" + } }); -module.exports = mongoose.model("Notification", NotificationSchema); \ No newline at end of file +const Notification = mongoose.model("Notification", NotificationSchema); +const UserNotificationPreference = mongoose.model("UserNotificationPreference", UserNotificationPreferenceSchema); + +module.exports = { Notification, UserNotificationPreference }; \ No newline at end of file diff --git a/src/Components/HMI/ui/node_modules/.package-lock.json b/src/Components/HMI/ui/node_modules/.package-lock.json index 85a2ddf4b..9c962e712 100644 --- a/src/Components/HMI/ui/node_modules/.package-lock.json +++ b/src/Components/HMI/ui/node_modules/.package-lock.json @@ -655,6 +655,15 @@ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@petamoriken/float16": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.0.tgz", @@ -1227,6 +1236,21 @@ "node": ">=14.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/disposable-email-domains": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/disposable-email-domains/-/disposable-email-domains-1.0.4.tgz", @@ -1251,16 +1275,17 @@ "integrity": "sha512-5j/lXt7unfPOUlrKC34HIaedONleyLtwkKggiD/0uuMfT8gg2EOpg0dz4lCD15Ga7muC+1WzJZAjIB9simWd6Q==" }, "node_modules/@types/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" }, "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", "dependencies": { - "@types/node": "*", "@types/webidl-conversions": "*" } }, @@ -1286,6 +1311,15 @@ "node": ">= 0.6" } }, + "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/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1317,6 +1351,18 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1356,6 +1402,15 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -1374,6 +1429,12 @@ "node": ">=8" } }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -1729,6 +1790,61 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2007,6 +2123,15 @@ "node": ">=16.0.0" } }, + "node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2022,6 +2147,36 @@ "node": ">= 0.8" } }, + "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/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2239,7 +2394,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true + "license": "MIT" }, "node_modules/merge-descriptors": { "version": "1.0.1", @@ -2284,6 +2439,12 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2304,29 +2465,68 @@ } }, "node_modules/mongodb": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.16.0.tgz", - "integrity": "sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", + "license": "Apache-2.0", "dependencies": { - "bson": "^4.7.2", - "mongodb-connection-string-url": "^2.5.4", - "socks": "^2.7.1" + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" }, "engines": { - "node": ">=12.9.0" + "node": ">=16.20.1" }, - "optionalDependencies": { - "@aws-sdk/credential-providers": "^3.186.0", - "saslprep": "^1.0.3" + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } } }, "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongodb/node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" } }, "node_modules/mongoose": { @@ -2350,6 +2550,69 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.16.0.tgz", + "integrity": "sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==", + "license": "Apache-2.0", + "dependencies": { + "bson": "^4.7.2", + "mongodb-connection-string-url": "^2.5.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=12.9.0" + }, + "optionalDependencies": { + "@aws-sdk/credential-providers": "^3.186.0", + "saslprep": "^1.0.3" + } + }, + "node_modules/mongoose/node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongoose/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongoose/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -2611,9 +2874,10 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2755,6 +3019,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "license": "MIT", "optional": true, "dependencies": { "sparse-bitfield": "^3.0.3" @@ -2962,6 +3227,98 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -3007,7 +3364,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, + "license": "MIT", "dependencies": { "memory-pager": "^1.0.2" } @@ -3163,14 +3520,15 @@ } }, "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/tslib": { @@ -3251,6 +3609,46 @@ "node": ">= 0.8" } }, + "node_modules/web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "license": "MPL-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "bin": { + "web-push": "src/cli.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/web-push/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/web-push/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/web-worker": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", @@ -3260,20 +3658,43 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/xml-utils": { diff --git a/src/Components/HMI/ui/package-lock.json b/src/Components/HMI/ui/package-lock.json index 92ce3fa5b..48a8198a6 100644 --- a/src/Components/HMI/ui/package-lock.json +++ b/src/Components/HMI/ui/package-lock.json @@ -22,6 +22,7 @@ "helmet": "^7.0.0", "jquery": "^3.6.4", "jsonwebtoken": "^9.0.1", + "mongodb": "^6.18.0", "mongoose": "^6.10.0", "nodemailer": "^6.9.1", "nodemon": "^2.0.22", @@ -31,7 +32,9 @@ "requirejs": "^2.3.6", "serve-index": "^1.9.1", "simplebar": "^6.2.5", - "stripe": "^13.4.0" + "socket.io": "^4.8.1", + "stripe": "^13.4.0", + "web-push": "^3.6.7" } }, "node_modules/@aws-crypto/crc32": { @@ -685,6 +688,15 @@ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@petamoriken/float16": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.0.tgz", @@ -1257,6 +1269,21 @@ "node": ">=14.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/disposable-email-domains": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/disposable-email-domains/-/disposable-email-domains-1.0.4.tgz", @@ -1281,16 +1308,17 @@ "integrity": "sha512-5j/lXt7unfPOUlrKC34HIaedONleyLtwkKggiD/0uuMfT8gg2EOpg0dz4lCD15Ga7muC+1WzJZAjIB9simWd6Q==" }, "node_modules/@types/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" }, "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", "dependencies": { - "@types/node": "*", "@types/webidl-conversions": "*" } }, @@ -1316,6 +1344,15 @@ "node": ">= 0.6" } }, + "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/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1347,6 +1384,18 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1386,6 +1435,15 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -1404,6 +1462,12 @@ "node": ">=8" } }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -1759,6 +1823,61 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2050,6 +2169,15 @@ "node": ">=16.0.0" } }, + "node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2065,6 +2193,36 @@ "node": ">= 0.8" } }, + "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/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2282,7 +2440,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true + "license": "MIT" }, "node_modules/merge-descriptors": { "version": "1.0.1", @@ -2327,6 +2485,12 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2347,29 +2511,68 @@ } }, "node_modules/mongodb": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.16.0.tgz", - "integrity": "sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", + "license": "Apache-2.0", "dependencies": { - "bson": "^4.7.2", - "mongodb-connection-string-url": "^2.5.4", - "socks": "^2.7.1" + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" }, "engines": { - "node": ">=12.9.0" + "node": ">=16.20.1" }, - "optionalDependencies": { - "@aws-sdk/credential-providers": "^3.186.0", - "saslprep": "^1.0.3" + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } } }, "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongodb/node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" } }, "node_modules/mongoose": { @@ -2393,6 +2596,69 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.16.0.tgz", + "integrity": "sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==", + "license": "Apache-2.0", + "dependencies": { + "bson": "^4.7.2", + "mongodb-connection-string-url": "^2.5.4", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=12.9.0" + }, + "optionalDependencies": { + "@aws-sdk/credential-providers": "^3.186.0", + "saslprep": "^1.0.3" + } + }, + "node_modules/mongoose/node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongoose/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongoose/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -2654,9 +2920,10 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2798,6 +3065,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "license": "MIT", "optional": true, "dependencies": { "sparse-bitfield": "^3.0.3" @@ -3005,6 +3273,98 @@ "npm": ">= 3.0.0" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -3050,7 +3410,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, + "license": "MIT", "dependencies": { "memory-pager": "^1.0.2" } @@ -3206,14 +3566,15 @@ } }, "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/tslib": { @@ -3294,6 +3655,46 @@ "node": ">= 0.8" } }, + "node_modules/web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "license": "MPL-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "bin": { + "web-push": "src/cli.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/web-push/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/web-push/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/web-worker": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", @@ -3303,20 +3704,43 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/xml-utils": { @@ -3911,6 +4335,14 @@ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, + "@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "@petamoriken/float16": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.0.tgz", @@ -4371,6 +4803,19 @@ "tslib": "^2.5.0" } }, + "@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "requires": { + "@types/node": "*" + } + }, "@types/disposable-email-domains": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@types/disposable-email-domains/-/disposable-email-domains-1.0.4.tgz", @@ -4395,16 +4840,15 @@ "integrity": "sha512-5j/lXt7unfPOUlrKC34HIaedONleyLtwkKggiD/0uuMfT8gg2EOpg0dz4lCD15Ga7muC+1WzJZAjIB9simWd6Q==" }, "@types/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, "@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", "requires": { - "@types/node": "*", "@types/webidl-conversions": "*" } }, @@ -4427,6 +4871,11 @@ "negotiator": "0.6.3" } }, + "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==" + }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -4455,6 +4904,17 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4480,6 +4940,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -4495,6 +4960,11 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, + "bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, "body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -4765,6 +5235,42 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "requires": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4975,6 +5481,11 @@ "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.0.0.tgz", "integrity": "sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==" }, + "http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==" + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4987,6 +5498,25 @@ "toidentifier": "1.0.1" } }, + "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==", + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5150,8 +5680,7 @@ "memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, "merge-descriptors": { "version": "1.0.1", @@ -5181,6 +5710,11 @@ "mime-db": "1.52.0" } }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5195,24 +5729,29 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mongodb": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.16.0.tgz", - "integrity": "sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", "requires": { - "@aws-sdk/credential-providers": "^3.186.0", - "bson": "^4.7.2", - "mongodb-connection-string-url": "^2.5.4", - "saslprep": "^1.0.3", - "socks": "^2.7.1" + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "dependencies": { + "bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" + } } }, "mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "requires": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "mongoose": { @@ -5227,6 +5766,55 @@ "mquery": "4.0.3", "ms": "2.1.3", "sift": "16.0.1" + }, + "dependencies": { + "@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "requires": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "mongodb": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.16.0.tgz", + "integrity": "sha512-0EB113Fsucaq1wsY0dOhi1fmZOwFtLOtteQkiqOXGklvWMnSH3g2QS53f0KTP+/6qOkuoXE2JksubSZNmxeI+g==", + "requires": { + "@aws-sdk/credential-providers": "^3.186.0", + "bson": "^4.7.2", + "mongodb-connection-string-url": "^2.5.4", + "saslprep": "^1.0.3", + "socks": "^2.7.1" + } + }, + "mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "requires": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } } }, "mpath": { @@ -5419,9 +6007,9 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "qs": { "version": "6.11.0", @@ -5696,6 +6284,68 @@ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" }, + "socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, "socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -5728,7 +6378,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, "requires": { "memory-pager": "^1.0.2" } @@ -5847,11 +6496,11 @@ } }, "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "requires": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" } }, "tslib": { @@ -5915,6 +6564,39 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "requires": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "dependencies": { + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, "web-worker": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", @@ -5926,14 +6608,20 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" }, "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "requires": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + }, "xml-utils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.7.0.tgz", diff --git a/src/Components/HMI/ui/package.json b/src/Components/HMI/ui/package.json index c4acef7fe..14367ffbf 100644 --- a/src/Components/HMI/ui/package.json +++ b/src/Components/HMI/ui/package.json @@ -23,6 +23,7 @@ "helmet": "^7.0.0", "jquery": "^3.6.4", "jsonwebtoken": "^9.0.1", + "mongodb": "^6.18.0", "mongoose": "^6.10.0", "nodemailer": "^6.9.1", "nodemon": "^2.0.22", @@ -32,6 +33,8 @@ "requirejs": "^2.3.6", "serve-index": "^1.9.1", "simplebar": "^6.2.5", - "stripe": "^13.4.0" + "socket.io": "^4.8.1", + "stripe": "^13.4.0", + "web-push": "^3.6.7" } } diff --git a/src/Components/HMI/ui/public/admin/css/notifications.css b/src/Components/HMI/ui/public/admin/css/notifications.css index 73e523d91..997fa591d 100644 --- a/src/Components/HMI/ui/public/admin/css/notifications.css +++ b/src/Components/HMI/ui/public/admin/css/notifications.css @@ -70,4 +70,99 @@ justify-content: space-between; align-items: center; margin-bottom: 1.5rem; +} + +.notification-list { + max-height: 70vh; + overflow-y: auto; + padding-right: 10px; +} + +.notification-item { + padding: 15px; + border-radius: 8px; + margin-bottom: 10px; + transition: all 0.3s ease; + border-left: 4px solid transparent; +} + +.notification-item.unread { + background-color: #f8f9fa; + border-left-color: #0d6efd; +} + +.notification-item:hover { + background-color: #f1f1f1; +} + +.notification-icon { + font-size: 1.5rem; + color: #6c757d; + margin-right: 15px; +} + +.notification-content { + flex: 1; +} + +.notification-title { + font-weight: 600; + margin-bottom: 5px; + color: #212529; +} + +.notification-message { + color: #6c757d; + margin-bottom: 5px; +} + +.notification-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; +} + +.notification-time { + color: #adb5bd; +} + +.notification-actions { + display: flex; + gap: 5px; +} + +.empty-notifications { + text-align: center; + padding: 40px 20px; + color: #6c757d; +} + +.empty-notifications i { + font-size: 3rem; + margin-bottom: 15px; + color: #dee2e6; +} + +.empty-notifications h5 { + margin-bottom: 5px; +} + +/* Custom scrollbar */ +.notification-list::-webkit-scrollbar { + width: 8px; +} + +.notification-list::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.notification-list::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 10px; +} + +.notification-list::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; } \ No newline at end of file diff --git a/src/Components/HMI/ui/public/admin/js/notifications.js b/src/Components/HMI/ui/public/admin/js/notifications.js index 8876a0376..9a819a936 100644 --- a/src/Components/HMI/ui/public/admin/js/notifications.js +++ b/src/Components/HMI/ui/public/admin/js/notifications.js @@ -1,107 +1,201 @@ $(document).ready(function() { - // Dummy notifications for testing - let notifications = [ - { - id: 1, - message: "New donation received from Jon Doe", - date: new Date(), - read: false, - icon: "ti ti-receipt-2", - type: "donation" - }, - { - id: 2, - message: "New request from Jon Doe", - date: new Date(Date.now() - 3600000), // 1 hour ago - read: false, - icon: "ti ti-alert-circle", - type: "request" - }, - { - id: 3, - message: "New user registration: jondoe@example.com", - date: new Date(Date.now() - 172800000), // 2 days ago - read: false, - icon: "ti ti-user", - type: "user" + // Connect to Socket.io + const socket = io({ + auth: { + token: localStorage.getItem('token') // Assuming JWT is stored here } - ]; + }); + + let notifications = []; + let unreadCount = 0; + + // DOM Elements + const notificationList = $("#notification-list"); + const unreadBadge = $("#unread-badge"); + const markAllReadBtn = $("#mark-all-read"); + const loadMoreBtn = $("#load-more"); + let isLoading = false; + let offset = 0; + const limit = 10; + + // Initialize + loadNotifications(); + setupSocketListeners(); + setupEventHandlers(); + + function loadNotifications() { + if (isLoading) return; + + isLoading = true; + loadMoreBtn.prop('disabled', true).html(' Loading...'); + + $.ajax({ + url: `/api/notifications?limit=${limit}&offset=${offset}`, + headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }, + success: function(response) { + if (offset === 0) { + notifications = response.notifications; + renderNotifications(); + } else { + notifications = [...notifications, ...response.notifications]; + appendNotifications(response.notifications); + } + + updateUnreadCount(); + offset += limit; + + if (offset >= response.total) { + loadMoreBtn.hide(); + } + }, + complete: function() { + isLoading = false; + loadMoreBtn.prop('disabled', false).html('Load More'); + } + }); + } - // Render notifications function renderNotifications() { - const notificationContainer = $("#notification-list"); - notificationContainer.empty(); + notificationList.empty(); if (notifications.length === 0) { - notificationContainer.html(` + notificationList.html(`
-
No notifications
+
No notifications yet

You're all caught up!

`); return; } - // Sort by date (newest first) - notifications.sort((a, b) => b.date - a.date); - notifications.forEach(notification => { - const timeAgo = moment(notification.date).fromNow(); - const formattedDate = moment(notification.date).format('MMM D, YYYY h:mm A'); - const notificationItem = ` -
-
- -
-
-

${notification.message}

-

${timeAgo} (${formattedDate})

+ notificationList.append(createNotificationElement(notification)); + }); + } + + function appendNotifications(newNotifications) { + newNotifications.forEach(notification => { + notificationList.append(createNotificationElement(notification)); + }); + } + + function createNotificationElement(notification) { + const timeAgo = moment(notification.createdAt).fromNow(); + const isUnread = notification.status === 'unread'; + + return ` +
+
+ +
+
+
${notification.title}
+

${notification.message}

+
- `; - notificationContainer.append(notificationItem); - }); +
+ `; + } + + function updateUnreadCount() { + unreadCount = notifications.filter(n => n.status === 'unread').length; + unreadBadge.text(unreadCount).toggle(unreadCount > 0); } - // Mark notification as read - $(document).on("click", ".mark-read", function () { - const notificationId = $(this).closest(".notification-item").data("id"); - const notification = notifications.find(n => n.id === notificationId); - if (notification) { - notification.read = true; + function setupSocketListeners() { + // Initial notifications + socket.on('initialNotifications', (initialNotifications) => { + notifications = initialNotifications; renderNotifications(); - } - }); + updateUnreadCount(); + }); - // Delete notification - $(document).on("click", ".delete-notification", function () { - const notificationId = $(this).closest(".notification-item").data("id"); - notifications = notifications.filter(n => n.id !== notificationId); - renderNotifications(); - }); + // New notification + socket.on('newNotification', (newNotification) => { + notifications.unshift(newNotification); - // Mark all as read - $("#mark-all-read").on("click", function () { - notifications.forEach(notification => { - notification.read = true; + // If first page, prepend the new notification + if (offset === 0) { + notificationList.prepend(createNotificationElement(newNotification)); + } + + updateUnreadCount(); + + // Show toast notification + showToastNotification(newNotification); }); - renderNotifications(); - }); - // Delete all read notifications - $("#delete-all-read").on("click", function () { - notifications = notifications.filter(n => !n.read); - renderNotifications(); - }); + // Notification updated (e.g., marked as read) + socket.on('notificationUpdated', (updatedNotification) => { + const index = notifications.findIndex(n => n._id === updatedNotification._id); + if (index !== -1) { + notifications[index] = updatedNotification; + $(`.notification-item[data-id="${updatedNotification._id}"]`) + .removeClass('unread') + .addClass('read') + .find('.mark-read') + .removeClass('btn-outline-primary') + .addClass('btn-outline-secondary') + .text('Read'); + + updateUnreadCount(); + } + }); + } + + function setupEventHandlers() { + // Mark as read + notificationList.on('click', '.mark-read', function() { + const notificationId = $(this).closest('.notification-item').data('id'); + socket.emit('markAsRead', notificationId); + }); - renderNotifications(); -}); + // Mark all as read + markAllReadBtn.on('click', function() { + $.ajax({ + url: '/api/notifications/read-all', + method: 'PATCH', + headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }, + success: function() { + notifications.forEach(n => n.status = 'read'); + $('.notification-item').removeClass('unread').addClass('read'); + $('.mark-read') + .removeClass('btn-outline-primary') + .addClass('btn-outline-secondary') + .text('Read'); + updateUnreadCount(); + } + }); + }); + + // Load more + loadMoreBtn.on('click', loadNotifications); + } + + function showToastNotification(notification) { + // Using Toastify for toast notifications + Toastify({ + text: `${notification.title}: ${notification.message}`, + duration: 5000, + gravity: "bottom", + position: "right", + backgroundColor: notification.type === 'alert' ? '#dc3545' : '#28a745', + onClick: function() { + if (notification.link) { + window.location.href = notification.link; + } + } + }).showToast(); + } +}); \ No newline at end of file diff --git a/src/Components/HMI/ui/public/admin/notifications.html b/src/Components/HMI/ui/public/admin/notifications.html index 5f58f5f34..abb7277ec 100644 --- a/src/Components/HMI/ui/public/admin/notifications.html +++ b/src/Components/HMI/ui/public/admin/notifications.html @@ -6,12 +6,13 @@ Notifications - + - - - + + + + @@ -28,19 +29,20 @@
-
-

Notifications

+
+

Notifications 0

-
+ -
+
@@ -56,5 +58,6 @@

Notifications

$("#header").load("./admin/component/header-component.html"); }); + \ No newline at end of file diff --git a/src/Components/HMI/ui/public/sw.js b/src/Components/HMI/ui/public/sw.js new file mode 100644 index 000000000..a9feabf6b --- /dev/null +++ b/src/Components/HMI/ui/public/sw.js @@ -0,0 +1,53 @@ +const CACHE_NAME = 'echo-notifications-v1'; +const OFFLINE_URL = '/offline.html'; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + return cache.add(OFFLINE_URL); + }) + ); +}); + +self.addEventListener('fetch', (event) => { + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request) + .catch(() => { + return caches.match(OFFLINE_URL); + }) + ); + } +}); + +self.addEventListener('push', (event) => { + const data = event.data.json(); + + const options = { + body: data.body, + icon: data.icon || '/images/tabIcons/logo.png', + badge: '/images/tabIcons/logo.png', + data: { + url: data.url || '/' + } + }; + + event.waitUntil( + self.registration.showNotification(data.title, options) + ); +}); + +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + + event.waitUntil( + clients.matchAll({ type: 'window' }) + .then((clientList) => { + if (clientList.length > 0) { + return clientList[0].focus(); + } + return clients.openWindow(event.notification.data.url); + }) + ); +}); \ No newline at end of file diff --git a/src/Components/HMI/ui/server.js b/src/Components/HMI/ui/server.js index 6e8af55f7..f7a16573d 100644 --- a/src/Components/HMI/ui/server.js +++ b/src/Components/HMI/ui/server.js @@ -9,24 +9,50 @@ const { client, checkUserSession } = require('./middleware'); const controller = require('./controller/auth.controller'); const crypto = require('crypto'); const bcrypt = require('bcryptjs'); -client.connect(); const cors = require('cors'); require('dotenv').config(); const stripe = require('stripe')(process.env.STRIPE_PRIVATE_KEY); const axios = require('axios'); const { MongoClient, ObjectId } = require('mongodb'); - - +const mongoose = require('mongoose'); +const { Server } = require('socket.io'); +const http = require('http'); +const webpush = require('web-push'); +const { User } = require('./model/user.model'); +const { Notification, UserNotificationPreference } = require('./model/notification.model'); // Temporarily removed the variables that need the captcha-canvas package //const {createCaptchaSync} = require("captcha-canvas"); +// Initialize HTTP server for Socket.io +const server = http.createServer(app); + +// Connect to Redis +client.connect(); + +// MongoDB Connection +mongoose.set('strictQuery', false); + +const connectWithRetry = () => { + mongoose.connect('mongodb://root:root_password@ts-mongodb-cont:27017/EchoNet?authSource=admin', { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 45000 + }) + .then(() => console.log('MongoDB connection established')) + .catch(err => { + console.error('MongoDB connection error:', err); + setTimeout(connectWithRetry, 5000); + }); +}; + +connectWithRetry(); const port = 8080; const rootDirectory = __dirname; // This assumes the root directory is the current directory - //Security verification for email account and body content validation: const validation = require('deep-email-validator') @@ -35,25 +61,22 @@ const storeItems = new Map([[ ]]) app.use(express.json({limit: '10mb'})); -// Import the User model -const { User } = require('./model/user.model'); // Add this line - // Middleware to check if the user is an admin function isAdmin(req, res, next) { const token = req.headers.authorization.split(' ')[1]; const decoded = jwt.verify(token, process.env.JWT_SECRET); - + if (decoded.role !== 'admin') { return res.status(403).json({ message: 'Access denied: Admins only' }); } - + next(); } // API to suspend a user app.patch('/api/users/:id/suspend', isAdmin, async (req, res) => { const userId = req.params.id; - + try { const user = await User.findByIdAndUpdate(userId, { status: 'suspended' }, { new: true }); res.json({ message: `User ${user.email} suspended`, user }); @@ -65,7 +88,7 @@ app.patch('/api/users/:id/suspend', isAdmin, async (req, res) => { // API to ban a user app.patch('/api/users/:id/ban', isAdmin, async (req, res) => { const userId = req.params.id; - + try { const user = await User.findByIdAndUpdate(userId, { status: 'banned' }, { new: true }); res.json({ message: `User ${user.email} banned`, user }); @@ -77,7 +100,7 @@ app.patch('/api/users/:id/ban', isAdmin, async (req, res) => { // API to reinstate a user app.patch('/api/users/:id/reinstate', isAdmin, async (req, res) => { const userId = req.params.id; - + try { const user = await User.findByIdAndUpdate(userId, { status: 'active' }, { new: true }); res.json({ message: `User ${user.email} reinstated`, user }); @@ -89,7 +112,7 @@ app.patch('/api/users/:id/reinstate', isAdmin, async (req, res) => { // API to retrieve user status app.get('/api/users/:id/status', isAdmin, async (req, res) => { const userId = req.params.id; - + try { const user = await User.findById(userId, 'email status'); res.json({ user }); @@ -166,7 +189,7 @@ app.post("/api/create-checkout-session", async (req, res) => { }); firstPage = true; } - + if (!charges.has_more) { break; // Exit the loop when there are no more pages } @@ -185,24 +208,19 @@ app.post("/api/create-checkout-session", async (req, res) => { } }) */ +const Donation = require('./model/donation.model'); + app.get('/donations', async (req, res) => { try { - const client = new MongoClient("mongodb://modelUser:EchoNetAccess2023@ts-mongodb-cont:27017/EchoNet", - { - useNewUrlParser: true, - useUnifiedTopology: true - }); - await client.connect(); - const db = client.db("EchoNet"); - const donations = await db.collection("donations").find({}).toArray(); + const donations = await Donation.find({}); res.json({ charges: { data: donations } }); } catch (error) { - console.error("Error fetching donations from MongoDB:", error); + console.error("Error fetching donations:", error); res.status(500).json({ error: "Internal server error" }); } }); - - + + //This endpoint retrieves the donation amounts from the associated stripe account //it adds up the amounts to return a cumulative total. Used on admin dashboard. app.get('/cumulativeDonations', async(req, res) => { @@ -218,7 +236,7 @@ app.get('/cumulativeDonations', async(req, res) => { }); firstPage = true; } - + charges.data.forEach(charge => { cumulativeTotal += charge.amount; }); @@ -232,17 +250,17 @@ app.get('/cumulativeDonations', async(req, res) => { }); firstPage = true; } - + cumulativeTotal = cumulativeTotal / 100; cumulativeTotal = cumulativeTotal.toFixed(2); - + console.log('Cumulative Total:', cumulativeTotal); res.json({ cumulativeTotal }); } catch(error){ console.error('Error:', error); res.status(500).json({ error: 'Internal server error' }); - } + } }) // Temporarily removed the route for the captcha function @@ -270,7 +288,7 @@ const bodyParser = require("express"); app.use(bodyParser.json({ limit: '10mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })) -//const serveIndex = require('serve-index'); +//const serveIndex = require('serve-index'); //app.use('/images/bio', serveIndex(express.static(path.join(__dirname, '/images/bio')))); app.use( @@ -300,7 +318,7 @@ function escapeHtmlEntities(input) { async function testEmail(input) { let res = await validation.validate(input) return {result: res.valid, response: res.validators} - + } app.post("/send_email", async (req, res) => { @@ -342,7 +360,7 @@ app.post("/send_email", async (req, res) => { } else { return res.status(400).send(""); } - + } ); @@ -623,7 +641,7 @@ app.get('/api/requests', async (req, res) => { }) const axiosResponse = await axios.get('http://ts-api-cont:9000/hmi/requests', { headers: {"Authorization" : `Bearer ${token}`}}) - + if (axiosResponse.status === 200) { res.json(axiosResponse.data); } else { @@ -678,8 +696,8 @@ const suspendOrBlockUser = async (identifier, action) => { const usersCollection = userdb.collection('users'); // Determine the search criteria based on whether the identifier is an email or user ID - const query = ObjectId.isValid(identifier) - ? { _id: new ObjectId(identifier) } + const query = ObjectId.isValid(identifier) + ? { _id: new ObjectId(identifier) } : { email: identifier }; // Determine the status and message based on the action @@ -737,72 +755,7 @@ app.get("/map", async(req,res) => { res.sendFile(path.join(__dirname, 'public/index.html')) }) -// Get all notifications for current user -app.get('/api/notifications', async (req, res) => { - try { - const token = req.headers.authorization?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'Unauthorized' }); - - const decoded = jwt.verify(token, process.env.JWT_SECRET); - const notifications = await Notification.find({ userId: decoded.id }) - .sort({ createdAt: -1 }) - .limit(50); - - res.json(notifications); - } catch (error) { - res.status(500).json({ error: 'Error fetching notifications' }); - } -}); - -// Mark notification as read -app.patch('/api/notifications/:id/read', async (req, res) => { - try { - await Notification.findByIdAndUpdate(req.params.id, { read: true }); - res.json({ success: true }); - } catch (error) { - res.status(500).json({ error: 'Error updating notification' }); - } -}); - -// Mark all notifications as read -app.patch('/api/notifications/read-all', async (req, res) => { - try { - const token = req.headers.authorization?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'Unauthorized' }); - - const decoded = jwt.verify(token, process.env.JWT_SECRET); - await Notification.updateMany( - { userId: decoded.id, read: false }, - { $set: { read: true } } - ); - - res.json({ success: true }); - } catch (error) { - res.status(500).json({ error: 'Error updating notifications' }); - } -}); - -// Create a notification (helper function) -async function createNotification(userId, message, type, link = null, icon = null) { - return await Notification.create({ - userId, - message, - type, - link, - icon: icon || getDefaultIcon(type) - }); -} - -function getDefaultIcon(type) { - const icons = { - donation: 'ti ti-receipt-2', - request: 'ti ti-alert-circle', - user: 'ti ti-user', - system: 'ti ti-bell' - }; - return icons[type] || 'ti ti-bell'; -} - +console.log('test'); // start the server app.listen(port, () => { diff --git a/src/Components/HMI/ui/services/dispatchService.js b/src/Components/HMI/ui/services/dispatchService.js new file mode 100644 index 000000000..0b54b2ce3 --- /dev/null +++ b/src/Components/HMI/ui/services/dispatchService.js @@ -0,0 +1,52 @@ +const { UserNotificationPreference } = require('../model/user.model'); +const axios = require('axios'); + +class DispatchService { + constructor() { + this.channels = { + inApp: this.sendInApp, + email: this.sendEmail, + push: this.sendPush, + sms: this.sendSms + }; + } + + async dispatch(userId, notification, channel = null) { + try { + const preferences = await UserNotificationPreference.findOne({ userId }) || + new UserNotificationPreference({ userId }); + + if (channel) { + if (preferences.preferences[channel]) { + await this.channels[channel](userId, notification); + } + } else { + for (const [channelName, isEnabled] of Object.entries(preferences.preferences)) { + if (isEnabled && this.channels[channelName]) { + await this.channels[channelName](userId, notification); + } + } + } + } catch (error) { + console.error(`Error dispatching notification to user ${userId}:`, error); + } + } + + async sendInApp(userId, notification) { + // Handled by WebSocket + } + + async sendEmail(userId, notification) { + // Implement email sending + } + + async sendPush(userId, notification) { + // Implement push notification sending + } + + async sendSms(userId, notification) { + // Implement SMS sending + } +} + +module.exports = new DispatchService(); \ No newline at end of file diff --git a/src/Components/HMI/ui/services/notificationService.js b/src/Components/HMI/ui/services/notificationService.js new file mode 100644 index 000000000..0d8df94f7 --- /dev/null +++ b/src/Components/HMI/ui/services/notificationService.js @@ -0,0 +1,151 @@ +const { Notification, UserNotificationPreference } = require('../model/user.model'); +const axios = require('axios'); +const webpush = require('web-push'); + +class NotificationService { + constructor() { + // Initialize web push (for browser notifications) + webpush.setVapidDetails( + `mailto:${process.env.VAPID_EMAIL}`, + process.env.VAPID_PUBLIC_KEY, + process.env.VAPID_PRIVATE_KEY + ); + } + + async createNotification(userId, title, message, type, metadata = {}) { + try { + const notification = new Notification({ + userId, + title, + message, + type, + metadata, + icon: this.getIconForType(type), + expiresAt: this.getExpiryForType(type) + }); + + await notification.save(); + + // Dispatch to all channels based on user preferences + await this.dispatchNotification(notification); + + return notification; + } catch (error) { + console.error('Error creating notification:', error); + throw error; + } + } + + async dispatchNotification(notification) { + try { + const preferences = await UserNotificationPreference.findOne({ userId: notification.userId }) || + new UserNotificationPreference({ userId: notification.userId }); + + // Check if in do-not-disturb window + if (this.isInDoNotDisturb(preferences)) { + console.log(`Notification suppressed for user ${notification.userId} during DND hours`); + return; + } + + // In-app notification (always sent as it's stored in DB) + if (preferences.preferences.inApp) { + this.sendInAppNotification(notification); + } + + // Email notification + if (preferences.preferences.email) { + this.sendEmailNotification(notification); + } + + // Push notification + if (preferences.preferences.push) { + this.sendPushNotification(notification); + } + + // SMS notification (would require SMS service integration) + if (preferences.preferences.sms) { + this.sendSmsNotification(notification); + } + } catch (error) { + console.error('Error dispatching notification:', error); + } + } + + sendInAppNotification(notification) { + // This will be handled by the WebSocket connection + console.log(`In-app notification sent for ${notification.userId}`); + } + + async sendEmailNotification(notification) { + // Implement email sending logic using your email service + console.log(`Email notification sent for ${notification.userId}`); + } + + async sendPushNotification(notification) { + try { + // In a real app, you would look up the user's subscription + // const subscription = await getPushSubscription(notification.userId); + // await webpush.sendNotification(subscription, JSON.stringify({ + // title: notification.title, + // body: notification.message, + // icon: notification.icon, + // data: { url: notification.link } + // })); + console.log(`Push notification sent for ${notification.userId}`); + } catch (error) { + console.error('Error sending push notification:', error); + } + } + + sendSmsNotification(notification) { + // Implement SMS sending logic + console.log(`SMS notification sent for ${notification.userId}`); + } + + getIconForType(type) { + const icons = { + donation: 'ti ti-receipt-2', + request: 'ti ti-alert-circle', + user: 'ti ti-user', + system: 'ti ti-bell', + alert: 'ti ti-alert-triangle' + }; + return icons[type] || 'ti ti-bell'; + } + + getExpiryForType(type) { + const expiryDays = { + donation: 30, // 30 days for donations + request: 7, // 1 week for requests + user: 14, // 2 weeks for user notifications + system: null, // Never expire for system notifications + alert: 1 // 1 day for alerts + }; + + const days = expiryDays[type] || 7; + return days ? new Date(Date.now() + days * 24 * 60 * 60 * 1000) : null; + } + + isInDoNotDisturb(preferences) { + if (!preferences.doNotDisturb.enabled) return false; + + const now = new Date(); + const [startHour, startMinute] = preferences.doNotDisturb.startTime.split(':').map(Number); + const [endHour, endMinute] = preferences.doNotDisturb.endTime.split(':').map(Number); + + const startTime = new Date(); + startTime.setHours(startHour, startMinute, 0, 0); + + const endTime = new Date(); + endTime.setHours(endHour, endMinute, 0, 0); + + // Handle overnight DND (e.g., 22:00 to 08:00) + if (startTime > endTime) { + return now >= startTime || now <= endTime; + } + + return now >= startTime && now <= endTime; + } +} + +module.exports = new NotificationService(); \ No newline at end of file diff --git a/src/Components/docker-compose.yml b/src/Components/docker-compose.yml index 8f065e972..d93f2b424 100644 --- a/src/Components/docker-compose.yml +++ b/src/Components/docker-compose.yml @@ -58,6 +58,11 @@ services: tty: true environment: - NODE_ENV=development + - MONGO_HOST=ts-mongodb-cont + - MONGO_PORT=27017 + - MONGO_DB=EchoNet + - MONGO_USER=root + - MONGO_PASS=root_password echo_mqtt: diff --git a/src/Components/package-lock.json b/src/Components/package-lock.json index e147fb736..1de6c5d95 100644 --- a/src/Components/package-lock.json +++ b/src/Components/package-lock.json @@ -2,5 +2,1403 @@ "name": "Components", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "dependencies": { + "@google-cloud/recaptcha-enterprise": "^5.7.0", + "socket.io": "^4.8.1" + } + }, + "node_modules/@google-cloud/recaptcha-enterprise": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@google-cloud/recaptcha-enterprise/-/recaptcha-enterprise-5.13.0.tgz", + "integrity": "sha512-kIGnUfrAroVkjWD3M+NWaqTuwu0YEjIDulY8YVBozHiJaeztK+5mvgV5MKrwLYyqyK9JuswLXOLAnSc57hUcHA==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^4.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "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", + "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/@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", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "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", + "engines": { + "node": ">= 10" + } + }, + "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" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "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", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.5" + } + }, + "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" + }, + "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", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "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/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/cliui": { + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "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", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=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", + "engines": { + "node": ">=6" + } + }, + "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/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", + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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-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", + "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-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", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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", + "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", + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "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" + }, + "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" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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", + "engines": { + "node": ">= 0.6" + } + }, + "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", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "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", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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", + "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", + "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/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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", + "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" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "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", + "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", + "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", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } } diff --git a/src/Components/package.json b/src/Components/package.json index c9bddedd1..e755efa6a 100644 --- a/src/Components/package.json +++ b/src/Components/package.json @@ -1,5 +1,6 @@ { "dependencies": { - "@google-cloud/recaptcha-enterprise": "^5.7.0" + "@google-cloud/recaptcha-enterprise": "^5.7.0", + "socket.io": "^4.8.1" } } From e386b69b17138c1c77dab870572793a556a01c33 Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 16 Aug 2025 23:27:19 +1000 Subject: [PATCH 04/11] add backend notification --- .idea/workspace.xml | 53 +- .../HMI/ui/node_modules/.package-lock.json | 27 + src/Components/HMI/ui/package-lock.json | 41 + src/Components/HMI/ui/package.json | 1 + .../admin/component/sidebar-component.html | 2 +- .../HMI/ui/public/admin/js/notifications.js | 95 +- .../HMI/ui/public/admin/notifications.html | 2 +- src/Components/HMI/ui/server.js | 947 +++++++----------- .../HMI/ui/services/dispatchService.js | 7 +- .../HMI/ui/services/notificationService.js | 4 +- 10 files changed, 579 insertions(+), 600 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index e1c589ea0..35005b32a 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,23 +4,17 @@
@@ -219,6 +222,13 @@ $(document).ready(function() { updateUnreadCount(); } }); + + // Handle notification deletion from other devices + socket.on('notificationDeleted', (notificationId) => { + notifications = notifications.filter(n => n._id !== notificationId); + $(`.notification-item[data-id="${notificationId}"]`).remove(); + updateUnreadCount(); + }); } function setupEventHandlers() { @@ -248,6 +258,35 @@ $(document).ready(function() { // Load more loadMoreBtn.on('click', loadNotifications); + + // Delete notification + notificationList.on('click', '.delete-notification', function(e) { + e.stopPropagation(); + const notificationId = $(this).closest('.notification-item').data('id'); + deleteNotification(notificationId); + }); + } + + function deleteNotification(notificationId) { + if (!confirm('Are you sure you want to delete this notification?')) return; + + $.ajax({ + url: `/api/notifications/${notificationId}`, + method: 'DELETE', + headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }, + success: function() { + // Remove from local array + notifications = notifications.filter(n => n._id !== notificationId); + // Remove from DOM + $(`.notification-item[data-id="${notificationId}"]`).remove(); + updateUnreadCount(); + showSuccessToast('Notification deleted'); + }, + error: function(xhr) { + console.error('Delete error:', xhr.responseText); + showErrorToast('Failed to delete notification'); + } + }); } function showToastNotification(notification) { diff --git a/src/Components/HMI/ui/server.js b/src/Components/HMI/ui/server.js index cec8aa75f..93a460d9c 100644 --- a/src/Components/HMI/ui/server.js +++ b/src/Components/HMI/ui/server.js @@ -1,4 +1,3 @@ -const JWT_SECRET = "deff1952d59f883ece260e8683fed21ab0ad9a53323eca4f"; const express = require('express'); const app = express(); const path = require('path'); @@ -23,7 +22,8 @@ const { User } = require('./model/user.model'); const { Notification, UserNotificationPreference } = require('./model/notification.model'); const bodyParser = require('body-parser'); const Donation = require('./model/donation.model'); -const NotificationService = require('./services/notificationService'); +const notificationService = require('./services/notificationService'); +const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(64).toString('hex'); // Initialize HTTP server for Socket.io const server = http.createServer(app); @@ -66,6 +66,8 @@ const io = new Server(server, { } }); +global.io = io + // Track connected users const connectedUsers = new Map(); @@ -98,7 +100,8 @@ io.use(async (socket, next) => { // Socket.io connection handler io.on('connection', (socket) => { - console.log(`User ${socket.user.id} connected`); + // Store socket connection + connectedUsers.set(socket.user.id, socket.id); // Join user-specific room socket.join(`user_${socket.user.id}`); @@ -107,16 +110,15 @@ io.on('connection', (socket) => { sendPendingNotifications(socket.user.id); socket.on('disconnect', () => { - console.log(`User ${socket.user.id} disconnected`); + connectedUsers.delete(socket.user.id); }); // Handle mark as read socket.on('markAsRead', async (notificationId) => { try { - const notification = await Notification.findByIdAndUpdate( + const notification = await notificationService.markAsRead( notificationId, - { status: 'read' }, - { new: true } + socket.user.id ); if (notification) { @@ -133,7 +135,6 @@ async function sendPendingNotifications(userId) { try { const notifications = await Notification.find({ userId, - status: 'unread' }).sort({ createdAt: -1 }).limit(50); if (notifications.length > 0) { @@ -147,21 +148,6 @@ async function sendPendingNotifications(userId) { } } -// Function to send real-time notification -async function sendRealTimeNotification(userId, notification) { - try { - // Save to database first - const savedNotification = await Notification.create(notification); - - // Send to specific user room - io.to(`user_${userId}`).emit('newNotification', savedNotification); - - console.log(`Notification sent to user ${userId}`); - } catch (err) { - console.error('Error sending notification:', err); - } -} - app.get('/api/notification-preferences', async (req, res) => { try { const token = req.headers.authorization?.split(' ')[1]; @@ -602,14 +588,14 @@ app.get('/api/notifications', async (req, res) => { const decoded = jwt.verify(token, JWT_SECRET); - // Add validation for limit/offset const limit = Math.min(parseInt(req.query.limit) || 20, 100); const offset = parseInt(req.query.offset) || 0; - const notifications = await Notification.find({ userId: decoded.id }) - .sort({ createdAt: -1 }) - .skip(offset) - .limit(limit); + const notifications = await notificationService.getUserNotifications( + decoded.id, + limit, + offset + ); res.json({ notifications }); } catch (error) { @@ -628,10 +614,9 @@ app.patch('/api/notifications/:id/read', async (req, res) => { const decoded = jwt.verify(token, JWT_SECRET); - const notification = await Notification.findOneAndUpdate( - { _id: req.params.id, userId: decoded.id }, - { status: 'read' }, - { new: true } + const notification = await notificationService.markAsRead( + req.params.id, + decoded.id ); if (!notification) { @@ -644,6 +629,31 @@ app.patch('/api/notifications/:id/read', async (req, res) => { } }); +app.delete('/api/notifications/:id', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + + const notification = await notificationService.deleteNotification( + req.params.id, + decoded.id + ); + + if (!notification) { + return res.status(404).json({ error: 'Notification not found' }); + } + + // Emit deletion event to Socket.io + io.to(`user_${decoded.id}`).emit('notificationDeleted', req.params.id); + + res.json({ message: 'Notification deleted successfully' }); + } catch (error) { + res.status(500).json({ error: 'Error deleting notification' }); + } +}); + // Other routes require('./routes/auth.routes')(app); require('./routes/user.routes')(app); @@ -682,25 +692,34 @@ app.get("/welcome", async (req, res) => { } }); -// Temporary test route - REMOVE AFTER TESTING -app.post('/test-notification', async (req, res) => { +// Test the notification service architecture- REMOVE AFTER TESTING +app.post('/test-notification-service', async (req, res) => { try { - const { userId, title, message } = req.body; - - const notification = { - userId, - title: title || "Test Notification", - message: message || "This is a test notification", - type: "system", - status: "unread" - }; + const { userId, title, message, type, channel } = req.body; + + const result = await notificationService.createAndDispatch( + userId, + title || "Test Notification", + message || "This is a test notification", + type || "system", + { test: true }, + channel // Optional: specific channel to test + ); - await sendRealTimeNotification(userId, notification); + res.json({ + success: true, + result, + message: channel ? + `Notification sent via ${channel} channel` : + 'Notification dispatched to all enabled channels' + }); - res.json(notification); } catch (error) { - console.error('Test notification error:', error); - res.status(500).json({ error: error.message }); + console.error('Notification service test error:', error); + res.status(500).json({ + error: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); } }); diff --git a/src/Components/HMI/ui/services/dispatchService.js b/src/Components/HMI/ui/services/dispatchService.js index 7c7d314c1..e2819638e 100644 --- a/src/Components/HMI/ui/services/dispatchService.js +++ b/src/Components/HMI/ui/services/dispatchService.js @@ -1,15 +1,21 @@ -const { UserNotificationPreference } = require('../model/user.model'); -const { sendRealTimeNotification } = require('../server'); -const axios = require('axios'); +const { UserNotificationPreference } = require('../model/notification.model'); +const nodemailer = require('nodemailer'); class DispatchService { constructor() { this.channels = { inApp: this.sendInApp, email: this.sendEmail, - push: this.sendPush, - sms: this.sendSms + push: this.sendPush }; + // Initialize email transporter (mock for now) + this.transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } + }); } async dispatch(userId, notification, channel = null) { @@ -17,40 +23,118 @@ class DispatchService { const preferences = await UserNotificationPreference.findOne({ userId }) || new UserNotificationPreference({ userId }); + // Check Do Not Disturb first + if (this.isInDoNotDisturb(preferences)) { + console.log(`Notification suppressed for user ${userId} during DND hours`); + return { suppressed: true, reason: 'DND' }; + } + + const results = {}; + if (channel) { + // Send to specific channel only if (preferences.preferences[channel]) { - await this.channels[channel](userId, notification); + results[channel] = await this.channels[channel](userId, notification); } } else { + // Send to all enabled channels for (const [channelName, isEnabled] of Object.entries(preferences.preferences)) { if (isEnabled && this.channels[channelName]) { - await this.channels[channelName](userId, notification); + results[channelName] = await this.channels[channelName](userId, notification); } } } + + return results; } catch (error) { console.error(`Error dispatching notification to user ${userId}:`, error); + throw error; } } async sendInApp(userId, notification) { try { - await sendRealTimeNotification(userId, notification); + // This would emit via Socket.io in a real implementation + console.log(`In-app notification to user ${userId}: ${notification.title}`); + + // Simulate real-time notification + if (global.io) { + global.io.to(`user_${userId}`).emit('newNotification', notification); + } + + return { success: true, channel: 'inApp' }; } catch (error) { console.error('Error sending in-app notification:', error); + return { success: false, error: error.message }; } } async sendEmail(userId, notification) { - // Implement email sending + try { + // In a real implementation, you would: + // 1. Look up user email + // 2. Send actual email + console.log(`Email notification to user ${userId}: ${notification.title}`); + + // Mock implementation + const mailOptions = { + from: process.env.EMAIL_FROM, + to: 'user@example.com', // Would be user's actual email + subject: notification.title, + text: notification.message, + html: this.generateEmailTemplate(notification) + }; + + await this.transporter.sendMail(mailOptions); + return { success: true, channel: 'email' }; + } catch (error) { + console.error('Error sending email notification:', error); + return { success: false, error: error.message }; + } } async sendPush(userId, notification) { - // Implement push notification sending + try { + console.log(`Push notification to user ${userId}: ${notification.title}`); + // Push notification logic would go here + return { success: true, channel: 'push' }; + } catch (error) { + console.error('Error sending push notification:', error); + return { success: false, error: error.message }; + } + } + + isInDoNotDisturb(preferences) { + if (!preferences.doNotDisturb?.enabled) return false; + + const now = new Date(); + const [startHour, startMinute] = preferences.doNotDisturb.startTime.split(':').map(Number); + const [endHour, endMinute] = preferences.doNotDisturb.endTime.split(':').map(Number); + + const startTime = new Date(); + startTime.setHours(startHour, startMinute, 0, 0); + + const endTime = new Date(); + endTime.setHours(endHour, endMinute, 0, 0); + + if (startTime > endTime) { + return now >= startTime || now <= endTime; + } + + return now >= startTime && now <= endTime; } - async sendSms(userId, notification) { - // Implement SMS sending + generateEmailTemplate(notification) { + return ` +
+

${notification.title}

+

${notification.message}

+
+

+ Sent from Notification System • ${new Date().toLocaleString()} +

+
+ `; } } diff --git a/src/Components/HMI/ui/services/notificationService.js b/src/Components/HMI/ui/services/notificationService.js index fc1927f5f..afbffdf90 100644 --- a/src/Components/HMI/ui/services/notificationService.js +++ b/src/Components/HMI/ui/services/notificationService.js @@ -1,6 +1,5 @@ -const { Notification, UserNotificationPreference } = require('../model/user.model'); -const axios = require('axios'); -const webpush = require('web-push'); +const { Notification } = require('../model/notification.model'); +//const webpush = require('web-push'); const dispatchService = require('./dispatchService'); class NotificationService { @@ -13,25 +12,32 @@ class NotificationService { process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY );**/ + this.typeConfig = { + donation: { icon: 'ti ti-receipt-2', expiryDays: 30 }, + request: { icon: 'ti ti-alert-circle', expiryDays: 7 }, + user: { icon: 'ti ti-user', expiryDays: 14 }, + system: { icon: 'ti ti-bell', expiryDays: null }, + alert: { icon: 'ti ti-alert-triangle', expiryDays: 1 } + }; } - async createNotification(userId, title, message, type, metadata = {}) { + async createNotification(userId, title, message, type = 'system', metadata = {}) { try { + const typeConfig = this.typeConfig[type] || this.typeConfig.system; + const notification = new Notification({ userId, title, message, type, metadata, - icon: this.getIconForType(type), - expiresAt: this.getExpiryForType(type) + icon: typeConfig.icon, + expiresAt: typeConfig.expiryDays ? + new Date(Date.now() + typeConfig.expiryDays * 24 * 60 * 60 * 1000) : + null }); await notification.save(); - - // Dispatch to all channels based on user preferences - await this.dispatchNotification(notification); - return notification; } catch (error) { console.error('Error creating notification:', error); @@ -39,119 +45,67 @@ class NotificationService { } } - async dispatchNotification(notification) { + async createAndDispatch(userId, title, message, type = 'system', metadata = {}, channel = null) { try { - const preferences = await UserNotificationPreference.findOne({ userId: notification.userId }) || - new UserNotificationPreference({ userId: notification.userId }); - - await dispatchService.dispatch(notification.userId, notification); - - await dispatchService.sendInApp(notification.userId, notification); - - // Check if in do-not-disturb window - if (this.isInDoNotDisturb(preferences)) { - console.log(`Notification suppressed for user ${notification.userId} during DND hours`); - return; - } - - // In-app notification (always sent as it's stored in DB) - if (preferences.preferences.inApp) { - this.sendInAppNotification(notification); - } - - // Email notification - if (preferences.preferences.email) { - this.sendEmailNotification(notification); - } + // Create the notification + const notification = await this.createNotification(userId, title, message, type, metadata); - // Push notification - if (preferences.preferences.push) { - this.sendPushNotification(notification); - } + // Dispatch it + const dispatchResults = await dispatchService.dispatch(userId, notification, channel); - // SMS notification (would require SMS service integration) - if (preferences.preferences.sms) { - this.sendSmsNotification(notification); - } + return { + notification, + dispatchResults + }; } catch (error) { - console.error('Error dispatching notification:', error); + console.error('Error in createAndDispatch:', error); + throw error; } } - - sendInAppNotification(notification) { - dispatchService.dispatch(notification.userId, notification); - console.log(`In-app notification sent for ${notification.userId}`); - } - - async sendEmailNotification(notification) { - // Implement email sending logic using your email service - console.log(`Email notification sent for ${notification.userId}`); + async getUserNotifications(userId, limit = 20, offset = 0) { + try { + return await Notification.find({ userId }) + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); + } catch (error) { + console.error('Error fetching user notifications:', error); + throw error; + } } - async sendPushNotification(notification) { + async markAsRead(notificationId, userId) { try { - // In a real app, you would look up the user's subscription - // const subscription = await getPushSubscription(notification.userId); - // await webpush.sendNotification(subscription, JSON.stringify({ - // title: notification.title, - // body: notification.message, - // icon: notification.icon, - // data: { url: notification.link } - // })); - console.log(`Push notification sent for ${notification.userId}`); + return await Notification.findOneAndUpdate( + { _id: notificationId, userId }, + { status: 'read' }, + { new: true } + ); } catch (error) { - console.error('Error sending push notification:', error); + console.error('Error marking notification as read:', error); + throw error; } } - sendSmsNotification(notification) { - // Implement SMS sending logic - console.log(`SMS notification sent for ${notification.userId}`); + async deleteNotification(notificationId, userId) { + try { + return await Notification.findOneAndDelete({ + _id: notificationId, + userId + }); + }catch (error) { + console.error('Error deleting notification:', error); + throw error; + } } getIconForType(type) { - const icons = { - donation: 'ti ti-receipt-2', - request: 'ti ti-alert-circle', - user: 'ti ti-user', - system: 'ti ti-bell', - alert: 'ti ti-alert-triangle' - }; - return icons[type] || 'ti ti-bell'; + return this.typeConfig[type]?.icon || this.typeConfig.system.icon; } getExpiryForType(type) { - const expiryDays = { - donation: 30, // 30 days for donations - request: 7, // 1 week for requests - user: 14, // 2 weeks for user notifications - system: null, // Never expire for system notifications - alert: 1 // 1 day for alerts - }; - - const days = expiryDays[type] || 7; - return days ? new Date(Date.now() + days * 24 * 60 * 60 * 1000) : null; - } - - isInDoNotDisturb(preferences) { - if (!preferences.doNotDisturb.enabled) return false; - - const now = new Date(); - const [startHour, startMinute] = preferences.doNotDisturb.startTime.split(':').map(Number); - const [endHour, endMinute] = preferences.doNotDisturb.endTime.split(':').map(Number); - - const startTime = new Date(); - startTime.setHours(startHour, startMinute, 0, 0); - - const endTime = new Date(); - endTime.setHours(endHour, endMinute, 0, 0); - - // Handle overnight DND (e.g., 22:00 to 08:00) - if (startTime > endTime) { - return now >= startTime || now <= endTime; - } - - return now >= startTime && now <= endTime; + const expiryDays = this.typeConfig[type]?.expiryDays || 7; + return expiryDays ? new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000) : null; } } From b5d7487ae73b9bee893b08712babfea543acbdc7 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 7 Sep 2025 22:33:28 +1000 Subject: [PATCH 09/11] add markAllAsRead endpoint and method to notification service --- .idea/workspace.xml | 34 +++++++++++++------ src/Components/HMI/ui/server.js | 23 +++++++++++++ .../HMI/ui/services/notificationService.js | 15 ++++++++ 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 1d6d14d36..2d1d18d60 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,14 +4,7 @@
+ + \ No newline at end of file diff --git a/src/Components/HMI/ui/public/admin/css/notifications.css b/src/Components/HMI/ui/public/admin/css/notifications.css index 14fbb1b3d..949a0a262 100644 --- a/src/Components/HMI/ui/public/admin/css/notifications.css +++ b/src/Components/HMI/ui/public/admin/css/notifications.css @@ -1,104 +1,37 @@ -.notification-item { - padding: 1rem; - margin-bottom: 1rem; - border-radius: 8px; - transition: all 0.3s ease; - position: relative; - border-left: 4px solid; -} - -.notification-item.unread { - background-color: #f8f9fa; - border-left-color: #0d6efd; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); -} - -.notification-item.read { - background-color: #ffffff; - border-left-color: #adb5bd; -} - -.notification-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.notification-item .notification-icon { - font-size: 1.5rem; - margin-right: 1rem; - color: #0d6efd; -} - -.notification-item.read .notification-icon { - color: #adb5bd; -} - -.notification-item .notification-content { - flex: 1; -} - -.notification-item .notification-message { - font-weight: 500; - margin-bottom: 0.25rem; -} - -.notification-item .notification-date { - font-size: 0.85rem; - color: #6c757d; -} - -.notification-item .notification-actions { - margin-top: 0.5rem; - display: flex; - gap: 0.5rem; -} - -.empty-notifications { - text-align: center; - padding: 2rem; - color: #6c757d; -} - -.empty-notifications i { - font-size: 3rem; - margin-bottom: 1rem; - color: #dee2e6; -} - -.notification-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; -} - .notification-list { - max-height: 70vh; + max-height: 600px; overflow-y: auto; - padding-right: 10px; } .notification-item { - padding: 15px; - border-radius: 8px; - margin-bottom: 10px; - transition: all 0.3s ease; border-left: 4px solid transparent; + padding: 1rem; + margin-bottom: 1rem; + border-radius: 0.5rem; + background-color: #f8f9fa; + transition: all 0.3s ease; } .notification-item.unread { + border-left-color: #007bff; + background-color: #e3f2fd; +} + +.notification-item.archived { + border-left-color: #6c757d; background-color: #f8f9fa; - border-left-color: #0d6efd; + opacity: 0.8; } .notification-item:hover { - background-color: #f1f1f1; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .notification-icon { font-size: 1.5rem; color: #6c757d; - margin-right: 15px; + margin-right: 1rem; } .notification-content { @@ -107,71 +40,54 @@ .notification-title { font-weight: 600; - margin-bottom: 5px; - color: #212529; + margin-bottom: 0.5rem; + color: #343a40; } .notification-message { color: #6c757d; - margin-bottom: 5px; + margin-bottom: 0.5rem; } .notification-footer { display: flex; justify-content: space-between; align-items: center; - margin-top: 10px; + margin-top: 0.5rem; } .notification-time { color: #adb5bd; } -.notification-actions { - display: flex; - gap: 5px; -} - -.delete-notification { - padding: 0.15rem 0.3rem; - font-size: 0.8rem; -} - -.delete-notification i { - margin-right: 0; +.notification-actions .btn { + margin-left: 0.5rem; } .empty-notifications { text-align: center; - padding: 40px 20px; + padding: 3rem; color: #6c757d; } .empty-notifications i { font-size: 3rem; - margin-bottom: 15px; - color: #dee2e6; -} - -.empty-notifications h5 { - margin-bottom: 5px; -} - -/* Custom scrollbar */ -.notification-list::-webkit-scrollbar { - width: 8px; + margin-bottom: 1rem; + display: block; } -.notification-list::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 10px; +.connection-status { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 0.5rem; } -.notification-list::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 10px; +.connection-status.connected { + background-color: #28a745; } -.notification-list::-webkit-scrollbar-thumb:hover { - background: #a8a8a8; +.connection-status.disconnected { + background-color: #dc3545; } \ No newline at end of file diff --git a/src/Components/HMI/ui/public/admin/js/header.js b/src/Components/HMI/ui/public/admin/js/header.js new file mode 100644 index 000000000..d068dbc81 --- /dev/null +++ b/src/Components/HMI/ui/public/admin/js/header.js @@ -0,0 +1,79 @@ +class HeaderNotifications { + constructor() { + this.notificationBell = document.getElementById('notificationBell'); + this.unreadCountBadge = document.getElementById('unreadCountBadge'); + this.unreadCountText = document.getElementById('unreadCountText'); + this.bellIcon = document.getElementById('bellIcon'); + this.socket = null; + this.init(); + } + + init() { + this.setupEventListeners(); + this.updateNotificationCount(); + this.setupRealTimeUpdates(); + setInterval(() => this.updateNotificationCount(), 30000); + } + + setupEventListeners() { + this.notificationBell.addEventListener('click', (e) => { + e.preventDefault(); + window.location.href = '/admin-notifications'; + }); + } + + async updateNotificationCount() { + try { + const token = localStorage.getItem('token'); + if (!token) return; + + const response = await fetch('/api/notifications/unread-count', { + headers: { 'Authorization': 'Bearer ' + token } + }); + + if (response.ok) { + const data = await response.json(); + this.updateBadge(data.count || 0); + } + } catch (error) { + console.error('Error fetching notification count:', error); + } + } + + updateBadge(unreadCount) { + if (unreadCount > 0) { + this.unreadCountBadge.style.display = 'flex'; + this.unreadCountText.textContent = unreadCount > 99 ? '99+' : unreadCount; + + // Add animation for new notifications + this.unreadCountBadge.classList.add('notification-pulse'); + this.bellIcon.classList.add('bell-ring'); + + setTimeout(() => { + this.unreadCountBadge.classList.remove('notification-pulse'); + this.bellIcon.classList.remove('bell-ring'); + }, 1500); + } else { + this.unreadCountBadge.style.display = 'none'; + } + } + + setupRealTimeUpdates() { + if (typeof io !== 'undefined') { + this.socket = io(); + + this.socket.on('newNotification', () => { + this.updateNotificationCount(); + }); + + this.socket.on('notificationUpdated', () => { + this.updateNotificationCount(); + }); + } + } +} + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + new HeaderNotifications(); +}); \ No newline at end of file diff --git a/src/Components/HMI/ui/public/admin/js/notifications.js b/src/Components/HMI/ui/public/admin/js/notifications.js index 76d1d089b..f13b40f5d 100644 --- a/src/Components/HMI/ui/public/admin/js/notifications.js +++ b/src/Components/HMI/ui/public/admin/js/notifications.js @@ -6,14 +6,12 @@ $(document).ready(function() { } const token = localStorage.getItem('token'); - console.log('Current token:', token); - if (!token) { console.error('No token found in localStorage'); - // Redirect to login or handle missing token window.location.href = '/login'; return; } + // Connect to Socket.io const socket = io('http://localhost:8080', { auth: { token }, @@ -21,25 +19,25 @@ $(document).ready(function() { reconnectionAttempts: Infinity, reconnectionDelay: 1000, reconnectionDelayMax: 5000, - query: { token } // Fallback for auth + query: { token } }); // Connection status logging socket.on('connect', () => { console.log('Socket.IO connected with ID:', socket.id); + updateConnectionStatus(true); }); socket.on('disconnect', (reason) => { console.log('Socket.IO disconnected:', reason); + updateConnectionStatus(false); if (reason === 'io server disconnect') { - // Try to reconnect after getting new credentials if needed socket.connect(); } }); socket.on('connect_error', (err) => { console.error('Socket.IO connection error:', err.message); - // Try to reconnect with fresh token if auth fails if (err.message.includes('auth')) { socket.auth.token = localStorage.getItem('token'); socket.connect(); @@ -48,12 +46,21 @@ $(document).ready(function() { let notifications = []; let unreadCount = 0; + let currentTab = 'all'; // 'all', 'unread', 'archived' // DOM Elements const notificationList = $("#notification-list"); const unreadBadge = $("#unread-badge"); const markAllReadBtn = $("#mark-all-read"); const loadMoreBtn = $("#load-more"); + const archiveAllBtn = $("#archive-all"); + const connectionIndicator = $("#connection-indicator"); + + // Tab elements + const allTab = $("#all-tab"); + const unreadTab = $("#unread-tab"); + const archivedTab = $("#archived-tab"); + let isLoading = false; let offset = 0; const limit = 10; @@ -63,16 +70,30 @@ $(document).ready(function() { setupSocketListeners(); setupEventHandlers(); - function loadNotifications() { + function updateConnectionStatus(connected) { + connectionIndicator + .removeClass(connected ? 'disconnected' : 'connected') + .addClass(connected ? 'connected' : 'disconnected') + .text(connected ? 'Connected' : 'Disconnected'); + } + + function loadNotifications(status = null) { if (isLoading) return; isLoading = true; loadMoreBtn.prop('disabled', true).html(' Loading...'); + let url = `/api/notifications?limit=${limit}&offset=${offset}`; + if (status === 'archived') { + url = `/api/notifications/archived?limit=${limit}&offset=${offset}`; + } else if (status) { + url += `&status=${status}`; + } + $.ajax({ - url: `/api/notifications?limit=${limit}&offset=${offset}`, + url: url, headers: { - 'Authorization': 'Bearer ' + localStorage.getItem('token'), + 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, success: function(response) { @@ -94,9 +115,10 @@ $(document).ready(function() { isLoading = false; loadMoreBtn.prop('disabled', false).html('Load More'); - // Only hide if we know we've got all notifications if (notifications.length > 0 && notifications.length % limit !== 0) { loadMoreBtn.hide(); + } else { + loadMoreBtn.show(); } } }); @@ -113,6 +135,17 @@ $(document).ready(function() { }).showToast(); } + function showSuccessToast(message) { + Toastify({ + text: message, + duration: 5000, + gravity: "top", + position: "right", + backgroundColor: "#28a745", + stopOnFocus: true + }).showToast(); + } + function renderNotifications() { notificationList.empty(); @@ -140,11 +173,10 @@ $(document).ready(function() { function createNotificationElement(notification) { const timeAgo = moment(notification.createdAt).fromNow(); - const isUnread = notification.status === 'unread'; + const isArchived = notification.status === 'archived'; return ` -
+
@@ -155,10 +187,21 @@ $(document).ready(function() { ${timeAgo}
${notification.link ? - `View` : ''} - + `View` : ''} + + ${!isArchived ? ` + + + ` : ` + + `} + @@ -175,17 +218,6 @@ $(document).ready(function() { } function setupSocketListeners() { - // Connection status indicator - const updateConnectionStatus = (connected) => { - const indicator = $('#connection-indicator'); - indicator.removeClass(connected ? 'disconnected' : 'connected') - .addClass(connected ? 'connected' : 'disconnected') - .text(connected ? 'Connected' : 'Disconnected'); - }; - - socket.on('connect', () => updateConnectionStatus(true)); - socket.on('disconnect', () => updateConnectionStatus(false)); - // Initial notifications socket.on('initialNotifications', (initialNotifications) => { notifications = initialNotifications; @@ -197,16 +229,12 @@ $(document).ready(function() { socket.on('newNotification', (newNotification) => { console.log('Received new notification:', newNotification); notifications.unshift(newNotification); - notificationList.prepend(createNotificationElement(newNotification)); - updateUnreadCount(); - - // Show toast notification showToastNotification(newNotification); }); - // Notification updated (e.g., marked as read) + // Notification updated socket.on('notificationUpdated', (updatedNotification) => { const index = notifications.findIndex(n => n._id === updatedNotification._id); if (index !== -1) { @@ -218,12 +246,28 @@ $(document).ready(function() { .removeClass('btn-outline-primary') .addClass('btn-outline-secondary') .text('Read'); - updateUnreadCount(); } }); - // Handle notification deletion from other devices + // Handle archive events + socket.on('notificationArchived', (notificationId) => { + $(`.notification-item[data-id="${notificationId}"]`).remove(); + updateUnreadCount(); + }); + + socket.on('allNotificationsArchived', () => { + notificationList.empty(); + unreadCount = 0; + unreadBadge.text(unreadCount).toggle(unreadCount > 0); + showSuccessToast('All notifications archived'); + }); + + socket.on('notificationRestored', (notification) => { + $(`.notification-item[data-id="${notification._id}"]`).remove(); + showSuccessToast('Notification restored'); + }); + socket.on('notificationDeleted', (notificationId) => { notifications = notifications.filter(n => n._id !== notificationId); $(`.notification-item[data-id="${notificationId}"]`).remove(); @@ -243,7 +287,7 @@ $(document).ready(function() { $.ajax({ url: '/api/notifications/read-all', method: 'PATCH', - headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }, + headers: { 'Authorization': 'Bearer ' + token }, success: function() { notifications.forEach(n => n.status = 'read'); $('.notification-item').removeClass('unread').addClass('read'); @@ -252,12 +296,33 @@ $(document).ready(function() { .addClass('btn-outline-secondary') .text('Read'); updateUnreadCount(); + showSuccessToast('All notifications marked as read'); + }, + error: function(xhr) { + console.error('Error marking all as read:', xhr.responseText); + showErrorToast('Failed to mark all as read'); } }); }); - // Load more - loadMoreBtn.on('click', loadNotifications); + // Archive notification + notificationList.on('click', '.archive-notification', function() { + const notificationId = $(this).closest('.notification-item').data('id'); + archiveNotification(notificationId); + }); + + // Restore notification + notificationList.on('click', '.restore-notification', function() { + const notificationId = $(this).closest('.notification-item').data('id'); + restoreNotification(notificationId); + }); + + // Archive all + archiveAllBtn.on('click', function() { + if (confirm('Are you sure you want to archive all notifications?')) { + archiveAllNotifications(); + } + }); // Delete notification notificationList.on('click', '.delete-notification', function(e) { @@ -265,6 +330,81 @@ $(document).ready(function() { const notificationId = $(this).closest('.notification-item').data('id'); deleteNotification(notificationId); }); + + // Load more + loadMoreBtn.on('click', function() { + loadNotifications(currentTab === 'archived' ? 'archived' : null); + }); + + // Tab switching + allTab.on('click', function() { + currentTab = 'all'; + offset = 0; + loadNotifications(); + }); + + unreadTab.on('click', function() { + currentTab = 'unread'; + offset = 0; + loadNotifications('unread'); + }); + + archivedTab.on('click', function() { + currentTab = 'archived'; + offset = 0; + loadNotifications('archived'); + }); + } + + function archiveNotification(notificationId) { + $.ajax({ + url: `/api/notifications/${notificationId}/archive`, + method: 'PATCH', + headers: { 'Authorization': 'Bearer ' + token }, + success: function(response) { + $(`.notification-item[data-id="${notificationId}"]`).remove(); + showSuccessToast('Notification archived'); + updateUnreadCount(); + }, + error: function(xhr) { + console.error('Archive error:', xhr.responseText); + showErrorToast('Failed to archive notification'); + } + }); + } + + function archiveAllNotifications() { + $.ajax({ + url: '/api/notifications/archive-all', + method: 'PATCH', + headers: { 'Authorization': 'Bearer ' + token }, + success: function(response) { + notificationList.empty(); + unreadCount = 0; + unreadBadge.text(unreadCount).toggle(unreadCount > 0); + showSuccessToast(`Archived ${response.archivedCount} notifications`); + }, + error: function(xhr) { + console.error('Archive all error:', xhr.responseText); + showErrorToast('Failed to archive all notifications'); + } + }); + } + + function restoreNotification(notificationId) { + $.ajax({ + url: `/api/notifications/${notificationId}/restore`, + method: 'PATCH', + headers: { 'Authorization': 'Bearer ' + token }, + success: function(response) { + $(`.notification-item[data-id="${notificationId}"]`).remove(); + showSuccessToast('Notification restored'); + }, + error: function(xhr) { + console.error('Restore error:', xhr.responseText); + showErrorToast('Failed to restore notification'); + } + }); } function deleteNotification(notificationId) { @@ -273,11 +413,9 @@ $(document).ready(function() { $.ajax({ url: `/api/notifications/${notificationId}`, method: 'DELETE', - headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }, + headers: { 'Authorization': 'Bearer ' + token }, success: function() { - // Remove from local array notifications = notifications.filter(n => n._id !== notificationId); - // Remove from DOM $(`.notification-item[data-id="${notificationId}"]`).remove(); updateUnreadCount(); showSuccessToast('Notification deleted'); @@ -290,7 +428,6 @@ $(document).ready(function() { } function showToastNotification(notification) { - // Using Toastify for toast notifications Toastify({ text: `${notification.title}: ${notification.message}`, duration: 5000, diff --git a/src/Components/HMI/ui/public/admin/notifications.html b/src/Components/HMI/ui/public/admin/notifications.html index d1349ae28..7c5782374 100644 --- a/src/Components/HMI/ui/public/admin/notifications.html +++ b/src/Components/HMI/ui/public/admin/notifications.html @@ -1,6 +1,5 @@ - @@ -15,35 +14,63 @@ - -
- - -
-

Notifications 0

+

Notifications + 0 + Disconnected +

+
- -
- + + + + +
+
+
+
+
+
+
+
+
+
@@ -53,7 +80,6 @@

Notifications { } }); +// Get unread notification count +app.get('/api/notifications/unread-count', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + + const count = await Notification.countDocuments({ + userId: decoded.id, + status: 'unread' + }); + + res.json({ count }); + } catch (error) { + console.error('Error fetching unread count:', error); + res.status(500).json({ error: 'Error fetching notification count' }); + } +}); + +// Archive a specific notification +app.patch('/api/notifications/:id/archive', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + const notification = await notificationService.archiveNotification( + req.params.id, + decoded.id + ); + + if (!notification) { + return res.status(404).json({ error: 'Notification not found' }); + } + + // Emit real-time update + io.to(`user_${decoded.id}`).emit('notificationArchived', req.params.id); + + res.json({ + message: 'Notification archived successfully', + notification + }); + } catch (error) { + console.error('Error archiving notification:', error); + res.status(500).json({ error: 'Error archiving notification' }); + } +}); + +// Archive all notifications +app.patch('/api/notifications/archive-all', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + const result = await notificationService.archiveAllNotifications(decoded.id); + + // Emit real-time update + io.to(`user_${decoded.id}`).emit('allNotificationsArchived'); + + res.json({ + message: 'All notifications archived successfully', + archivedCount: result.modifiedCount + }); + } catch (error) { + console.error('Error archiving all notifications:', error); + res.status(500).json({ error: 'Error archiving all notifications' }); + } +}); + +// Get archived notifications +app.get('/api/notifications/archived', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + const limit = Math.min(parseInt(req.query.limit) || 20, 100); + const offset = parseInt(req.query.offset) || 0; + + const notifications = await notificationService.getArchivedNotifications( + decoded.id, + limit, + offset + ); + + res.json({ notifications }); + } catch (error) { + console.error('Error fetching archived notifications:', error); + res.status(500).json({ error: 'Error fetching archived notifications' }); + } +}); + +// Restore an archived notification +app.patch('/api/notifications/:id/restore', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + const notification = await notificationService.restoreNotification( + req.params.id, + decoded.id + ); + + if (!notification) { + return res.status(404).json({ error: 'Archived notification not found' }); + } + + // Emit real-time update + io.to(`user_${decoded.id}`).emit('notificationRestored', notification); + + res.json({ + message: 'Notification restored successfully', + notification + }); + } catch (error) { + console.error('Error restoring notification:', error); + res.status(500).json({ error: 'Error restoring notification' }); + } +}); + // Other routes require('./routes/auth.routes')(app); require('./routes/user.routes')(app); diff --git a/src/Components/HMI/ui/services/notificationService.js b/src/Components/HMI/ui/services/notificationService.js index 8fd925f4b..adf529291 100644 --- a/src/Components/HMI/ui/services/notificationService.js +++ b/src/Components/HMI/ui/services/notificationService.js @@ -1,17 +1,9 @@ const { Notification } = require('../model/notification.model'); -//const webpush = require('web-push'); const dispatchService = require('./dispatchService'); class NotificationService { constructor() { - console.log('NotificationService initialized (push notifications disabled)'); - // Initialize web push (for browser notifications) - /** - webpush.setVapidDetails( - `mailto:${process.env.VAPID_EMAIL}`, - process.env.VAPID_PUBLIC_KEY, - process.env.VAPID_PRIVATE_KEY - );**/ + console.log('NotificationService initialized'); this.typeConfig = { donation: { icon: 'ti ti-receipt-2', expiryDays: 30 }, request: { icon: 'ti ti-alert-circle', expiryDays: 7 }, @@ -47,10 +39,7 @@ class NotificationService { async createAndDispatch(userId, title, message, type = 'system', metadata = {}, channel = null) { try { - // Create the notification const notification = await this.createNotification(userId, title, message, type, metadata); - - // Dispatch it const dispatchResults = await dispatchService.dispatch(userId, notification, channel); return { @@ -62,9 +51,15 @@ class NotificationService { throw error; } } - async getUserNotifications(userId, limit = 20, offset = 0) { + + async getUserNotifications(userId, limit = 20, offset = 0, status = null) { try { - return await Notification.find({ userId }) + const query = { userId }; + if (status) { + query.status = status; + } + + return await Notification.find(query) .sort({ createdAt: -1 }) .skip(offset) .limit(limit); @@ -89,9 +84,6 @@ class NotificationService { async markAllAsRead(userId) { try { - if (!Notification || typeof Notification.updateMany !== 'function') { - throw new Error('Notification model is not properly initialized'); - } return await Notification.updateMany( { userId: userId, status: 'unread'}, { status: 'read' } @@ -108,12 +100,110 @@ class NotificationService { _id: notificationId, userId }); - }catch (error) { + } catch (error) { console.error('Error deleting notification:', error); throw error; } } + async getUnreadCount(userId) { + try { + return await Notification.countDocuments({ + userId: userId, + status: 'unread' + }); + } catch (error) { + console.error('Error getting unread count:', error); + throw error; + } + } + + async archiveNotification(notificationId, userId) { + try { + const notification = await Notification.findOneAndUpdate( + { + _id: notificationId, + userId: userId + }, + { + status: 'archived', + archivedAt: new Date() + }, + { new: true } + ); + + if (!notification) { + throw new Error('Notification not found or access denied'); + } + + return notification; + } catch (error) { + console.error('Error archiving notification:', error); + throw error; + } + } + + async archiveAllNotifications(userId) { + try { + const result = await Notification.updateMany( + { + userId: userId, + status: { $in: ['unread', 'read'] } + }, + { + status: 'archived', + archivedAt: new Date() + } + ); + + return result; + } catch (error) { + console.error('Error archiving all notifications:', error); + throw error; + } + } + + async getArchivedNotifications(userId, limit = 20, offset = 0) { + try { + return await Notification.find({ + userId: userId, + status: 'archived' + }) + .sort({ archivedAt: -1, createdAt: -1 }) + .skip(offset) + .limit(limit); + } catch (error) { + console.error('Error fetching archived notifications:', error); + throw error; + } + } + + async restoreNotification(notificationId, userId) { + try { + const notification = await Notification.findOneAndUpdate( + { + _id: notificationId, + userId: userId, + status: 'archived' + }, + { + status: 'read', + archivedAt: null + }, + { new: true } + ); + + if (!notification) { + throw new Error('Archived notification not found or access denied'); + } + + return notification; + } catch (error) { + console.error('Error restoring notification:', error); + throw error; + } + } + getIconForType(type) { return this.typeConfig[type]?.icon || this.typeConfig.system.icon; } From 7787dac303861bd899c9ab58832ad6176d39ed57 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 21 Sep 2025 03:25:28 +1000 Subject: [PATCH 11/11] add user notification preferences management with real-time updates and UI enhancements --- .idea/workspace.xml | 65 ++- src/Components/HMI/ui/model/donation.model.js | 35 ++ .../HMI/ui/model/notification.model.js | 45 +- src/Components/HMI/ui/model/user.model.js | 57 ++- src/Components/HMI/ui/package-lock.json | 15 + src/Components/HMI/ui/package.json | 1 + .../admin/component/sidebar-component.html | 2 +- .../admin/css/notification-preferences.css | 109 ++++ .../admin/js/notification-preferences.js | 170 ++++++ .../HMI/ui/public/admin/js/notifications.js | 246 +++++++-- .../admin/notification-preferences.html | 221 ++++++++ .../HMI/ui/public/admin/notifications.html | 18 +- .../ui/public/admin/scss/layouts/_header.scss | 482 +++++++----------- .../HMI/ui/public/admin/template.html | 2 +- src/Components/HMI/ui/server.js | 190 +++++-- .../HMI/ui/services/dispatchService.js | 191 +++++-- .../HMI/ui/services/notificationService.js | 99 +++- src/Components/HMI/utils/tokenUtils.js | 24 + src/Components/MongoDB/init/init-mongo.js | 4 + .../init/notification-preferences-seed.json | 18 + 20 files changed, 1500 insertions(+), 494 deletions(-) create mode 100644 src/Components/HMI/ui/public/admin/css/notification-preferences.css create mode 100644 src/Components/HMI/ui/public/admin/js/notification-preferences.js create mode 100644 src/Components/HMI/ui/public/admin/notification-preferences.html create mode 100644 src/Components/HMI/utils/tokenUtils.js create mode 100644 src/Components/MongoDB/init/notification-preferences-seed.json diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 74d48e3f7..52ef688ee 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,17 +4,27 @@

diff --git a/src/Components/HMI/ui/public/admin/scss/layouts/_header.scss b/src/Components/HMI/ui/public/admin/scss/layouts/_header.scss index 6193b4592..07bc4d3dc 100644 --- a/src/Components/HMI/ui/public/admin/scss/layouts/_header.scss +++ b/src/Components/HMI/ui/public/admin/scss/layouts/_header.scss @@ -1,356 +1,270 @@ -// ---------------------------------------------- // Variables -// ---------------------------------------------- -$notification-badge-size: 12px; -$notification-badge-font-size: 0.5rem; -$notification-badge-padding: 0.15rem 0.3rem; -$notification-pulse-color: rgba(13, 110, 253, 0.7); - -// ---------------------------------------------- -// Header Styles -// ---------------------------------------------- -.app-header { - position: relative; - z-index: 50; - width: 100%; - background: $white; - padding: 0 25px; - - .container-fluid { - max-width: $boxed-width; - margin: 0 auto; - padding: 0 30px; - } +$primary-color: #696cff; +$danger-color: #ff3e1d; +$success-color: #71dd37; +$warning-color: #ffab00; +$dark-color: #233446; +$light-color: #f5f5f9; +$border-radius: 0.375rem; +$box-shadow: 0 0.125rem 0.25rem rgba(165, 163, 174, 0.3); +$transition: all 0.2s ease; + +// Base styles +body { + font-family: 'Inter', sans-serif; + background-color: #f5f5f9; + color: #333; + padding: 0; + margin: 0; +} - .navbar { - min-height: $header-height; - padding: 0; - - .navbar-nav { - .nav-item { - .nav-link { - padding: $navlink-padding; - line-height: $header-height; - height: $header-height; - display: flex; - align-items: center; - position: relative; - font-size: 20px; - z-index: 2; - } - } - - &.quick-links { - .nav-item { - .nav-link { - font-size: $font-size-base; - position: relative; - z-index: 2; - } - - &:hover { - .nav-link { - transition: all 0.1s ease-in-out; - color: $primary !important; - - &:before { - content: ""; - position: absolute; - left: 0; - right: 0; - height: 36px; - width: 100%; - border-radius: $border-radius; - background: $light-primary; - z-index: -1; - } - } - } - } - } - } - } +.app-header { + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + position: sticky; + top: 0; + z-index: 1000; } -// ---------------------------------------------- -// Navigation Components -// ---------------------------------------------- -.nav-icon-hover { - transition: all 0.3s ease-in-out; +.navbar-nav .nav-link { + color: #6e6b7b; + padding: 0.7rem 1rem; + transition: $transition; &:hover { - &::before { - content: ""; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - height: 40px; - width: 40px; - z-index: -1; - border-radius: 100px; - transition: all 0.3s ease-in-out; - background-color: $light-primary; - } + color: $primary-color; } } -.navbar-nav { - .dropdown-menu { - position: absolute; - min-width: 200px; +.nav-icon-hover { + transition: $transition; + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; - .dropdown-item { - border-radius: 8px; - } + &:hover { + background-color: rgba(105, 108, 255, 0.08); } } -// ---------------------------------------------- -// Notification Styles -// ---------------------------------------------- -.notification { - content: ""; - position: absolute; - top: 22px; - right: 9px; - width: 8px; - height: 8px; +.badge { + font-size: 0.65rem; + padding: 0.35rem 0.5rem; } -.notification-badge { - font-size: $notification-badge-font-size; - padding: $notification-badge-padding; - display: none; +// Notification Bell Animation +.bell-ring { + animation: bellRing 0.9s both; +} - &.show { - display: block; - } +@keyframes bellRing { + 0%, 100% { transform: rotate(0); } + 10%, 30%, 50%, 70%, 90% { transform: rotate(-10deg); } + 20%, 40%, 60%, 80% { transform: rotate(10deg); } } -#unreadCountBadge { - width: $notification-badge-size; - height: $notification-badge-size; - z-index: 1; - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8); - display: none; +// Notification Badge Pulse Animation +.notification-pulse { + animation: notificationPulse 1.5s infinite; +} - &.show { - display: block; - } +@keyframes notificationPulse { + 0% { box-shadow: 0 0 0 0 rgba(255, 62, 29, 0.7); } + 70% { box-shadow: 0 0 0 6px rgba(255, 62, 29, 0); } + 100% { box-shadow: 0 0 0 0 rgba(255, 62, 29, 0); } } -#unreadCountText { - z-index: 2; - font-weight: bold; - min-width: 16px; - line-height: 1; +// Notification Dropdown +.notification-dropdown { + width: 380px; + padding: 0; + border: 1px solid rgba(0,0,0,0.05); + box-shadow: 0 5px 25px rgba(0,0,0,0.1); } -// ---------------------------------------------- -// Notification Animations -// ---------------------------------------------- -.notification-pulse { - animation: pulse-blue 2s infinite; +.notification-header { + padding: 1rem; + border-bottom: 1px solid rgba(0,0,0,0.05); + display: flex; + justify-content: space-between; + align-items: center; } -.bell-ring { - animation: ring 0.5s ease-in-out; +.notification-body { + max-height: 350px; + overflow-y: auto; } -@keyframes pulse-blue { - 0% { - transform: translate(-50%, -50%) scale(0.95); - box-shadow: 0 0 0 0 $notification-pulse-color; - } +.notification-item { + padding: 1rem; + border-bottom: 1px solid rgba(0,0,0,0.05); + transition: $transition; + display: flex; + align-items: flex-start; - 70% { - transform: translate(-50%, -50%) scale(1); - box-shadow: 0 0 0 10px rgba(13, 110, 253, 0); + &:hover { + background-color: rgba(105, 108, 255, 0.04); } - 100% { - transform: translate(-50%, -50%) scale(0.95); - box-shadow: 0 0 0 0 rgba(13, 110, 253, 0); + &.unread { + background-color: rgba(105, 108, 255, 0.06); } } -@keyframes ring { - 0% { transform: rotate(0deg); } - 25% { transform: rotate(15deg); } - 50% { transform: rotate(-15deg); } - 75% { transform: rotate(10deg); } - 100% { transform: rotate(0deg); } +.notification-icon { + width: 38px; + height: 38px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + flex-shrink: 0; } -// ---------------------------------------------- -// Utility Classes -// ---------------------------------------------- -.position-relative { - position: relative; +.notification-content { + flex: 1; } -.position-absolute { - position: absolute; +.notification-title { + font-weight: 500; + margin-bottom: 0.25rem; + color: $dark-color; } -.top-0 { - top: 0; +.notification-message { + font-size: 0.875rem; + color: #6e6b7b; + margin-bottom: 0.25rem; } -.start-100 { - left: 100%; +.notification-time { + font-size: 0.75rem; + color: #b9b9c3; } -.translate-middle { - transform: translate(-50%, -50%); +.notification-actions { + display: flex; + justify-content: space-between; + padding: 0.75rem 1rem; + border-top: 1px solid rgba(0,0,0,0.05); } -.visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} +.empty-notifications { + padding: 2rem 1rem; + text-align: center; + color: #b9b9c3; -// ---------------------------------------------- -// Dropdown Animations -// ---------------------------------------------- -@keyframes animation-dropdown-menu-move-up-scroll { - from { - top: 71px; - } - to { - top: 70px; + i { + font-size: 2rem; + margin-bottom: 0.5rem; } } -@keyframes animation-dropdown-menu-fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } +.mark-all-read { + cursor: pointer; + color: $primary-color; + font-size: 0.875rem; } -@keyframes fadeInUp { - from { - opacity: 0; - transform: translate3d(0, 100%, 0); - } - to { - opacity: 1; - transform: none; - } +.mark-as-read { + width: 8px; + height: 8px; + border-radius: 50%; + background: $primary-color; + flex-shrink: 0; + margin-top: 8px; + margin-left: 10px; + cursor: pointer; } -.dropdown-menu-animate-up { - animation: animation-dropdown-menu-fade-in 0.5s ease 1, - animation-dropdown-menu-move-up 0.5s ease-out 1; +// Sound Toggle +.sound-toggle { + position: relative; + display: inline-block; + width: 45px; + height: 24px; + margin-left: 10px; } -// ---------------------------------------------- -// Media Queries -// ---------------------------------------------- -@include media-breakpoint-down(lg) { - .app-header { - .navbar { - flex-wrap: nowrap; - - .navbar-nav { - flex-direction: row; - } - } - } - - .w-xs-100 { - width: 100% !important; - } +.sound-toggle input { + opacity: 0; + width: 0; + height: 0; } -@include media-breakpoint-down(md) { - .navbar-nav { - .dropdown-menu { - position: absolute; - width: 100%; - } - - .nav-item.dropdown { - position: static; - } - } +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; } -/* Notification Styles */ -.notification-badge { - font-size: 0.5rem; - padding: 0.15rem 0.3rem; - display: none; +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; } -.notification-badge.show { - display: block; +input:checked + .slider { + background-color: $primary-color; } -.notification-pulse { - animation: pulse-blue 2s infinite; +input:checked + .slider:before { + transform: translateX(21px); } -.bell-ring { - animation: ring 0.5s ease-in-out; -} +// Toast notification +.notification-toast { + position: fixed; + top: 20px; + right: 20px; + z-index: 9999; + min-width: 300px; + background: white; + border-left: 4px solid $primary-color; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); + border-radius: 4px; + padding: 1rem; + display: flex; + align-items: flex-start; + transform: translateX(100%); + opacity: 0; + transition: all 0.3s ease; -@keyframes pulse-blue { - 0% { - transform: translate(-50%, -50%) scale(0.95); - box-shadow: 0 0 0 0 rgba(13, 110, 253, 0.7); - } - 70% { - transform: translate(-50%, -50%) scale(1); - box-shadow: 0 0 0 10px rgba(13, 110, 253, 0); - } - 100% { - transform: translate(-50%, -50%) scale(0.95); - box-shadow: 0 0 0 0 rgba(13, 110, 253, 0); + &.show { + transform: translateX(0); + opacity: 1; } } -@keyframes ring { - 0% { transform: rotate(0deg); } - 25% { transform: rotate(15deg); } - 50% { transform: rotate(-15deg); } - 75% { transform: rotate(10deg); } - 100% { transform: rotate(0deg); } -} - -/* Header Styles */ -.app-header { - position: relative; - z-index: 50; - width: 100%; - background: white; - padding: 0 25px; +.toast-close { + background: none; + border: none; + font-size: 1.2rem; + color: #b9b9c3; + cursor: pointer; + margin-left: 10px; } -.nav-icon-hover { - transition: all 0.3s ease-in-out; -} - -.nav-icon-hover:hover::before { - content: ""; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - height: 40px; - width: 40px; - z-index: -1; - border-radius: 100px; - background-color: rgba(13, 110, 253, 0.1); +// Responsive adjustments +@media (max-width: 576px) { + .notification-dropdown { + width: 300px; + right: -50px !important; + } } \ No newline at end of file diff --git a/src/Components/HMI/ui/public/admin/template.html b/src/Components/HMI/ui/public/admin/template.html index 9baa84e68..ee9f04454 100644 --- a/src/Components/HMI/ui/public/admin/template.html +++ b/src/Components/HMI/ui/public/admin/template.html @@ -8,7 +8,7 @@ - + { // Helper function to send pending notifications async function sendPendingNotifications(userId) { try { + // Get user preferences first + const preferences = await UserNotificationPreference.findOne({ userId }) || + new UserNotificationPreference({ userId }); + const notifications = await Notification.find({ userId, }).sort({ createdAt: -1 }).limit(50); - if (notifications.length > 0) { + // Filter notifications based on user preferences + const filteredNotifications = notifications.filter(notification => { + const typeEnabled = preferences.preferences.types[notification.type]; + return typeEnabled !== false; // Only show if explicitly enabled or undefined + }); + + if (filteredNotifications.length > 0) { const socketId = connectedUsers.get(userId); if (socketId) { - io.to(socketId).emit('initialNotifications', notifications); + io.to(socketId).emit('initialNotifications', filteredNotifications); } } } catch (error) { @@ -148,39 +159,6 @@ async function sendPendingNotifications(userId) { } } -app.get('/api/notification-preferences', async (req, res) => { - try { - const token = req.headers.authorization?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'Unauthorized' }); - - const decoded = jwt.verify(token, JWT_SECRET); - const preferences = await UserNotificationPreference.findOne({ userId: decoded.id }) || - new UserNotificationPreference({ userId: decoded.id }); - - res.json(preferences); - } catch (error) { - res.status(500).json({ error: 'Error fetching preferences' }); - } -}); - -app.put('/api/notification-preferences', async (req, res) => { - try { - const token = req.headers.authorization?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'Unauthorized' }); - - const decoded = jwt.verify(token, JWT_SECRET); - const preferences = await UserNotificationPreference.findOneAndUpdate( - { userId: decoded.id }, - req.body, - { new: true, upsert: true } - ); - - res.json(preferences); - } catch (error) { - res.status(500).json({ error: 'Error updating preferences' }); - } -}); - const storeItems = new Map([[ 1, { priceInCents: 100, name: "donation"} ]]); @@ -588,14 +566,37 @@ app.get('/api/notifications', async (req, res) => { const decoded = jwt.verify(token, JWT_SECRET); + // Get user preferences + const preferences = await UserNotificationPreference.findOne({ userId: decoded.id }) || + new UserNotificationPreference({ userId: decoded.id }); + const limit = Math.min(parseInt(req.query.limit) || 20, 100); const offset = parseInt(req.query.offset) || 0; + const status = req.query.status; - const notifications = await notificationService.getUserNotifications( - decoded.id, - limit, - offset - ); + let query = { userId: decoded.id }; + + // For the main notifications endpoint, exclude archived notifications + if (!status || status !== 'archived') { + query.status = { $ne: 'archived' }; + } + + // If a specific status is requested, use it + if (status && status !== 'archived') { + query.status = status; + } + + // Get all notifications first + let notifications = await Notification.find(query) + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); + + // Filter out notifications where the type is disabled in user preferences + notifications = notifications.filter(notification => { + const typeEnabled = preferences.preferences.types[notification.type]; + return typeEnabled !== false; // Only show if explicitly enabled or undefined + }); res.json({ notifications }); } catch (error) { @@ -685,12 +686,23 @@ app.get('/api/notifications/unread-count', async (req, res) => { const decoded = jwt.verify(token, JWT_SECRET); - const count = await Notification.countDocuments({ + // Get user preferences + const preferences = await UserNotificationPreference.findOne({ userId: decoded.id }) || + new UserNotificationPreference({ userId: decoded.id }); + + // Get all unread notifications first + const allUnreadNotifications = await Notification.find({ userId: decoded.id, status: 'unread' }); - res.json({ count }); + // Filter out notifications where the type is disabled in user preferences + const filteredUnreadNotifications = allUnreadNotifications.filter(notification => { + const typeEnabled = preferences.preferences.types[notification.type]; + return typeEnabled !== false; // Only show if explicitly enabled or undefined + }); + + res.json({ count: filteredUnreadNotifications.length }); } catch (error) { console.error('Error fetching unread count:', error); res.status(500).json({ error: 'Error fetching notification count' }); @@ -755,15 +767,26 @@ app.get('/api/notifications/archived', async (req, res) => { if (!token) return res.status(401).json({ error: 'Unauthorized' }); const decoded = jwt.verify(token, JWT_SECRET); + + // Get user preferences + const preferences = await UserNotificationPreference.findOne({ userId: decoded.id }) || + new UserNotificationPreference({ userId: decoded.id }); + const limit = Math.min(parseInt(req.query.limit) || 20, 100); const offset = parseInt(req.query.offset) || 0; - const notifications = await notificationService.getArchivedNotifications( + let notifications = await notificationService.getArchivedNotifications( decoded.id, limit, offset ); + // Filter out notifications where the type is disabled in user preferences + notifications = notifications.filter(notification => { + const typeEnabled = preferences.preferences.types[notification.type]; + return typeEnabled !== false; // Only show if explicitly enabled or undefined + }); + res.json({ notifications }); } catch (error) { console.error('Error fetching archived notifications:', error); @@ -800,6 +823,79 @@ app.patch('/api/notifications/:id/restore', async (req, res) => { } }); +// Get notification preferences +app.get('/api/notification-preferences', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + + // First check Redis cache + const cachedPreferences = await client.get(`preferences:${decoded.id}`); + if (cachedPreferences) { + return res.json(JSON.parse(cachedPreferences)); + } + + // Use Mongoose model (connected to EchoNet) + const preferences = await UserNotificationPreference.findOne({ userId: decoded.id }); + + if (!preferences) { + // Create default preferences if not found + const defaultPreferences = new UserNotificationPreference({ + userId: decoded.id, + preferences: { + channels: { inApp: true, email: false, push: false }, + types: { system: true, donation: true, request: true, user: false }, + doNotDisturb: { enabled: false, startTime: '22:00', endTime: '07:00', days: [0, 1, 2, 3, 4, 5, 6] } + } + }); + + await defaultPreferences.save(); + await client.setEx(`preferences:${decoded.id}`, 3600, JSON.stringify(defaultPreferences)); + return res.json(defaultPreferences); + } + + // Cache in Redis for future requests + await client.setEx(`preferences:${decoded.id}`, 3600, JSON.stringify(preferences)); + res.json(preferences); + } catch (error) { + console.error('Error fetching preferences:', error); + res.status(500).json({ error: 'Error fetching preferences' }); + } +}); + +app.put('/api/notification-preferences', async (req, res) => { + try { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) return res.status(401).json({ error: 'Unauthorized' }); + + const decoded = jwt.verify(token, JWT_SECRET); + const { channels, types, doNotDisturb } = req.body; + + // Use Mongoose model (connected to EchoNet) + const preferences = await UserNotificationPreference.findOneAndUpdate( + { userId: decoded.id }, + { + preferences: { channels, types, doNotDisturb } + }, + { + new: true, + upsert: true, + runValidators: true + } + ); + + // Update Redis cache + await client.setEx(`preferences:${decoded.id}`, 3600, JSON.stringify(preferences)); + + res.json(preferences); + } catch (error) { + console.error('Error updating notification preferences:', error); + res.status(500).json({ error: 'Error updating preferences' }); + } +}); + // Other routes require('./routes/auth.routes')(app); require('./routes/user.routes')(app); @@ -894,6 +990,10 @@ app.get("/admin-notifications", (req, res) => { res.sendFile(path.join(__dirname, 'public/admin/notifications.html')); }); +app.get("/notification-preferences", (req, res) => { + res.sendFile(path.join(__dirname, 'public/admin/notification-preferences.html')); +}); + app.get("/login", (req, res) => { res.sendFile(path.join(__dirname, 'public/login.html')); }) diff --git a/src/Components/HMI/ui/services/dispatchService.js b/src/Components/HMI/ui/services/dispatchService.js index e2819638e..1fdb7a1d8 100644 --- a/src/Components/HMI/ui/services/dispatchService.js +++ b/src/Components/HMI/ui/services/dispatchService.js @@ -1,19 +1,30 @@ const { UserNotificationPreference } = require('../model/notification.model'); +const { User } = require('../model/user.model'); const nodemailer = require('nodemailer'); class DispatchService { constructor() { this.channels = { - inApp: this.sendInApp, - email: this.sendEmail, - push: this.sendPush + inApp: this.sendInApp.bind(this), + email: this.sendEmail.bind(this), + push: this.sendPush.bind(this) }; - // Initialize email transporter (mock for now) + + // Notification type configuration + this.typeConfig = { + system: { defaultEnabled: true, priority: 'high' }, + donation: { defaultEnabled: true, priority: 'medium' }, + request: { defaultEnabled: true, priority: 'high' }, + user: { defaultEnabled: false, priority: 'low' }, + alert: { defaultEnabled: true, priority: 'critical' } + }; + + // Initialize email transporter this.transporter = nodemailer.createTransport({ service: 'gmail', auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS + user: process.env.EMAIL_USER || 'echodatabytes@gmail.com', + pass: process.env.EMAIL_PASS || 'ltzoycrrkpeipngi' } }); } @@ -23,24 +34,21 @@ class DispatchService { const preferences = await UserNotificationPreference.findOne({ userId }) || new UserNotificationPreference({ userId }); - // Check Do Not Disturb first - if (this.isInDoNotDisturb(preferences)) { - console.log(`Notification suppressed for user ${userId} during DND hours`); - return { suppressed: true, reason: 'DND' }; - } + console.log('User preferences:', JSON.stringify(preferences, null, 2)); + // Initialize results object const results = {}; if (channel) { - // Send to specific channel only - if (preferences.preferences[channel]) { - results[channel] = await this.channels[channel](userId, notification); + // Send to specific channel only if enabled + if (preferences.preferences.channels[channel]) { + results[channel] = await this.channels[channel](userId, notification, preferences); } } else { // Send to all enabled channels - for (const [channelName, isEnabled] of Object.entries(preferences.preferences)) { + for (const [channelName, isEnabled] of Object.entries(preferences.preferences.channels)) { if (isEnabled && this.channels[channelName]) { - results[channelName] = await this.channels[channelName](userId, notification); + results[channelName] = await this.channels[channelName](userId, notification, preferences); } } } @@ -52,14 +60,16 @@ class DispatchService { } } - async sendInApp(userId, notification) { + async sendInApp(userId, notification, preferences) { try { - // This would emit via Socket.io in a real implementation console.log(`In-app notification to user ${userId}: ${notification.title}`); - // Simulate real-time notification + // Emit via Socket.io if (global.io) { - global.io.to(`user_${userId}`).emit('newNotification', notification); + global.io.to(`user_${userId}`).emit('newNotification', { + ...notification.toObject ? notification.toObject() : notification, + timestamp: new Date() + }); } return { success: true, channel: 'inApp' }; @@ -69,19 +79,22 @@ class DispatchService { } } - async sendEmail(userId, notification) { + async sendEmail(userId, notification, preferences) { try { - // In a real implementation, you would: - // 1. Look up user email - // 2. Send actual email - console.log(`Email notification to user ${userId}: ${notification.title}`); + // Get user email from database + const user = await User.findById(userId); + if (!user || !user.email) { + console.log(`User ${userId} not found or no email address`); + return { success: false, error: 'User email not found' }; + } + + console.log(`Email notification to user ${userId} (${user.email}): ${notification.title}`); - // Mock implementation const mailOptions = { - from: process.env.EMAIL_FROM, - to: 'user@example.com', // Would be user's actual email - subject: notification.title, - text: notification.message, + from: process.env.EMAIL_FROM || 'echodatabytes@gmail.com', + to: user.email, + subject: `EchoNet: ${notification.title}`, + text: this.generateEmailText(notification), html: this.generateEmailTemplate(notification) }; @@ -93,10 +106,13 @@ class DispatchService { } } - async sendPush(userId, notification) { + async sendPush(userId, notification, preferences) { try { console.log(`Push notification to user ${userId}: ${notification.title}`); - // Push notification logic would go here + + // TODO: Implement actual push notification logic + // This would integrate with Firebase Cloud Messaging, OneSignal, etc. + return { success: true, channel: 'push' }; } catch (error) { console.error('Error sending push notification:', error); @@ -104,38 +120,103 @@ class DispatchService { } } - isInDoNotDisturb(preferences) { - if (!preferences.doNotDisturb?.enabled) return false; - - const now = new Date(); - const [startHour, startMinute] = preferences.doNotDisturb.startTime.split(':').map(Number); - const [endHour, endMinute] = preferences.doNotDisturb.endTime.split(':').map(Number); - - const startTime = new Date(); - startTime.setHours(startHour, startMinute, 0, 0); + generateEmailText(notification) { + return ` +${notification.title} - const endTime = new Date(); - endTime.setHours(endHour, endMinute, 0, 0); +${notification.message} - if (startTime > endTime) { - return now >= startTime || now <= endTime; - } +${notification.link ? `View more: ${notification.link}` : ''} - return now >= startTime && now <= endTime; +--- +Sent from EchoNet Notification System +${new Date().toLocaleString()} + `.trim(); } generateEmailTemplate(notification) { + const iconConfig = { + system: '🔔', + donation: '💰', + request: '📝', + user: '👤', + alert: '🚨' + }; + + const icon = iconConfig[notification.type] || '🔔'; + return ` -
-

${notification.title}

-

${notification.message}

-
-

- Sent from Notification System • ${new Date().toLocaleString()} -

-
+ + + + + + + +
+
+
${icon}
+

EchoNet Notification

+
+
+

${notification.title}

+

${notification.message}

+ + ${notification.link ? ` + + ` : ''} + + +
+
+ + `; } + + // Helper method to check if user should receive a specific type of notification + async shouldReceiveNotification(userId, notificationType) { + try { + const preferences = await UserNotificationPreference.findOne({ userId }) || + new UserNotificationPreference({ userId }); + + return this.isNotificationTypeEnabled(preferences, notificationType) && + !this.isInDoNotDisturb(preferences); + } catch (error) { + console.error('Error checking notification preferences:', error); + return true; // Default to allowing notifications on error + } + } + + // Method to get user notification settings + async getUserNotificationSettings(userId) { + try { + const preferences = await UserNotificationPreference.findOne({ userId }) || + new UserNotificationPreference({ userId }); + + return { + channels: preferences.preferences.channels, + types: preferences.preferences.types, + doNotDisturb: preferences.preferences.doNotDisturb + }; + } catch (error) { + console.error('Error getting user notification settings:', error); + return null; + } + } } module.exports = new DispatchService(); \ No newline at end of file diff --git a/src/Components/HMI/ui/services/notificationService.js b/src/Components/HMI/ui/services/notificationService.js index adf529291..bf4703f6c 100644 --- a/src/Components/HMI/ui/services/notificationService.js +++ b/src/Components/HMI/ui/services/notificationService.js @@ -1,4 +1,4 @@ -const { Notification } = require('../model/notification.model'); +const { Notification, UserNotificationPreference } = require('../model/notification.model'); const dispatchService = require('./dispatchService'); class NotificationService { @@ -39,6 +39,28 @@ class NotificationService { async createAndDispatch(userId, title, message, type = 'system', metadata = {}, channel = null) { try { + const preferences = await UserNotificationPreference.findOne({ userId }) || + new UserNotificationPreference({ userId }); + + // Check if this notification type is enabled + if (!this.isNotificationTypeEnabled(preferences, type)) { + console.log(`Notification type ${type} is disabled for user ${userId}`); + return { + notification: null, + dispatchResults: { suppressed: true, reason: 'type_disabled' } + }; + } + + // Check Do Not Disturb + if (type !== 'alert' && this.isInDoNotDisturb(preferences)) { + console.log(`Notification suppressed for user ${userId} during DND hours`); + return { + notification: null, + dispatchResults: { suppressed: true, reason: 'DND' } + }; + } + + // Only create notification if it will be dispatched const notification = await this.createNotification(userId, title, message, type, metadata); const dispatchResults = await dispatchService.dispatch(userId, notification, channel); @@ -52,6 +74,51 @@ class NotificationService { } } + isNotificationTypeEnabled(preferences, notificationType) { + // Critical alerts always go through + if (notificationType === 'alert') { + return true; + } + + // Check if this specific type is enabled in user preferences + const typeEnabled = preferences.preferences.types[notificationType]; + + if (typeEnabled !== undefined) { + return typeEnabled; + } + + // Fallback to default configuration + return this.typeConfig[notificationType]?.defaultEnabled || false; + } + + isInDoNotDisturb(preferences) { + if (!preferences.preferences.doNotDisturb?.enabled) { + return false; + } + + const now = new Date(); + const currentDay = now.getDay(); + + if (!preferences.preferences.doNotDisturb.days.includes(currentDay)) { + return false; + } + + const [startHour, startMinute] = preferences.preferences.doNotDisturb.startTime.split(':').map(Number); + const [endHour, endMinute] = preferences.preferences.doNotDisturb.endTime.split(':').map(Number); + + const startTime = new Date(); + startTime.setHours(startHour, startMinute, 0, 0); + + const endTime = new Date(); + endTime.setHours(endHour, endMinute, 0, 0); + + if (startTime > endTime) { + return now >= startTime || now <= endTime; + } + + return now >= startTime && now <= endTime; + } + async getUserNotifications(userId, limit = 20, offset = 0, status = null) { try { const query = { userId }; @@ -204,6 +271,36 @@ class NotificationService { } } + async getUserNotificationsWithFilter(userId, limit = 20, offset = 0, status = null) { + try { + // Get user preferences + const preferences = await UserNotificationPreference.findOne({ userId }) || + new UserNotificationPreference({ userId }); + + const query = { userId }; + if (status) { + query.status = status; + } + + // Get all notifications first + let notifications = await Notification.find(query) + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); + + // Filter out notifications where the type is disabled in user preferences + notifications = notifications.filter(notification => { + const typeEnabled = preferences.preferences.types[notification.type]; + return typeEnabled !== false; // Only show if explicitly enabled or undefined + }); + + return notifications; + } catch (error) { + console.error('Error fetching user notifications with filter:', error); + throw error; + } + } + getIconForType(type) { return this.typeConfig[type]?.icon || this.typeConfig.system.icon; } diff --git a/src/Components/HMI/utils/tokenUtils.js b/src/Components/HMI/utils/tokenUtils.js new file mode 100644 index 000000000..470ce6eef --- /dev/null +++ b/src/Components/HMI/utils/tokenUtils.js @@ -0,0 +1,24 @@ +const jwt = require('jsonwebtoken'); +const config = require('../config/auth.config'); + +function verifyToken(token) { + try { + return jwt.verify(token, config.secret); + } catch (error) { + throw new Error('Invalid token'); + } +} + +function getUserIdFromToken(token) { + try { + const decoded = verifyToken(token); + return decoded.id; + } catch (error) { + return null; + } +} + +module.exports = { + verifyToken, + getUserIdFromToken +}; \ No newline at end of file diff --git a/src/Components/MongoDB/init/init-mongo.js b/src/Components/MongoDB/init/init-mongo.js index 4c5f408f4..6e5189532 100644 --- a/src/Components/MongoDB/init/init-mongo.js +++ b/src/Components/MongoDB/init/init-mongo.js @@ -20,9 +20,13 @@ apidb.createCollection("microphones"); apidb.createCollection("movements"); apidb.createCollection("species"); apidb.createCollection("nodes"); +apidb.createCollection("UserNotificationPreference"); + //const eventsData = JSON.parse(cat('/docker-entrypoint-initdb.d/events.json')); //db.events.insertMany(eventsData); +const notificationPreferencesData = JSON.parse(cat('/docker-entrypoint-initdb.d/notification-preferences-seed.json')); +apidb.UserNotificationPreference.insertMany(notificationPreferencesData); const microphonesData = JSON.parse(cat('/docker-entrypoint-initdb.d/microphones.json')); apidb.microphones.insertMany(microphonesData); diff --git a/src/Components/MongoDB/init/notification-preferences-seed.json b/src/Components/MongoDB/init/notification-preferences-seed.json new file mode 100644 index 000000000..153899697 --- /dev/null +++ b/src/Components/MongoDB/init/notification-preferences-seed.json @@ -0,0 +1,18 @@ +[ + { + "userId": "64bf20397e048a9822077b74", + "preferences": { + "channels": { "inApp": true, "email": true, "push": false }, + "types": { "system": true, "donation": true, "request": false, "user": false }, + "doNotDisturb": { "enabled": false, "startTime": "22:00", "endTime": "07:00", "days": [0, 1, 2, 3, 4, 5, 6] } + } + }, + { + "userId": "64bf206f7e048a9822077b7a", + "preferences": { + "channels": { "inApp": true, "email": false, "push": true }, + "types": { "system": true, "donation": false, "request": true, "user": false }, + "doNotDisturb": { "enabled": false, "startTime": "22:00", "endTime": "07:00", "days": [0, 1, 2, 3, 4, 5, 6] } + } + } +] \ No newline at end of file