Skip to content

Commit

Permalink
Merge pull request #74 from icssc/33-feat-email-subscription-for-cert…
Browse files Browse the repository at this point in the history
…ain-keywords-or-item-type

feat: Email subscription for certain keywords or item type
  • Loading branch information
NwinNwin authored Jan 21, 2025
2 parents b2fbaad + ab6babe commit 138263d
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 131 deletions.
1 change: 1 addition & 0 deletions packages/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"vitest": "^1.5.3"
},
"dependencies": {
"aws-sdk": "^2.1692.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
Expand Down
3 changes: 3 additions & 0 deletions packages/functions/src/config/db-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ const config = {
development: {
leaderboardTable: "dev.leaderboard",
itemsTable: "dev.items",
searchesTable: "dev.searches"
},
production: {
leaderboardTable: "public.leaderboard",
itemsTable: "public.items",
searchesTable: "public.searches"
},
// Add more environments if needed
};
Expand All @@ -16,3 +18,4 @@ const table = config[process.env.NODE_ENV] || config.development;

export const leaderboardTable = table.leaderboardTable;
export const itemsTable = table.itemsTable;
export const searchesTable = table.searchesTable;
12 changes: 12 additions & 0 deletions packages/functions/src/routes/email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import express from "express";
import sendEmail from "../util/sendEmail";


const emailRouter = express.Router();

emailRouter.get("/", async (req, res) => {
sendEmail("[email protected]", "test email", "email test");
res.json("email sent");
});

export default emailRouter;
53 changes: 52 additions & 1 deletion packages/functions/src/routes/items.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import express from "express";

import client from "../server/db.js";
import sendEmail from "../util/sendEmail";
// const middleware = require("../middleware/index.js");
const itemsRouter = express.Router();

// const templatePath = path.resolve(__dirname, "../emailTemplate/index.html");
// const template = fs.readFileSync(templatePath, "utf-8");
import isPositionWithinBounds from "../util/inbound.js";
import { leaderboardTable, itemsTable } from "../config/db-config.js";
import { leaderboardTable, itemsTable, searchesTable } from "../config/db-config.js";

//Add a item
itemsRouter.post("/", async (req, res) => {
Expand Down Expand Up @@ -64,6 +65,56 @@ itemsRouter.post("/", async (req, res) => {
]
);

// // query to get user emails subscribed to relevant keywords
// const subscribers = await client.query(
// `SELECT emails
// FROM ${searchesTable}
// WHERE keyword IN ($1, $2, $3);`,
// [name, description, type]
// );

// // add emails to set to remove duplicates
// const emailSet = new Set();
// subscribers.rows.forEach(row => {
// row.emails.forEach(email => {
// uniqueEmails.add(email);
// });
// });

// const uniqueEmails = Array.from(emailSet);
// console.log(uniqueEmails);

// 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") {
// function sendDelayedEmail(index) {
// if (index >= uniqueEmails.length) return;

// let email = uniqueEmails[index].email;
// contentString += `A new item, ${name}, is added to ZotnFound!`;

// const dynamicContent = {
// content: contentString,
// image: image,
// url: `https://zotnfound.com/${item.rows[0].id}`,
// };

// // const customizedTemplate = template
// // .replace("{{content}}", dynamicContent.content)
// // .replace("{{image}}", dynamicContent.image)
// // .replace("{{url}}", dynamicContent.url);

// // sendEmail(email, "A nearby item was added.", customizedTemplate);

// contentString = "";
// console.log("sent " + email);
// setTimeout(() => sendDelayedEmail(index + 1), 500); // recursive call to iterate through all user emails
// }

// sendDelayedEmail(0);
// }
// Send a success response
res.status(200).json({ message: "Item was added successfully." });
} catch (error) {
Expand Down
91 changes: 91 additions & 0 deletions packages/functions/src/routes/searches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import express from "express";
import client from "../server/db.js";
import { searchesTable } from "../config/db-config.js";

const searchRouter = express.Router();

// Add a search keyword and/or email
searchRouter.post("/", async (req, res) => {
try {
const { keyword, email } = req.body;

// search for emails subscribed to certain keyword
const search = await client.query(
`SELECT emails
FROM ${searchesTable}
WHERE keyword = $1`,
[keyword]);

if (search.rows.length == 1){
// subscribe user to keyword notification if they aren't already subscribed to it
if (!search.rows[0].emails.includes(email)){
const item = await client.query(
`UPDATE ${searchesTable}
SET emails = array_append(emails, $1)
WHERE keyword = $2
RETURNING *`,
[email, keyword]);
res.json(item.rows[0]);
console.log("updated subscribers of", keyword);
}
else {
res.json("email already subscribed to keyword");
}
} else { // keyword doesn't exist in table yet, add new row
const item = await client.query(
`INSERT INTO ${searchesTable} (keyword, emails) VALUES($1, $2) RETURNING *`,
[keyword, [email]]
);
console.log("inserted new row");
res.json(item.rows[0]);
}
} catch (error) {
console.error(error);
}
});

// Remove user's keyword subscription
searchRouter.delete("/", async (req, res) => {
try {
const { keyword, email } = req.body;

const updatedSubscription = await client.query(
`UPDATE ${searchesTable}
SET emails = array_remove(emails, $1)
WHERE keyword = $2
RETURNING *`,
[email, keyword]
);

if (updatedSubscription.rowCount === 0) {
return res.status(404).json({message: "keyword not found"});
}

res.json(updatedSubscription.rows[0]);
} catch (error) {
console.error(error.message);
res.status(500).send("Server error");
}
});

// Find all keywords that user is subscribed to
searchRouter.get("/:email", async (req, res) => {
try {
const { email } = req.params;

const keywords = await client.query(
`SELECT keyword
FROM ${searchesTable}
WHERE $1 = ANY(emails);`,
[email]
);

const keywordList = keywords.rows.map(row => row.keyword);
res.json(keywordList);
} catch (error) {
console.error(error.message);
res.status(500).send("Server error");
}
});

export default searchRouter;
5 changes: 5 additions & 0 deletions packages/functions/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import items from "./routes/items.js";
import nodemailer from "./routes/nodeMailer.js";
import leaderboard from "./routes/leaderboard.js";
import upload from "./routes/upload.js";
import email from "./routes/email.js";
import searches from "./routes/searches.js";

app.use(cors());
app.use(express.json({ limit: "25mb" }));
Expand All @@ -32,9 +34,12 @@ app.get("/", async (req, res) => {
console.error(error);
}
});

app.use("/items", items);
app.use("/leaderboard", leaderboard);
app.use("/nodemailer", nodemailer);
app.use("/upload", upload);
app.use("/email", email);
app.use("/searches", searches);

export const handler = serverless(app);
7 changes: 6 additions & 1 deletion packages/functions/src/server/database.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ CREATE TABLE leaderboard(
email VARCHAR(255),
points INTEGER,
subscription BOOLEAN
)
);

CREATE TABLE SEARCHES (
keyword VARCHAR(50) PRIMARY KEY,
emails VARCHAR(254)[]
);
21 changes: 1 addition & 20 deletions packages/functions/src/server/db.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
// const Pool = require("pg").Pool;
// const pgp = require("pg-promise")({});

// const pool = new Pool({
// user: process.env.AWS_USER,
// password: process.env.AWS_PASSWORD,
// host: process.env.AWS_HOST,
// port: process.env.AWS_PORT,
// database: process.env.AWS_DB_NAME,
// ssl: {
// rejectUnauthorized: false,
// },
// });

import { Client } from "pg";

const client = new Client({
Expand All @@ -24,9 +10,4 @@ const client = new Client({
});
client.connect();

export default client;

// const cn = `postgres://${process.env.AWS_USER}:${encodeURIComponent(
// process.env.AWS_PASSWORD
// )}@${process.env.AWS_HOST}:${process.env.AWS_PORT}/${process.env.AWS_DB_NAME}`; // For pgp
// const db = pgp(cn);
export default client;
39 changes: 39 additions & 0 deletions packages/functions/src/util/sendEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import AWS from 'aws-sdk';

const SES_CONFIG = {
accessKeyId: process.env.AWS_SES_ACCESS_KEY,
secretAccessKey: process.env.AWS_SES_SECRET_ACCESS_KEY,
region: process.env.AWS_SES_REGION,
};

const AWS_SES = new AWS.SES(SES_CONFIG);

const sendEmail = async (email, subject, message) => {
const params = {
Destination: {
ToAddresses: [email],
},
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: message,
},
},
Subject: {
Charset: "UTF-8",
Data: subject,
},
},
Source: email,
};

try {
await AWS_SES.sendEmail(params).promise();
console.log(`Email sent to ${email}`);
} catch (error) {
console.log("Error sending email", error);
}
}

export default sendEmail;
Loading

0 comments on commit 138263d

Please sign in to comment.