diff --git a/package-lock.json b/package-lock.json index e89c1a5..4d57c29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "firebase-admin": "^11.10.1", "googleapis": "^123.0.0", "nodemailer": "^6.9.4", + "path": "^0.12.7", "pg": "^8.11.2", - "pg-promise": "^11.5.3" + "pg-promise": "^11.5.3", + "web-push": "^3.6.7" } }, "node_modules/@babel/parser": { @@ -614,6 +616,17 @@ "node": ">=8" } }, + "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==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/assert-options": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.1.tgz", @@ -669,6 +682,11 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "optional": true }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -1703,6 +1721,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "engines": { + "node": ">=16" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2249,6 +2275,11 @@ "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==" + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -2434,6 +2465,15 @@ "node": ">= 0.8" } }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2620,6 +2660,14 @@ "node": ">= 0.8.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proto3-json-serializer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz", @@ -3333,12 +3381,25 @@ "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, "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==", "optional": true }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3363,6 +3424,68 @@ "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==", + "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/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/web-push/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/web-push/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/web-push/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 23817d3..570d9df 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "firebase-admin": "^11.10.1", "googleapis": "^123.0.0", "nodemailer": "^6.9.4", + "path": "^0.12.7", "pg": "^8.11.2", - "pg-promise": "^11.5.3" + "pg-promise": "^11.5.3", + "web-push": "^3.6.7" } } diff --git a/routes/items.js b/routes/items.js index 9a44e3c..e10305d 100644 --- a/routes/items.js +++ b/routes/items.js @@ -1,16 +1,17 @@ const express = require("express"); const sendEmail = require("../utils"); const fs = require("fs"); +const webpush = require("web-push"); const path = require("path"); const middleware = require("../middleware"); +// const notificationRouter = require("./notification"); const itemsRouter = express.Router(); const pool = require("../server/db"); const templatePath = path.join(__dirname, "../emailTemplate/index.html"); const template = fs.readFileSync(templatePath, "utf-8"); const isPositionWithinBounds = require("../util/inbound"); -const {leaderboardTable, itemsTable} = require("../config/db-config.js"); - +const { leaderboardTable, itemsTable } = require("../config/db-config.js"); //Add a item itemsRouter.post("/", async (req, res) => { @@ -56,11 +57,19 @@ itemsRouter.post("/", async (req, res) => { [email] ); + // query to get keys of users subscribed for push notifications + const userNotifKeys = await pool.query( + `SELECT notification_key FROM ${leaderboardTable} WHERE email!=$1 AND subscription=True AND notification_key IS NOT NULL`, + [email] + ); + res.json(item.rows[0]); // send the response immediately after adding the item let contentString = ""; // COMMENT OUT FOR TESTING PURPOSES if (process.env.NODE_ENV === "production") { + // Send push and email notifications to all subscribed users + function sendDelayedEmail(index) { if (index >= subscribedUsers.rows.length) return; @@ -85,6 +94,29 @@ itemsRouter.post("/", async (req, res) => { setTimeout(() => sendDelayedEmail(index + 1), 500); // recursive call to iterate through all user emails } + function sendPushNotifications( + subscriptionKeys, + notificationTitle, + notificationBody + ) { + console.log("Sending push notifications..."); + const payload = JSON.stringify({ + title: notificationTitle, + body: notificationBody, + }); + + subscriptionKeys.forEach((subscription) => { + webpush + .sendNotification(JSON.parse(subscription), payload) + .catch(console.log); + }); + } + + sendPushNotifications( + userNotifKeys.rows.map((row) => row.notification_key), + "A nearby item was added.", + `A new item, ${name}, was added to ZotnFound!` + ); sendDelayedEmail(0); } } catch (error) { @@ -188,7 +220,9 @@ itemsRouter.get("/year", async (req, res) => { itemsRouter.get("/:id", async (req, res) => { try { const { id } = req.params; - const item = await pool.query(`SELECT * FROM ${itemsTable} WHERE id=$1`, [id]); + const item = await pool.query(`SELECT * FROM ${itemsTable} WHERE id=$1`, [ + id, + ]); res.json(item.rows[0]); } catch (error) { console.error(error); @@ -199,7 +233,10 @@ itemsRouter.get("/:id", async (req, res) => { itemsRouter.get("/:id/email", middleware.decodeToken, async (req, res) => { try { const { id } = req.params; - const item = await pool.query(`SELECT email FROM ${itemsTable} WHERE id=$1`, [id]); + const item = await pool.query( + `SELECT email FROM ${itemsTable} WHERE id=$1`, + [id] + ); res.json(item.rows[0]); } catch (error) { console.error(error); @@ -210,9 +247,10 @@ itemsRouter.get("/:id/email", middleware.decodeToken, async (req, res) => { itemsRouter.get("/category/:category", async (req, res) => { try { const { category } = req.params; - const items = await pool.query(`SELECT * FROM ${itemsTable} WHERE type=$1`, [ - category, - ]); + const items = await pool.query( + `SELECT * FROM ${itemsTable} WHERE type=$1`, + [category] + ); res.json(items.rows); } catch (error) { console.error(error); diff --git a/routes/leaderboard.js b/routes/leaderboard.js index 08d1e96..6e0915e 100644 --- a/routes/leaderboard.js +++ b/routes/leaderboard.js @@ -1,7 +1,7 @@ const express = require("express"); const leaderboardRouter = express.Router(); const middleware = require("../middleware"); -const {leaderboardTable} = require("../config/db-config.js"); +const { leaderboardTable } = require("../config/db-config.js"); const pool = require("../server/db"); // add a user to leaderboard @@ -75,6 +75,28 @@ leaderboardRouter.patch( } ); +// Update user's notification subscription key +leaderboardRouter.patch( + "/changeNotificationKey", + middleware.decodeToken, + async (req, res) => { + try { + const { notificationKey, email } = req.body; + if (!notificationKey || !email) { + return res.status(400).send("Invalid request parameters"); + } + await pool.query( + `UPDATE ${leaderboardTable} SET notification_key=$1 WHERE email=$2`, + [notificationKey, email] + ); + res.send("Notification subscription key updated successfully!"); + } catch (err) { + console.error(err); + res.status(500).send("Internal server error"); + } + } +); + // update user's points leaderboardRouter.put("/", middleware.decodeToken, async (req, res) => { const { email, pointsToAdd } = req.body; // Assume you're sending email and pointsToAdd in the request body @@ -98,10 +120,10 @@ leaderboardRouter.put("/", middleware.decodeToken, async (req, res) => { const newPoints = currentPoints + pointsToAdd; // Now, update the user's points in the leaderboard - await pool.query(`UPDATE ${leaderboardTable} SET points=$1 WHERE email=$2`, [ - newPoints, - email, - ]); + await pool.query( + `UPDATE ${leaderboardTable} SET points=$1 WHERE email=$2`, + [newPoints, email] + ); res.send("Points updated successfully"); } catch (err) { diff --git a/routes/notification.js b/routes/notification.js new file mode 100644 index 0000000..70b53ae --- /dev/null +++ b/routes/notification.js @@ -0,0 +1,81 @@ +const express = require("express"); +const webpush = require("web-push"); +const path = require("path"); +const cors = require("cors"); + +const notificationRouter = express(); +notificationRouter.use(cors()); +notificationRouter.use(express.static(path.join(__dirname, "client"))); +notificationRouter.use(express.json()); + +const publicVapidKey = process.env.PUBLIC_VAPID_KEY; + +const privateVapidKey = process.env.PRIVATE_VAPID_KEY; + +webpush.setVapidDetails( + "mailto:test@test.com", + publicVapidKey, + privateVapidKey +); + +notificationRouter.post("/", (req, res) => { + console.log("Pushing Notifications to User"); + const subscription = req.body; + res.status(201).json({}); + const payload = JSON.stringify({ + title: "TEST NOTIFICATION", + body: "HEHEHEHEHEHEH", + }); + console.log(subscription); + webpush.sendNotification(subscription, payload).catch(console.log); +}); + +// function sendPushNotifications( +// subscriptionKeys, +// notificationTitle, +// notificationBody +// ) { +// console.log("Sending Push Notifications"); +// const payload = JSON.stringify({ +// title: notificationTitle, +// body: notificationBody, +// }); +// console.log(subscriptionKeys); + +// subscriptionKeys.forEach((subscription) => +// webpush +// .sendNotification(JSON.parse(subscription), payload) +// .catch(console.log) +// ); +// } + +// // Send notifications to all subscribed users +// notificationRouter.post("/keys", (req, res) => { +// console.log("Pushing Notifications to All Subscribed Users"); +// const { subscriptionKeys, notificationTitle, notificationBody } = req.body; +// res.status(201).json({}); + +// console.log("Sending Push Notifications"); +// const payload = JSON.stringify({ +// title: notificationTitle, +// body: notificationBody, +// }); +// console.log(subscriptionKeys); + +// subscriptionKeys.forEach((subscription) => +// webpush +// .sendNotification(JSON.parse(subscription), payload) +// .catch(console.log) +// ); +// }); + +notificationRouter.get("/test", (req, res) => { + console.log("Testing Routing!"); + res.status(201).json({ message: "Routing is working!" }); +}); + +module.exports = notificationRouter; +// module.exports = { +// router: notificationRouter, +// sendPushNotifications: sendPushNotifications, +// }; diff --git a/server.js b/server.js index f64869f..66918bc 100644 --- a/server.js +++ b/server.js @@ -8,6 +8,7 @@ const port = 3001; const items = require("./routes/items"); const nodemailer = require("./routes/nodeMailer"); const leaderboard = require("./routes/leaderboard"); +const notification = require("./routes/notification"); app.use(cors()); app.use(express.json({ limit: "25mb" })); @@ -27,6 +28,7 @@ app.get("/", async (req, res) => { app.use("/items", items); app.use("/leaderboard", leaderboard); app.use("/nodemailer", nodemailer); +app.use("/notification", notification); app.listen(port, () => { console.log(`server is running on ${port}`); diff --git a/server/database.sql b/server/database.sql index c16cd2c..c0c0de6 100644 --- a/server/database.sql +++ b/server/database.sql @@ -20,5 +20,6 @@ CREATE TABLE leaderboard( id SERIAL PRIMARY KEY, email VARCHAR(255), points INTEGER, - subscription BOOLEAN + subscription BOOLEAN, + notification_key VARCHAR(500) ) \ No newline at end of file