From 7348cbf5737c48ea4a54ef5aa1ee1bffb9ca7773 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Fri, 11 Jul 2025 14:54:39 -0400 Subject: [PATCH 01/70] first commit on frank --- api/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api/index.js b/api/index.js index f08162e..082cf7b 100644 --- a/api/index.js +++ b/api/index.js @@ -5,3 +5,4 @@ const testDbRouter = require("./test-db"); router.use("/test-db", testDbRouter); module.exports = router; +// From 112fa4fef7823d2fe804a89dd37003e1725e0935 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Fri, 11 Jul 2025 14:59:06 -0400 Subject: [PATCH 02/70] Publishing my branch --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index af0cf82..1b30289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "license": "ISC", "dependencies": { From 2fb541aff1c111b42a7c0d003a09dd7f14686e38 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 10:21:43 -0400 Subject: [PATCH 03/70] Poll table defined --- database/poll.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 database/poll.js diff --git a/database/poll.js b/database/poll.js new file mode 100644 index 0000000..7d1a20d --- /dev/null +++ b/database/poll.js @@ -0,0 +1,36 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Poll = db.define( + "poll", + { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: DataTypes.TEXT, + allowAnonymous: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + status: { + type: DataTypes.ENUM("draft", "published", "closed"), + defaultValue: "draft", + }, + endAt: DataTypes.DATE, + allowListOnly: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + publishedAt: DataTypes.DATE, + }, + { + underscored: true, + } +); + +module.exports = Poll; From 64897c4370b32bd334724bf729baf12e0690e660 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 10:33:09 -0400 Subject: [PATCH 04/70] added the ballot to separate rankings from poll def --- README.md | 24 +----------------------- database/ballot.js | 25 +++++++++++++++++++++++++ database/index.js | 6 ++++++ database/pollOption.js | 20 ++++++++++++++++++++ 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 database/ballot.js create mode 100644 database/pollOption.js diff --git a/README.md b/README.md index b8ac36b..173d786 100644 --- a/README.md +++ b/README.md @@ -1,23 +1 @@ -# Capstone I Backend - -## Getting Started - -This project uses Express.js to serve up an API server, and Sequelize to connect to a PostgreSQL database. It uses JWTs for authentication with username and password. - -You will also need to create the database: by default it is called `capstone-1`, but you are welcome to rename it in `database/db.js` - -After that, you can get started with these commands - -``` -npm install # 📦 To install the packages -npm run seed # 🌱 To seed the database -npm run start-dev # 🚀 To start the server in development mode -``` - -This project runs in the Node.js runtime environment. We're not using Webpack here, but we are using a tool called nodemon, which re-runs our app whenever we save a file. You should see a message in the terminal telling you that the server is running on port 8080. - -When an error occurs on the backend (Express), you'll see a message in the terminal. When an error occurs on the frontend (React), you'll see that error in the browser. - -## Deployment - -This project has a vercel.json file, which will make it easier to deploy this project to Vercel. Check the video listed in the cohort repository for a walkthrough of how to connect your deployed Express server to Neon Postgres. +# FriendPollster Backend \ No newline at end of file diff --git a/database/ballot.js b/database/ballot.js new file mode 100644 index 0000000..0bb0888 --- /dev/null +++ b/database/ballot.js @@ -0,0 +1,25 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Ballot = db.define( + "ballot", + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + anonToken: { + type: DataTypes.UUID, + unique: true, + allowNull: true, + }, + submittedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + }, + { underscored: true } +); + +module.exports = Ballot; diff --git a/database/index.js b/database/index.js index e498df6..ea6f737 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,13 @@ const db = require("./db"); const User = require("./user"); +const Poll = require("./poll"); +const PollOption = require("./pollOption"); +const Ballot = require("./ballot"); module.exports = { db, User, + Poll, + PollOption, + Ballot, }; diff --git a/database/pollOption.js b/database/pollOption.js new file mode 100644 index 0000000..f0bfed1 --- /dev/null +++ b/database/pollOption.js @@ -0,0 +1,20 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollOption = db.define( + "pollOption", + { + text: { + type: DataTypes.STRING, + allowNull: false, + }, + position: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + }, + }, + { underscored: true } +); + +module.exports = PollOption; \ No newline at end of file From 66fa23013fce912504d8fce02d7d48bb61b29335 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 10:46:03 -0400 Subject: [PATCH 05/70] defined all the tables according to schema --- database/ballotRanking.js | 20 ++++++++++++++++++++ database/index.js | 4 ++++ database/pollAllowedUser.js | 9 +++++++++ 3 files changed, 33 insertions(+) create mode 100644 database/ballotRanking.js create mode 100644 database/pollAllowedUser.js diff --git a/database/ballotRanking.js b/database/ballotRanking.js new file mode 100644 index 0000000..0b7e100 --- /dev/null +++ b/database/ballotRanking.js @@ -0,0 +1,20 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const BallotRanking = db.define( + "ballotRanking", + { + rank: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + underscored: true, + indexes: [ + { unique: true, fields: ["ballot_id", "rank"] }, + ], + } +); + +module.exports = BallotRanking; diff --git a/database/index.js b/database/index.js index ea6f737..84c93b4 100644 --- a/database/index.js +++ b/database/index.js @@ -3,6 +3,8 @@ const User = require("./user"); const Poll = require("./poll"); const PollOption = require("./pollOption"); const Ballot = require("./ballot"); +const BallotRanking = require("./ballotRanking"); +const PollAllowedUser = require("./pollAllowedUser"); module.exports = { db, @@ -10,4 +12,6 @@ module.exports = { Poll, PollOption, Ballot, + BallotRanking, + PollAllowedUser, }; diff --git a/database/pollAllowedUser.js b/database/pollAllowedUser.js new file mode 100644 index 0000000..c32165c --- /dev/null +++ b/database/pollAllowedUser.js @@ -0,0 +1,9 @@ +const db = require("./db"); + +const PollAllowedUser = db.define( + "pollAllowedUser", + {}, + { timestamps: false, underscored: true } +); + +module.exports = PollAllowedUser; From 63dac2c61e6d9418982db087d9fe7d485cc7e6d1 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 11:21:16 -0400 Subject: [PATCH 06/70] db schema is always matched to the code --- app.js | 3 ++- database/ballotRanking.js | 2 +- database/index.js | 41 +++++++++++++++++++++++++++++++++------ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/app.js b/app.js index 5857036..0363675 100644 --- a/app.js +++ b/app.js @@ -39,7 +39,8 @@ app.use((err, req, res, next) => { const runApp = async () => { try { - await db.sync(); + // we have to keep it true, so that schema always matches the code before we happy with it + await db.sync({force: true}); console.log("✅ Connected to the database"); app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); diff --git a/database/ballotRanking.js b/database/ballotRanking.js index 0b7e100..1753bf7 100644 --- a/database/ballotRanking.js +++ b/database/ballotRanking.js @@ -17,4 +17,4 @@ const BallotRanking = db.define( } ); -module.exports = BallotRanking; +module.exports = BallotRanking; \ No newline at end of file diff --git a/database/index.js b/database/index.js index 84c93b4..ad9e089 100644 --- a/database/index.js +++ b/database/index.js @@ -1,11 +1,40 @@ const db = require("./db"); -const User = require("./user"); -const Poll = require("./poll"); -const PollOption = require("./pollOption"); -const Ballot = require("./ballot"); -const BallotRanking = require("./ballotRanking"); +const User = require("./user"); +const Poll = require("./poll"); +const PollOption = require("./pollOption"); +const Ballot = require("./ballot"); +const BallotRanking = require("./ballotRanking"); const PollAllowedUser = require("./pollAllowedUser"); +User.hasMany(Poll, { foreignKey: "creator_id" }); +Poll.belongsTo(User, { as: "creator", foreignKey: "creator_id" }); + +Poll.hasMany(PollOption, { foreignKey: "poll_id", onDelete: "CASCADE" }); +PollOption.belongsTo(Poll, { foreignKey: "poll_id" }); + +Poll.hasMany(Ballot, { foreignKey: "poll_id", onDelete: "CASCADE" }); +Ballot.belongsTo(Poll, { foreignKey: "poll_id" }); + +User.hasMany(Ballot, { foreignKey: "user_id" }); // nullable for anonymous +Ballot.belongsTo(User, { foreignKey: "user_id" }); + +Ballot.hasMany(BallotRanking, { foreignKey: "ballot_id", onDelete: "CASCADE" }); +BallotRanking.belongsTo(Ballot, { foreignKey: "ballot_id" }); + +PollOption.hasMany(BallotRanking, { foreignKey: "option_id" }); +BallotRanking.belongsTo(PollOption, { foreignKey: "option_id" }); + +Poll.belongsToMany(User, { + through: PollAllowedUser, + as: "allowedUsers", + foreignKey: "poll_id", +}); +User.belongsToMany(Poll, { + through: PollAllowedUser, + as: "allowedPolls", + foreignKey: "user_id", +}); + module.exports = { db, User, @@ -14,4 +43,4 @@ module.exports = { Ballot, BallotRanking, PollAllowedUser, -}; +}; \ No newline at end of file From 375d573b37e9b9a8825fb91dc1f0516b5fb43c75 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Fri, 11 Jul 2025 14:59:06 -0400 Subject: [PATCH 07/70] Publishing my branch --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index af0cf82..1b30289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "license": "ISC", "dependencies": { From bef3e8cc0f55bd2b69948be5b045c7bf7da13d1c Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 10:21:43 -0400 Subject: [PATCH 08/70] Poll table defined --- database/poll.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 database/poll.js diff --git a/database/poll.js b/database/poll.js new file mode 100644 index 0000000..7d1a20d --- /dev/null +++ b/database/poll.js @@ -0,0 +1,36 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Poll = db.define( + "poll", + { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: DataTypes.TEXT, + allowAnonymous: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + status: { + type: DataTypes.ENUM("draft", "published", "closed"), + defaultValue: "draft", + }, + endAt: DataTypes.DATE, + allowListOnly: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + publishedAt: DataTypes.DATE, + }, + { + underscored: true, + } +); + +module.exports = Poll; From f95629d3a9d30ccc6031ea2d174d8f81bd77822e Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 10:33:09 -0400 Subject: [PATCH 09/70] added the ballot to separate rankings from poll def --- README.md | 24 +----------------------- database/ballot.js | 25 +++++++++++++++++++++++++ database/index.js | 6 ++++++ database/pollOption.js | 20 ++++++++++++++++++++ 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 database/ballot.js create mode 100644 database/pollOption.js diff --git a/README.md b/README.md index b8ac36b..173d786 100644 --- a/README.md +++ b/README.md @@ -1,23 +1 @@ -# Capstone I Backend - -## Getting Started - -This project uses Express.js to serve up an API server, and Sequelize to connect to a PostgreSQL database. It uses JWTs for authentication with username and password. - -You will also need to create the database: by default it is called `capstone-1`, but you are welcome to rename it in `database/db.js` - -After that, you can get started with these commands - -``` -npm install # 📦 To install the packages -npm run seed # 🌱 To seed the database -npm run start-dev # 🚀 To start the server in development mode -``` - -This project runs in the Node.js runtime environment. We're not using Webpack here, but we are using a tool called nodemon, which re-runs our app whenever we save a file. You should see a message in the terminal telling you that the server is running on port 8080. - -When an error occurs on the backend (Express), you'll see a message in the terminal. When an error occurs on the frontend (React), you'll see that error in the browser. - -## Deployment - -This project has a vercel.json file, which will make it easier to deploy this project to Vercel. Check the video listed in the cohort repository for a walkthrough of how to connect your deployed Express server to Neon Postgres. +# FriendPollster Backend \ No newline at end of file diff --git a/database/ballot.js b/database/ballot.js new file mode 100644 index 0000000..0bb0888 --- /dev/null +++ b/database/ballot.js @@ -0,0 +1,25 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Ballot = db.define( + "ballot", + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + anonToken: { + type: DataTypes.UUID, + unique: true, + allowNull: true, + }, + submittedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + }, + { underscored: true } +); + +module.exports = Ballot; diff --git a/database/index.js b/database/index.js index e498df6..ea6f737 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,13 @@ const db = require("./db"); const User = require("./user"); +const Poll = require("./poll"); +const PollOption = require("./pollOption"); +const Ballot = require("./ballot"); module.exports = { db, User, + Poll, + PollOption, + Ballot, }; diff --git a/database/pollOption.js b/database/pollOption.js new file mode 100644 index 0000000..f0bfed1 --- /dev/null +++ b/database/pollOption.js @@ -0,0 +1,20 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollOption = db.define( + "pollOption", + { + text: { + type: DataTypes.STRING, + allowNull: false, + }, + position: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + }, + }, + { underscored: true } +); + +module.exports = PollOption; \ No newline at end of file From edbf8f974e5a564cb7d966d03692b61eb3857ad0 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 10:46:03 -0400 Subject: [PATCH 10/70] defined all the tables according to schema --- database/ballotRanking.js | 20 ++++++++++++++++++++ database/index.js | 4 ++++ database/pollAllowedUser.js | 9 +++++++++ 3 files changed, 33 insertions(+) create mode 100644 database/ballotRanking.js create mode 100644 database/pollAllowedUser.js diff --git a/database/ballotRanking.js b/database/ballotRanking.js new file mode 100644 index 0000000..0b7e100 --- /dev/null +++ b/database/ballotRanking.js @@ -0,0 +1,20 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const BallotRanking = db.define( + "ballotRanking", + { + rank: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + underscored: true, + indexes: [ + { unique: true, fields: ["ballot_id", "rank"] }, + ], + } +); + +module.exports = BallotRanking; diff --git a/database/index.js b/database/index.js index ea6f737..84c93b4 100644 --- a/database/index.js +++ b/database/index.js @@ -3,6 +3,8 @@ const User = require("./user"); const Poll = require("./poll"); const PollOption = require("./pollOption"); const Ballot = require("./ballot"); +const BallotRanking = require("./ballotRanking"); +const PollAllowedUser = require("./pollAllowedUser"); module.exports = { db, @@ -10,4 +12,6 @@ module.exports = { Poll, PollOption, Ballot, + BallotRanking, + PollAllowedUser, }; diff --git a/database/pollAllowedUser.js b/database/pollAllowedUser.js new file mode 100644 index 0000000..c32165c --- /dev/null +++ b/database/pollAllowedUser.js @@ -0,0 +1,9 @@ +const db = require("./db"); + +const PollAllowedUser = db.define( + "pollAllowedUser", + {}, + { timestamps: false, underscored: true } +); + +module.exports = PollAllowedUser; From e7c5f89f4529f0490eb8d4c55098604579c5f948 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 11:21:16 -0400 Subject: [PATCH 11/70] db schema is always matched to the code --- app.js | 3 ++- database/ballotRanking.js | 2 +- database/index.js | 41 +++++++++++++++++++++++++++++++++------ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/app.js b/app.js index 5857036..0363675 100644 --- a/app.js +++ b/app.js @@ -39,7 +39,8 @@ app.use((err, req, res, next) => { const runApp = async () => { try { - await db.sync(); + // we have to keep it true, so that schema always matches the code before we happy with it + await db.sync({force: true}); console.log("✅ Connected to the database"); app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); diff --git a/database/ballotRanking.js b/database/ballotRanking.js index 0b7e100..1753bf7 100644 --- a/database/ballotRanking.js +++ b/database/ballotRanking.js @@ -17,4 +17,4 @@ const BallotRanking = db.define( } ); -module.exports = BallotRanking; +module.exports = BallotRanking; \ No newline at end of file diff --git a/database/index.js b/database/index.js index 84c93b4..ad9e089 100644 --- a/database/index.js +++ b/database/index.js @@ -1,11 +1,40 @@ const db = require("./db"); -const User = require("./user"); -const Poll = require("./poll"); -const PollOption = require("./pollOption"); -const Ballot = require("./ballot"); -const BallotRanking = require("./ballotRanking"); +const User = require("./user"); +const Poll = require("./poll"); +const PollOption = require("./pollOption"); +const Ballot = require("./ballot"); +const BallotRanking = require("./ballotRanking"); const PollAllowedUser = require("./pollAllowedUser"); +User.hasMany(Poll, { foreignKey: "creator_id" }); +Poll.belongsTo(User, { as: "creator", foreignKey: "creator_id" }); + +Poll.hasMany(PollOption, { foreignKey: "poll_id", onDelete: "CASCADE" }); +PollOption.belongsTo(Poll, { foreignKey: "poll_id" }); + +Poll.hasMany(Ballot, { foreignKey: "poll_id", onDelete: "CASCADE" }); +Ballot.belongsTo(Poll, { foreignKey: "poll_id" }); + +User.hasMany(Ballot, { foreignKey: "user_id" }); // nullable for anonymous +Ballot.belongsTo(User, { foreignKey: "user_id" }); + +Ballot.hasMany(BallotRanking, { foreignKey: "ballot_id", onDelete: "CASCADE" }); +BallotRanking.belongsTo(Ballot, { foreignKey: "ballot_id" }); + +PollOption.hasMany(BallotRanking, { foreignKey: "option_id" }); +BallotRanking.belongsTo(PollOption, { foreignKey: "option_id" }); + +Poll.belongsToMany(User, { + through: PollAllowedUser, + as: "allowedUsers", + foreignKey: "poll_id", +}); +User.belongsToMany(Poll, { + through: PollAllowedUser, + as: "allowedPolls", + foreignKey: "user_id", +}); + module.exports = { db, User, @@ -14,4 +43,4 @@ module.exports = { Ballot, BallotRanking, PollAllowedUser, -}; +}; \ No newline at end of file From 9b02ab0af2d1acbdc59eea40e40b37429ccce13a Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Mon, 14 Jul 2025 15:51:20 -0400 Subject: [PATCH 12/70] created basic user routes --- api/index.js | 4 ++-- api/test-db.js | 25 ------------------------- api/users.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ app.js | 2 +- 4 files changed, 53 insertions(+), 28 deletions(-) delete mode 100644 api/test-db.js create mode 100644 api/users.js diff --git a/api/index.js b/api/index.js index 082cf7b..7129906 100644 --- a/api/index.js +++ b/api/index.js @@ -1,8 +1,8 @@ const express = require("express"); const router = express.Router(); -const testDbRouter = require("./test-db"); +const usersRouter = require("./users"); -router.use("/test-db", testDbRouter); +router.use("/users", usersRouter); module.exports = router; // diff --git a/api/test-db.js b/api/test-db.js deleted file mode 100644 index 4109c34..0000000 --- a/api/test-db.js +++ /dev/null @@ -1,25 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { User } = require("../database"); - -// You don't actually need this route, it's just a good way to confirm that your database connection is working. -// Feel free to delete this entire file. -router.get("/", async (req, res) => { - try { - const users = await User.findAll(); - console.log(`Found ${users.length} users`); - res.json({ - message: "You successfully connected to the database 🥳", - usersCount: users.length, - }); - } catch (error) { - console.error("Error fetching users:", error); - res.status(500).json({ - error: "Failed to fetch users", - message: - "Check your database connection, and consider running your seed file: npm run seed", - }); - } -}); - -module.exports = router; diff --git a/api/users.js b/api/users.js new file mode 100644 index 0000000..666c34a --- /dev/null +++ b/api/users.js @@ -0,0 +1,50 @@ +const express = require("express"); +const router = express.Router(); +const { User } = require("../database"); + +//get all users +router.get ("/", async (req,res) => { + try{ + const users = await User.findAll(); + res.status(200).send(users); + }catch(error){ + console.error("Error fetching users: ", error); + res.status(500).send("Error Fetching Students"); + } +}); + +//get a user by id +router.get("/:id", async (req, res) => { + try { + const user = await User.findByPk(req.params.id); + + if (!user) { + return res.status(404).send("User not found"); + } + res.status(200).send(user); + } catch (error) { + console.error("Error fetching user by ID:", error); + res.status(500).send("Error fetching user"); + } +}); + +//create a user +router.post("/", async (req, res) => { + try { + + const { username, email, password } = req.body; + + const passwordHash = User.hashPassword(password); + + const newUser = {username, email, passwordHash}; + + const savedUser = await User.create(newUser); + + res.status(201).send(savedUser); + } catch (error) { + console.error("Error creating user:", error); + res.status(500).send("Error creating user"); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/app.js b/app.js index 0363675..791e510 100644 --- a/app.js +++ b/app.js @@ -40,7 +40,7 @@ app.use((err, req, res, next) => { const runApp = async () => { try { // we have to keep it true, so that schema always matches the code before we happy with it - await db.sync({force: true}); + await db.sync(); console.log("✅ Connected to the database"); app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); From 55e65969460d787304b2de8a951b95a72f397f04 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 16:02:42 -0400 Subject: [PATCH 13/70] Resolved the conflict --- api/index.js | 6 +- api/polls.js | 65 +++ app.js | 4 + package-lock.json | 1353 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- 5 files changed, 1409 insertions(+), 22 deletions(-) create mode 100644 api/polls.js diff --git a/api/index.js b/api/index.js index 7129906..797f688 100644 --- a/api/index.js +++ b/api/index.js @@ -1,8 +1,10 @@ const express = require("express"); const router = express.Router(); + const usersRouter = require("./users"); +const pollsRouter = require("./polls"); router.use("/users", usersRouter); +router.use("/polls", pollsRouter); -module.exports = router; -// +module.exports = router; \ No newline at end of file diff --git a/api/polls.js b/api/polls.js new file mode 100644 index 0000000..c193792 --- /dev/null +++ b/api/polls.js @@ -0,0 +1,65 @@ +const express = require("express"); +const router = express.Router(); +const {Poll} = require("../database"); + +router.get("/", async (req, res) => { + try { + const polls = await Poll.findAll( + // // not yet + // include: User, + // include: PollOption, + // include: Ballot, + // include: BallotRanking, + ); + console.log(`Found ${polls.length} polls`); + res.status(200).send(polls); + } catch (error) { + console.error("Error fetching polls:", error); + res.status(500).json({ + error: "Failed to fetch polls", + message: "Check your database connection", + }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id, { + // // not yet + // include: User, + // include: PollOption, + // include: Ballot, + // include: BallotRanking, + }); + res.status(200).send(Poll); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + } catch (error) { + res.status(500).json({ error: "Failed to fetch poll by id" }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id); + if (!poll) { + return res.status(404).json({ error: "poll not found" }); + } + await poll.destroy(); + res.status(200).json({ message: "Poll deleted successfully" }); + } catch (error) { + res.status(500).json({ error: "Failed to delete a poll" }); + } +}); + +router.post("/", async (req, res) => { + try { + const poll = await Poll.create(req.body); + res.status(201).send(poll); + } catch (error) { + res.status(500).json({ error: "Failed to create a poll" }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/app.js b/app.js index 934d9f9..99d4847 100644 --- a/app.js +++ b/app.js @@ -41,7 +41,11 @@ const runApp = async () => { try { // we have to keep it true, so that schema always matches the code before we happy with it await db.sync(); +<<<<<<< HEAD og("✅ Connected to the database"); +======= + console.log("✅ Connected to the database"); +>>>>>>> 142785f (Api Routes for polls) app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); }); diff --git a/package-lock.json b/package-lock.json index 1b30289..7b55c6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "start-dev": "^1.0.0" }, "devDependencies": { "nodemon": "^3.1.10" @@ -26,6 +27,263 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@beamwind/core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@beamwind/core/-/core-2.3.0.tgz", + "integrity": "sha512-unXFgXRhPYVRPYS0ew675MPH10v9C5rQmY37r6p5FQ6SbamFaW3CNO/mYoLuRV/gBvK96l9OrTDr3EExnQfHMA==", + "license": "MIT", + "dependencies": { + "tiny-css-prefixer": "^1.1.4" + } + }, + "node_modules/@beamwind/preflight": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@beamwind/preflight/-/preflight-2.0.2.tgz", + "integrity": "sha512-pGuIpblgufW5qIWp/75LcKt8pUes9WeR5cOPJdnDS8mxn9C4oDJ0VPACOYsuXa8B7n0qIFjnY8Y3LbM6LkVyPw==", + "license": "MIT" + }, + "node_modules/@beamwind/preset-tailwind": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@beamwind/preset-tailwind/-/preset-tailwind-2.0.2.tgz", + "integrity": "sha512-ceEZqzDHnrAfSSeXXPDoVT9N0WcY16I4Sr0yEy3c8yEM/oSkDXA/LoS0xnYEuht+r1ie5o+wy2NR2YnUcqMzpw==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", + "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^2.30.0" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "license": "MIT" + }, + "node_modules/@start-dev/bundle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/bundle/-/bundle-1.0.0.tgz", + "integrity": "sha512-5BUW5XB7UYv3TAczpc83NrJcVbc/Iq24BwYLt42oZ5lWIhi+d2eU93h6jWiG5Rodh//5RQaYcnan+EExmiJDmg==", + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "rollup": "^2.34.2", + "rollup-plugin-node-polyfills": "^0.2.1", + "vm2": "^3.9.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@start-dev/core/-/core-1.0.1.tgz", + "integrity": "sha512-sCVRo8wB0pLU7Y61JkeSTPBfHdQO3Ts5etWhxC808ueMOXzkBEblABNSS8pSdvP+hCOhTtNGyC6QljAeaRbNxg==", + "license": "MIT", + "dependencies": { + "@start-dev/bundle": "^1.0.0", + "@start-dev/dark-mode-selector": "^1.0.0", + "@start-dev/find-package-locations": "^1.0.0", + "@start-dev/get-package-exports": "^1.0.0", + "@start-dev/rewrite-imports": "^1.0.0", + "@start-dev/websocket-rpc": "^1.0.0", + "@types/find-cache-dir": "^3.2.0", + "beamwind": "^2.0.2", + "chalk": "^4.1.0", + "find-cache-dir": "^3.3.1", + "mime": "^2.4.6", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/dark-mode-selector": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/dark-mode-selector/-/dark-mode-selector-1.0.0.tgz", + "integrity": "sha512-/cAzS9zAW9Kqd7g2CiIAqDcDY05uuf/B9z1rousCf/8QqkTYaMjTHU8BQnox10RkdA8+yypGSzRtiKWjIomU/Q==", + "license": "MIT", + "dependencies": { + "@start-dev/state": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "beamwind": "*", + "react": "*" + } + }, + "node_modules/@start-dev/find-package-locations": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/find-package-locations/-/find-package-locations-1.0.0.tgz", + "integrity": "sha512-FWmYE6MlacNdXq7paibQchBCX9nb5nHMyiJpYMqmWV4u27hgVHT2lKnaevRWSFOyG/8EN6BA45h4iH2TalkXWw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/get-package-exports": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/get-package-exports/-/get-package-exports-1.0.0.tgz", + "integrity": "sha512-WkQn9kapJJO1LCHuRvgOuEfLrDg1kpAd3t5dsl2uNhTmw6u6u+ct3lh07/QiW3138S7a0oLXKWxtv9HANN5RXQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/rewrite-imports": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/rewrite-imports/-/rewrite-imports-1.0.0.tgz", + "integrity": "sha512-+9cIugsJQVx9HiGXPaWWhvXmnhVNNWUou2XAsjuV8hTXlGMjzIBmHDIOCd/qwB5Zrc0INK75tC+yVjwdxolkmg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/state/-/state-1.0.0.tgz", + "integrity": "sha512-zjnJzc/k+7/AzCiUe3EeuQn7PFTXVGSDTAZgiN71/dQ03HOyZyUfpM2Mi9xM4UosiK8tn7ToEnthSfus0THryg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@start-dev/websocket-rpc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/websocket-rpc/-/websocket-rpc-1.0.0.tgz", + "integrity": "sha512-NtGSzMAE4/cFmLIEkSc0Dsd4nGE8Cd+7Xmjp7gAH1uoUE2xkaS9GKgnkPpNkWSteSSX7yAb63ExhOmQ7PGVmxg==", + "license": "MIT", + "dependencies": { + "sucrase": "^3.16.0", + "ws": "^7.4.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -35,6 +293,18 @@ "@types/ms": "*" } }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "license": "MIT" + }, + "node_modules/@types/find-cache-dir": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", + "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -50,6 +320,15 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/validator": { "version": "13.15.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", @@ -69,6 +348,63 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -87,7 +423,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/basic-auth": { @@ -122,6 +457,17 @@ "node": ">= 18" } }, + "node_modules/beamwind": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/beamwind/-/beamwind-2.0.2.tgz", + "integrity": "sha512-XyXjZDz4krw1czYr6UA9igxVEg8T6WmL631fTQ/aXKUrcnRQe8QZ8Lhr3OJCilIKzAAYxEhtPe9R84V3+MXTEw==", + "license": "MIT", + "dependencies": { + "@beamwind/core": "^2.0.2", + "@beamwind/preflight": "^2.0.0", + "@beamwind/preset-tailwind": "^2.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -159,7 +505,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -185,6 +530,18 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -223,6 +580,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -248,11 +642,43 @@ "fsevents": "~2.3.2" } }, + "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/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -326,6 +752,20 @@ "node": ">= 0.10" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -343,6 +783,15 @@ } } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -384,6 +833,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -399,6 +854,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -444,6 +905,12 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -525,29 +992,80 @@ "node": ">= 0.8" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/fsevents": { - "version": "2.3.3", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -604,6 +1122,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -707,6 +1246,17 @@ ], "license": "MIT" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -735,6 +1285,21 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -745,6 +1310,15 @@ "node": ">=0.10.0" } }, + "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-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -758,6 +1332,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -774,6 +1354,36 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -817,6 +1427,24 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -865,6 +1493,45 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -895,6 +1562,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -920,7 +1599,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -929,6 +1607,15 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -999,6 +1686,17 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1118,6 +1816,48 @@ "wrappy": "1" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1127,6 +1867,55 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -1229,7 +2018,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -1238,6 +2026,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -1336,6 +2145,16 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1349,12 +2168,105 @@ "node": ">=8.10.0" } }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/retry-as-promised": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", "license": "MIT" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-inject/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "license": "MIT" + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1523,6 +2435,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1595,6 +2528,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1608,6 +2553,13 @@ "node": ">=10" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "license": "MIT" + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1617,6 +2569,21 @@ "node": ">= 10.x" } }, + "node_modules/start-dev": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/start-dev/-/start-dev-1.0.0.tgz", + "integrity": "sha512-AgSsh5mHOrweIHzIfqu6os5V0BoqA5R28v/J8zZBUZlD/GAB6uyWKkTnStvSOyfoqt8SDXQo6J6QyEl3GMjE9g==", + "license": "MIT", + "dependencies": { + "@start-dev/core": "^1.0.0" + }, + "bin": { + "start-dev": "lib/index.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1626,6 +2593,168 @@ "node": ">= 0.8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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/string-width-cjs/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/string-width-cjs/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/string-width-cjs/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/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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/strip-ansi-cjs/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/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1639,6 +2768,45 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-css-prefixer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/tiny-css-prefixer/-/tiny-css-prefixer-1.1.4.tgz", + "integrity": "sha512-QLnDGLUmMkNlSuWogBaSu92uQnvWhcQwUc+MG8L4YgBkjSmBkAz29pTq+8Rh3YWR1TwPrjuZDKWzO7v4PXkipQ==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1677,6 +2845,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1740,6 +2914,38 @@ "node": ">= 0.8" } }, + "node_modules/vm2": { + "version": "3.9.19", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz", + "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==", + "deprecated": "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.", + "license": "MIT", + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/win-node-env": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/win-node-env/-/win-node-env-0.6.1.tgz", @@ -1767,12 +2973,121 @@ "@types/node": "*" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?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": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 7e0a0af..02f3514 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "start-dev": "^1.0.0" }, "devDependencies": { "nodemon": "^3.1.10" From b8a77f95fa9c76126ae00906254bf718e7cf780f Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 14 Jul 2025 16:12:04 -0400 Subject: [PATCH 14/70] Poll Api routes --- api/polls.js | 2 +- app.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/api/polls.js b/api/polls.js index c193792..449b653 100644 --- a/api/polls.js +++ b/api/polls.js @@ -31,7 +31,7 @@ router.get("/:id", async (req, res) => { // include: Ballot, // include: BallotRanking, }); - res.status(200).send(Poll); + res.status(200).send(poll); if (!poll) { return res.status(404).json({ error: "Poll not found" }); } diff --git a/app.js b/app.js index 99d4847..791e510 100644 --- a/app.js +++ b/app.js @@ -41,11 +41,7 @@ const runApp = async () => { try { // we have to keep it true, so that schema always matches the code before we happy with it await db.sync(); -<<<<<<< HEAD -og("✅ Connected to the database"); -======= console.log("✅ Connected to the database"); ->>>>>>> 142785f (Api Routes for polls) app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); }); From c4c446df5a8ad7fd9534aa796d40fb45ce780d2d Mon Sep 17 00:00:00 2001 From: tabularasae Date: Tue, 15 Jul 2025 12:27:37 -0400 Subject: [PATCH 15/70] Defined the API routes for the poll options --- api/index.js | 4 +++- api/pollOptions.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 api/pollOptions.js diff --git a/api/index.js b/api/index.js index 797f688..1828319 100644 --- a/api/index.js +++ b/api/index.js @@ -3,8 +3,10 @@ const router = express.Router(); const usersRouter = require("./users"); const pollsRouter = require("./polls"); +const pollOptionsRouter = require("./pollOptions"); router.use("/users", usersRouter); -router.use("/polls", pollsRouter); +router.use("/polls", pollsRouter); +router.use("/pollOptions", pollOptionsRouter); module.exports = router; \ No newline at end of file diff --git a/api/pollOptions.js b/api/pollOptions.js new file mode 100644 index 0000000..5059e64 --- /dev/null +++ b/api/pollOptions.js @@ -0,0 +1,56 @@ +const express = require("express"); +const router = express.Router(); +const { PollOption } = require("../database"); + +router.get("/", async (req, res) => { + try { + const options = await PollOption.findAll(); + console.log(`Found ${options.length} poll options`); + res.status(200).send(options); + } catch (error) { + console.error("Error fetching poll options:", error); + res.status(500).json({ + error: "Failed to fetch poll options", + message: "Check your database connection", + }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const option = await PollOption.findByPk(req.params.id); + if (!option) { + return res.status(404).json({ error: "Poll option not found" }); + } + res.status(200).send(option); + } catch (error) { + console.error("Error fetching poll option:", error); + res.status(500).json({ error: "Failed to fetch poll option by id" }); + } +}); + +router.post("/", async (req, res) => { + try { + const option = await PollOption.create(req.body); + res.status(201).send(option); + } catch (error) { + console.error("Error creating poll option:", error); + res.status(500).json({ error: "Failed to create poll option" }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const option = await PollOption.findByPk(req.params.id); + if (!option) { + return res.status(404).json({ error: "Poll option not found" }); + } + await option.destroy(); + res.status(200).json({ message: "Poll option deleted successfully" }); + } catch (error) { + console.error("Error deleting poll option:", error); + res.status(500).json({ error: "Failed to delete poll option" }); + } +}); + +module.exports = router; \ No newline at end of file From 5f5f7b4c7cc5d0fb2c93154e6be703e4a4eb1b50 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Tue, 15 Jul 2025 14:50:13 -0400 Subject: [PATCH 16/70] added the eager loading to the polls api routes --- api/polls.js | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/api/polls.js b/api/polls.js index 449b653..c6e36ca 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,16 +1,12 @@ const express = require("express"); const router = express.Router(); -const {Poll} = require("../database"); +const {Poll, User, PollOption, Ballot} = require("../database"); router.get("/", async (req, res) => { try { - const polls = await Poll.findAll( - // // not yet - // include: User, - // include: PollOption, - // include: Ballot, - // include: BallotRanking, - ); + const polls = await Poll.findAll({ + include: PollOption, + }); console.log(`Found ${polls.length} polls`); res.status(200).send(polls); } catch (error) { @@ -25,11 +21,7 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const poll = await Poll.findByPk(req.params.id, { - // // not yet - // include: User, - // include: PollOption, - // include: Ballot, - // include: BallotRanking, + include: PollOption, }); res.status(200).send(poll); if (!poll) { @@ -42,7 +34,9 @@ router.get("/:id", async (req, res) => { router.delete("/:id", async (req, res) => { try { - const poll = await Poll.findByPk(req.params.id); + const poll = await Poll.findByPk(req.params.id, { + include: PollOption, + }); if (!poll) { return res.status(404).json({ error: "poll not found" }); } From 19748b093a66b86d958f4504e7aee77a607edb44 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Tue, 15 Jul 2025 14:54:35 -0400 Subject: [PATCH 17/70] Created seed file --- database/seed.js | 115 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 6 deletions(-) diff --git a/database/seed.js b/database/seed.js index e58b595..8978f97 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,23 +1,126 @@ const db = require("./db"); -const { User } = require("./index"); +const { User, Poll, PollOption } = require("./index"); const seed = async () => { try { db.logging = false; - await db.sync({ force: true }); // Drop and recreate tables + await db.sync({ force: true }); const users = await User.bulkCreate([ { username: "admin", passwordHash: User.hashPassword("admin123") }, { username: "user1", passwordHash: User.hashPassword("user111") }, { username: "user2", passwordHash: User.hashPassword("user222") }, + { username: "pollmaster", passwordHash: User.hashPassword("polls123") }, ]); console.log(`👤 Created ${users.length} users`); - // Create more seed data here once you've created your models - // Seed files are a great way to test your database schema! + const now = new Date(); + const polls = await Poll.bulkCreate([ + { + title: "What's your favorite programming language?", + description: "Let us know what your fav language is", + creator_id: users[0].id, + status: "published", + endAt: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000), + allowAnonymous: true, + publishedAt: now, + }, + { + title: "Best time for team meetings?", + description: "Lets find a time that works for everyone", + creator_id: users[1].id, + status: "published", + endAt: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000), + allowAnonymous: false, + publishedAt: now, + }, + { + title: "Weekend activity preferences", + description: "What should we do this weekend?", + creator_id: users[2].id, + status: "published", + endAt: new Date(now.getTime() + 2 * 60 * 60 * 1000), + allowAnonymous: true, + publishedAt: now, + }, + { + title: "Office lunch options", + description: "Vote for next weeks catered lunch", + creator_id: users[3].id, + status: "published", + endAt: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000), + allowAnonymous: false, + publishedAt: now, + }, + { + title: "Project deadline preference", + description: "When should we target the release?", + creator_id: users[0].id, + status: "published", + endAt: new Date(now.getTime() + 24 * 60 * 60 * 1000), + allowAnonymous: false, + publishedAt: now, + }, + { + title: "Ended Poll Example", + description: "This poll has already ended", + creator_id: users[1].id, + status: "closed", + endAt: new Date(now.getTime() - 24 * 60 * 60 * 1000), + allowAnonymous: true, + publishedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000), + }, + ]); + + console.log(`📊 Created ${polls.length} polls`); + + const pollOptions = await PollOption.bulkCreate([ + // Options for "What's your favorite programming language?" + { text: "JavaScript", position: 1, poll_id: polls[0].id }, + { text: "Python", position: 2, poll_id: polls[0].id }, + { text: "Java", position: 3, poll_id: polls[0].id }, + { text: "C++", position: 4, poll_id: polls[0].id }, + + // Options for "Best time for team meetings?" + { text: "9:00 AM", position: 1, poll_id: polls[1].id }, + { text: "1:00 PM", position: 2, poll_id: polls[1].id }, + { text: "3:00 PM", position: 3, poll_id: polls[1].id }, + + // Options for "Weekend activity preferences" + { text: "Hiking", position: 1, poll_id: polls[2].id }, + { text: "Movie night", position: 2, poll_id: polls[2].id }, + { text: "Board games", position: 3, poll_id: polls[2].id }, + { text: "Cooking together", position: 4, poll_id: polls[2].id }, + + // Options for "Office lunch options" + { text: "Pizza", position: 1, poll_id: polls[3].id }, + { text: "Chinese food", position: 2, poll_id: polls[3].id }, + { text: "Sandwiches", position: 3, poll_id: polls[3].id }, + { text: "Mexican food", position: 4, poll_id: polls[3].id }, + + // Options for "Project deadline preference" + { text: "End of this month", position: 1, poll_id: polls[4].id }, + { text: "Next month", position: 2, poll_id: polls[4].id }, + { text: "In 6 weeks", position: 3, poll_id: polls[4].id }, + + // Options for "Ended Poll Example" + { text: "Option A", position: 1, poll_id: polls[5].id }, + { text: "Option B", position: 2, poll_id: polls[5].id }, + { text: "Option C", position: 3, poll_id: polls[5].id }, + ]); + + console.log(`📝 Created ${pollOptions.length} poll options`); + + console.log("🌱 Seeded the database successfully!"); + console.log("\n📊 Sample polls created:"); + polls.forEach((poll, index) => { + const timeLeft = poll.endAt > now ? + `${Math.ceil((poll.endAt - now) / (1000 * 60 * 60 * 24))} days left` : + 'Ended'; + console.log(` ${index + 1}. "${poll.title}" - ${timeLeft}`); + }); - console.log("🌱 Seeded the database"); } catch (error) { console.error("Error seeding database:", error); if (error.message.includes("does not exist")) { @@ -27,4 +130,4 @@ const seed = async () => { db.close(); }; -seed(); +seed(); \ No newline at end of file From 87baf5ce46321eb9f6b3fe6ec66e150fb2b3a570 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Tue, 15 Jul 2025 14:57:41 -0400 Subject: [PATCH 18/70] added the eager loading to the users api routes --- api/users.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/api/users.js b/api/users.js index 666c34a..eb66535 100644 --- a/api/users.js +++ b/api/users.js @@ -1,23 +1,24 @@ const express = require("express"); const router = express.Router(); -const { User } = require("../database"); +const { User, Poll } = require("../database"); //get all users -router.get ("/", async (req,res) => { - try{ - const users = await User.findAll(); - res.status(200).send(users); - }catch(error){ - console.error("Error fetching users: ", error); - res.status(500).send("Error Fetching Students"); - } +router.get("/", async (req, res) => { + try { + const users = await User.findAll({ include: Poll }); + res.status(200).send(users); + } catch (error) { + console.error("Error fetching users: ", error); + res.status(500).send("Error Fetching Students"); + } }); //get a user by id router.get("/:id", async (req, res) => { try { - const user = await User.findByPk(req.params.id); - + const user = await User.findByPk(req.params.id, { + include: Poll, + }); if (!user) { return res.status(404).send("User not found"); } @@ -28,15 +29,16 @@ router.get("/:id", async (req, res) => { } }); + + //create a user router.post("/", async (req, res) => { try { - const { username, email, password } = req.body; const passwordHash = User.hashPassword(password); - const newUser = {username, email, passwordHash}; + const newUser = { username, email, passwordHash }; const savedUser = await User.create(newUser); @@ -47,4 +49,4 @@ router.post("/", async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; From 1963f6543a7b4c3c51c417cc1815ce17db527304 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Tue, 15 Jul 2025 15:07:37 -0400 Subject: [PATCH 19/70] Added a delete method to Users API --- api/users.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/api/users.js b/api/users.js index eb66535..950b20f 100644 --- a/api/users.js +++ b/api/users.js @@ -9,7 +9,7 @@ router.get("/", async (req, res) => { res.status(200).send(users); } catch (error) { console.error("Error fetching users: ", error); - res.status(500).send("Error Fetching Students"); + res.status(500).send("Error fetching users"); } }); @@ -29,7 +29,18 @@ router.get("/:id", async (req, res) => { } }); - +router.delete("/:id", async (req, res) => { + try { + const user = await User.findByPk(req.params.id); + if (!user) { + return res.status(404).json({ error: "user not found" }); + } + await user.destroy(); + res.status(200).json({ message: "User deleted successfully" }); + } catch (error) { + res.status(500).json({ error: "Failed to delete a user" }); + } +}); //create a user router.post("/", async (req, res) => { @@ -49,4 +60,4 @@ router.post("/", async (req, res) => { } }); -module.exports = router; +module.exports = router; \ No newline at end of file From 0b6a10852e9448ceb5a4afaf167e8c55225dcfcc Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 11:04:12 -0400 Subject: [PATCH 20/70] Added the the image member for user and poll entity --- database/poll.js | 9 +++++++++ database/user.js | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/database/poll.js b/database/poll.js index 7d1a20d..89e6b7c 100644 --- a/database/poll.js +++ b/database/poll.js @@ -8,6 +8,15 @@ const Poll = db.define( type: DataTypes.STRING, allowNull: false, }, + imageUrl: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: + "https://online.eou.edu/wp-content/uploads/2020/10/eou_article_psychology-competitiveness_header.jpg", + validate: { + isUrl: true, + }, + }, description: DataTypes.TEXT, allowAnonymous: { type: DataTypes.BOOLEAN, diff --git a/database/user.js b/database/user.js index 755c757..2653f8e 100644 --- a/database/user.js +++ b/database/user.js @@ -11,6 +11,14 @@ const User = db.define("user", { len: [3, 20], }, }, + imageUrl: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "https://cdn-icons-png.flaticon.com/512/2528/2528787.png", + validate: { + isUrl: true, + }, + }, email: { type: DataTypes.STRING, allowNull: true, From 31d2f7a6b326f4e1f76f64631fe12abf8049db86 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 11:08:37 -0400 Subject: [PATCH 21/70] Added the ballot eager loading to User api routes --- api/users.js | 6 +++--- app.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/users.js b/api/users.js index 950b20f..d9c3f15 100644 --- a/api/users.js +++ b/api/users.js @@ -1,11 +1,11 @@ const express = require("express"); const router = express.Router(); -const { User, Poll } = require("../database"); +const { User, Poll, Ballot } = require("../database"); //get all users router.get("/", async (req, res) => { try { - const users = await User.findAll({ include: Poll }); + const users = await User.findAll({ include: [Poll, Ballot] }); res.status(200).send(users); } catch (error) { console.error("Error fetching users: ", error); @@ -17,7 +17,7 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const user = await User.findByPk(req.params.id, { - include: Poll, + include: [Poll, Ballot] }); if (!user) { return res.status(404).send("User not found"); diff --git a/app.js b/app.js index 791e510..76dde7b 100644 --- a/app.js +++ b/app.js @@ -52,4 +52,4 @@ const runApp = async () => { runApp(); -module.exports = app; +module.exports = app; \ No newline at end of file From 0ec6b137320abb52e8e50ebf62ec0c3adf967a9d Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 11:11:01 -0400 Subject: [PATCH 22/70] Added the eager loading to the poll api routes --- api/polls.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/polls.js b/api/polls.js index c6e36ca..51763b0 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,11 +1,11 @@ const express = require("express"); const router = express.Router(); -const {Poll, User, PollOption, Ballot} = require("../database"); +const {Poll, User, PollOption, Ballot, BallotRanking} = require("../database"); router.get("/", async (req, res) => { try { const polls = await Poll.findAll({ - include: PollOption, + include: [PollOption, Ballot, BallotRanking] }); console.log(`Found ${polls.length} polls`); res.status(200).send(polls); @@ -21,7 +21,7 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const poll = await Poll.findByPk(req.params.id, { - include: PollOption, + include: [PollOption, Ballot, BallotRanking] }); res.status(200).send(poll); if (!poll) { @@ -35,7 +35,7 @@ router.get("/:id", async (req, res) => { router.delete("/:id", async (req, res) => { try { const poll = await Poll.findByPk(req.params.id, { - include: PollOption, + include: [PollOption, Ballot, BallotRanking] }); if (!poll) { return res.status(404).json({ error: "poll not found" }); From 8f4aac39d2fa396fe1484c1b055a704c76409914 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 11:18:35 -0400 Subject: [PATCH 23/70] get API route for All ballots --- api/ballots.js | 21 +++++++++++++++++++++ api/index.js | 3 +++ 2 files changed, 24 insertions(+) create mode 100644 api/ballots.js diff --git a/api/ballots.js b/api/ballots.js new file mode 100644 index 0000000..edb86c9 --- /dev/null +++ b/api/ballots.js @@ -0,0 +1,21 @@ +const express = require("express"); +const router = express.Router(); +const {Poll, User, PollOption, Ballot, BallotRanking} = require("../database"); + +router.get("/", async (req, res) => { + try { + const ballots = await Ballot.findAll( + // {include: [Poll, User, PollOption, BallotRanking]} + ); + console.log(`Found ${ballots.length} ballots`); + res.status(200).send(ballots); + } catch (error) { + console.error("Error fetching ballots:", error); + res.status(500).json({ + error: "Failed to fetch ballots", + message: "Check your database connection", + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/index.js b/api/index.js index 1828319..5854508 100644 --- a/api/index.js +++ b/api/index.js @@ -4,9 +4,12 @@ const router = express.Router(); const usersRouter = require("./users"); const pollsRouter = require("./polls"); const pollOptionsRouter = require("./pollOptions"); +const ballotRouter = require("./ballots") router.use("/users", usersRouter); router.use("/polls", pollsRouter); router.use("/pollOptions", pollOptionsRouter); +router.use("/ballots", ballotRouter); + module.exports = router; \ No newline at end of file From c5063b39664f6e6289637ca35a8d3189a05152b5 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 11:29:02 -0400 Subject: [PATCH 24/70] API routes for ballots --- api/ballots.js | 67 +++++++++++++++++++++++++++++++++++++++++++++----- api/polls.js | 18 +++++++++----- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/api/ballots.js b/api/ballots.js index edb86c9..f381908 100644 --- a/api/ballots.js +++ b/api/ballots.js @@ -1,14 +1,18 @@ const express = require("express"); const router = express.Router(); -const {Poll, User, PollOption, Ballot, BallotRanking} = require("../database"); +const { + Poll, + User, + PollOption, + Ballot, + BallotRanking, +} = require("../database"); router.get("/", async (req, res) => { try { - const ballots = await Ballot.findAll( - // {include: [Poll, User, PollOption, BallotRanking]} - ); + const ballots = await Ballot.findAll({ include: BallotRanking }); console.log(`Found ${ballots.length} ballots`); - res.status(200).send(ballots); + res.status(200).json(ballots); } catch (error) { console.error("Error fetching ballots:", error); res.status(500).json({ @@ -18,4 +22,55 @@ router.get("/", async (req, res) => { } }); -module.exports = router; \ No newline at end of file +router.get("/:id", async (req, res) => { + try { + const ballot = await Ballot.findByPk(req.params.id, { + include: [BallotRanking], + }); + if (!ballot) { + return res.status(404).json({ error: "Ballot not found" }); + } + res.status(200).json(ballot); + } catch (error) { + console.error(`Error fetching ballot`, error); + res.status(500).json({ + error: "Failed to fetch ballot", + message: "Check your database connection", + }); + } +}); + +router.post("/", async (req, res) => { + try { + const newBallot = await Ballot.create(req.body); + console.log(`Created ballot ${newBallot.id}`); + res.status(201).send(newBallot); + } catch (error) { + console.error("Error creating ballot:", error); + res.status(400).json({ + error: "Failed to create ballot", + message: error.message, + }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const numDeleted = await Ballot.destroy({ + where: { id: req.params.id }, + }); + if (numDeleted === 0) { + return res.status(404).json({ error: "Ballot not found" }); + } + console.log(`Deleted ballot ${req.params.id}`); + res.sendStatus(204); + } catch (error) { + console.error(`Error deleting ballot ${req.params.id}:`, error); + res.status(500).json({ + error: "Failed to delete ballot", + message: "Check your database connection", + }); + } +}); + +module.exports = router; diff --git a/api/polls.js b/api/polls.js index 51763b0..e0b9c09 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,11 +1,17 @@ const express = require("express"); const router = express.Router(); -const {Poll, User, PollOption, Ballot, BallotRanking} = require("../database"); +const { + Poll, + User, + PollOption, + Ballot, + BallotRanking, +} = require("../database"); router.get("/", async (req, res) => { try { - const polls = await Poll.findAll({ - include: [PollOption, Ballot, BallotRanking] + const polls = await Poll.findAll({ + include: [PollOption, Ballot, BallotRanking], }); console.log(`Found ${polls.length} polls`); res.status(200).send(polls); @@ -21,7 +27,7 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const poll = await Poll.findByPk(req.params.id, { - include: [PollOption, Ballot, BallotRanking] + include: [PollOption, Ballot, BallotRanking], }); res.status(200).send(poll); if (!poll) { @@ -35,7 +41,7 @@ router.get("/:id", async (req, res) => { router.delete("/:id", async (req, res) => { try { const poll = await Poll.findByPk(req.params.id, { - include: [PollOption, Ballot, BallotRanking] + include: [PollOption, Ballot, BallotRanking], }); if (!poll) { return res.status(404).json({ error: "poll not found" }); @@ -56,4 +62,4 @@ router.post("/", async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; From a162c42c070c0cff416a2b939eafc2073eef044d Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 11:36:16 -0400 Subject: [PATCH 25/70] Revorked the post method to store the votes --- api/ballots.js | 54 ++++++++++++++++++++++++++++++-------------------- api/users.js | 4 ++-- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/api/ballots.js b/api/ballots.js index f381908..6a5f45d 100644 --- a/api/ballots.js +++ b/api/ballots.js @@ -41,34 +41,44 @@ router.get("/:id", async (req, res) => { }); router.post("/", async (req, res) => { - try { - const newBallot = await Ballot.create(req.body); - console.log(`Created ballot ${newBallot.id}`); - res.status(201).send(newBallot); - } catch (error) { - console.error("Error creating ballot:", error); - res.status(400).json({ - error: "Failed to create ballot", - message: error.message, - }); + const { userId, pollId, rankings } = req.body; + + if (!userId || !pollId || !Array.isArray(rankings) || rankings.length === 0) { + return res + .status(400) + .json({ error: "userId, pollId and rankings[] are required" }); } -}); -router.delete("/:id", async (req, res) => { + const t = await sequelize.transaction(); try { - const numDeleted = await Ballot.destroy({ - where: { id: req.params.id }, + const ballot = await Ballot.create( + { user_id: userId, poll_id: pollId }, + { transaction: t } + ); + + const createRankings = rankings.map(({ pollOptionId, rank }) => + BallotRanking.create( + { + ballot_id: ballot.id, + poll_option_id: pollOptionId, + rank, + }, + { transaction: t } + ) + ); + await Promise.all(createRankings); + + await t.commit(); + const result = await Ballot.findByPk(ballot.id, { + include: [{ model: BallotRanking }], }); - if (numDeleted === 0) { - return res.status(404).json({ error: "Ballot not found" }); - } - console.log(`Deleted ballot ${req.params.id}`); - res.sendStatus(204); + res.status(201).json(result); } catch (error) { - console.error(`Error deleting ballot ${req.params.id}:`, error); + await t.rollback(); + console.error("Error creating ballot + rankings:", error); res.status(500).json({ - error: "Failed to delete ballot", - message: "Check your database connection", + error: "Failed to submit votes", + message: error.message, }); } }); diff --git a/api/users.js b/api/users.js index d9c3f15..329e574 100644 --- a/api/users.js +++ b/api/users.js @@ -17,7 +17,7 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const user = await User.findByPk(req.params.id, { - include: [Poll, Ballot] + include: [Poll, Ballot], }); if (!user) { return res.status(404).send("User not found"); @@ -60,4 +60,4 @@ router.post("/", async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; From 3afe099d3aebd487ba18b12a65637e3fd52b3210 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 12:05:17 -0400 Subject: [PATCH 26/70] api routes hotfix --- api/ballots.js | 1 + api/index.js | 2 +- api/polls.js | 12 +++--------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/api/ballots.js b/api/ballots.js index 6a5f45d..c50f558 100644 --- a/api/ballots.js +++ b/api/ballots.js @@ -6,6 +6,7 @@ const { PollOption, Ballot, BallotRanking, + sequelize } = require("../database"); router.get("/", async (req, res) => { diff --git a/api/index.js b/api/index.js index 5854508..18de138 100644 --- a/api/index.js +++ b/api/index.js @@ -4,7 +4,7 @@ const router = express.Router(); const usersRouter = require("./users"); const pollsRouter = require("./polls"); const pollOptionsRouter = require("./pollOptions"); -const ballotRouter = require("./ballots") +const ballotRouter = require("./ballots"); router.use("/users", usersRouter); router.use("/polls", pollsRouter); diff --git a/api/polls.js b/api/polls.js index e0b9c09..b24486c 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,17 +1,11 @@ const express = require("express"); const router = express.Router(); -const { - Poll, - User, - PollOption, - Ballot, - BallotRanking, -} = require("../database"); +const { Poll, PollOption, Ballot, BallotRanking } = require("../database"); router.get("/", async (req, res) => { try { const polls = await Poll.findAll({ - include: [PollOption, Ballot, BallotRanking], + include: [PollOption, Ballot], }); console.log(`Found ${polls.length} polls`); res.status(200).send(polls); @@ -27,7 +21,7 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const poll = await Poll.findByPk(req.params.id, { - include: [PollOption, Ballot, BallotRanking], + include: [PollOption, Ballot], }); res.status(200).send(poll); if (!poll) { From ad8d42d7165b94bd5a291eb5199d83205527399b Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 14:16:16 -0400 Subject: [PATCH 27/70] Post method for the ballot api --- api/ballots.js | 71 +++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/api/ballots.js b/api/ballots.js index c50f558..6f54486 100644 --- a/api/ballots.js +++ b/api/ballots.js @@ -6,12 +6,14 @@ const { PollOption, Ballot, BallotRanking, - sequelize + db, } = require("../database"); router.get("/", async (req, res) => { try { - const ballots = await Ballot.findAll({ include: BallotRanking }); + const ballots = await Ballot.findAll({ + include: [{ model: BallotRanking }], + }); console.log(`Found ${ballots.length} ballots`); res.status(200).json(ballots); } catch (error) { @@ -26,7 +28,7 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const ballot = await Ballot.findByPk(req.params.id, { - include: [BallotRanking], + include: [{ model: BallotRanking }], }); if (!ballot) { return res.status(404).json({ error: "Ballot not found" }); @@ -42,44 +44,55 @@ router.get("/:id", async (req, res) => { }); router.post("/", async (req, res) => { - const { userId, pollId, rankings } = req.body; + const { userId = null, pollId, rankings } = req.body; + + if (!pollId) return res.status(400).json({ error: "pollId is required" }); - if (!userId || !pollId || !Array.isArray(rankings) || rankings.length === 0) { + if (!Array.isArray(rankings) || rankings.length === 0) return res .status(400) - .json({ error: "userId, pollId and rankings[] are required" }); + .json({ error: "rankings must be a non-empty array" }); + + for (const r of rankings) { + if (typeof r !== "object" || r.rank == null || r.pollOptionId == null) { + return res.status(400).json({ + error: "Each ranking must be an object { pollOptionId, rank }", + }); + } } - const t = await sequelize.transaction(); + let tx; try { + tx = await db.transaction(); + const ballot = await Ballot.create( - { user_id: userId, poll_id: pollId }, - { transaction: t } + { + user_id: userId, + poll_id: pollId, + }, + { transaction: tx } ); - const createRankings = rankings.map(({ pollOptionId, rank }) => - BallotRanking.create( - { - ballot_id: ballot.id, - poll_option_id: pollOptionId, - rank, - }, - { transaction: t } - ) - ); - await Promise.all(createRankings); + const rankingRows = rankings.map(({ pollOptionId, rank }) => ({ + ballot_id: ballot.id, + option_id: pollOptionId, + rank, + })); + await BallotRanking.bulkCreate(rankingRows, { transaction: tx }); + + await tx.commit(); - await t.commit(); const result = await Ballot.findByPk(ballot.id, { - include: [{ model: BallotRanking }], + include: [{ model: BallotRanking, order: [["rank", "ASC"]] }], }); - res.status(201).json(result); - } catch (error) { - await t.rollback(); - console.error("Error creating ballot + rankings:", error); - res.status(500).json({ - error: "Failed to submit votes", - message: error.message, + + return res.status(201).json(result); + } catch (err) { + if (tx) await tx.rollback(); + console.error("Failed to create ballot:", err); + return res.status(500).json({ + error: "Failed to submit ballot", + message: err.message, }); } }); From f88994baa56b425fd8bda340e88b836deb0b7d82 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Wed, 16 Jul 2025 15:38:30 -0400 Subject: [PATCH 28/70] pathc route for polls --- api/polls.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/api/polls.js b/api/polls.js index b24486c..b1df7f7 100644 --- a/api/polls.js +++ b/api/polls.js @@ -56,4 +56,21 @@ router.post("/", async (req, res) => { } }); +router.patch("/:id", async (req,res) => { + try{ + consst [updatedRows] = await Poll.update(req.body, { + where: {id: req.params.id}, + }); + if(updatedRows === 0){ + return res.status(404).send("Student not found"); + } + const updatedPoll = await Poll.findByPk(req.params.id); + res.status(200).send(updatedPoll); + + }catch(error){ + console.error("Error updating poll:", error); + res.status(500).send("Error updating poll"); + } +}) + module.exports = router; From 1106af39e63d45d37e99947e86cf7a4b7bf8b7ed Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Wed, 16 Jul 2025 15:42:22 -0400 Subject: [PATCH 29/70] Updated patch route --- api/polls.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/api/polls.js b/api/polls.js index b1df7f7..82fcdf1 100644 --- a/api/polls.js +++ b/api/polls.js @@ -56,21 +56,20 @@ router.post("/", async (req, res) => { } }); -router.patch("/:id", async (req,res) => { - try{ - consst [updatedRows] = await Poll.update(req.body, { - where: {id: req.params.id}, +router.patch("/:id", async (req, res) => { + try { + const [updatedRows] = await Poll.update(req.body, { + where: { id: req.params.id }, }); - if(updatedRows === 0){ - return res.status(404).send("Student not found"); + if (updatedRows === 0) { + return res.status(404).send("Poll not found"); } const updatedPoll = await Poll.findByPk(req.params.id); res.status(200).send(updatedPoll); - - }catch(error){ + } catch (error) { console.error("Error updating poll:", error); res.status(500).send("Error updating poll"); } -}) +}); module.exports = router; From 1036ffbf208a00d82b0952006f3a582658825458 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 22:05:28 -0400 Subject: [PATCH 30/70] Added result related models --- api/pollResults.js | 0 database/index.js | 25 +++++++++++++++++++------ database/pollResult.js | 18 ++++++++++++++++++ database/pollResultValue.js | 15 +++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 api/pollResults.js create mode 100644 database/pollResult.js create mode 100644 database/pollResultValue.js diff --git a/api/pollResults.js b/api/pollResults.js new file mode 100644 index 0000000..e69de29 diff --git a/database/index.js b/database/index.js index ad9e089..a734c72 100644 --- a/database/index.js +++ b/database/index.js @@ -1,10 +1,12 @@ const db = require("./db"); -const User = require("./user"); -const Poll = require("./poll"); -const PollOption = require("./pollOption"); -const Ballot = require("./ballot"); -const BallotRanking = require("./ballotRanking"); +const User = require("./user"); +const Poll = require("./poll"); +const PollOption = require("./pollOption"); +const Ballot = require("./ballot"); +const BallotRanking = require("./ballotRanking"); const PollAllowedUser = require("./pollAllowedUser"); +const PollResultValue = require("./pollResultValue"); +const PollResult = require("./pollResult"); User.hasMany(Poll, { foreignKey: "creator_id" }); Poll.belongsTo(User, { as: "creator", foreignKey: "creator_id" }); @@ -24,6 +26,15 @@ BallotRanking.belongsTo(Ballot, { foreignKey: "ballot_id" }); PollOption.hasMany(BallotRanking, { foreignKey: "option_id" }); BallotRanking.belongsTo(PollOption, { foreignKey: "option_id" }); +Poll.hasOne(PollResult, { foreignKey: "poll_id", onDelete: "CASCADE" }); +PollResult.belongsTo(Poll, { foreignKey: "poll_id" }); + +PollOption.hasMany(PollResultValue, { foreignKey: "option_id" }); +PollResultValue.belongsTo(PollOption, { foreignKey: "option_id" }); + +PollResult.hasMany(PollResultValue, { foreignKey: "poll_result_id" }); +PollResultValue.belongsTo(PollResult, { foreignKey: "poll_result_id" }); + Poll.belongsToMany(User, { through: PollAllowedUser, as: "allowedUsers", @@ -43,4 +54,6 @@ module.exports = { Ballot, BallotRanking, PollAllowedUser, -}; \ No newline at end of file + PollResult, + PollResultValue, +}; diff --git a/database/pollResult.js b/database/pollResult.js new file mode 100644 index 0000000..451b29f --- /dev/null +++ b/database/pollResult.js @@ -0,0 +1,18 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollResult = db.define( + "pollResult", + { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + totalBallots: { type: DataTypes.INTEGER, allowNull: false }, + completedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { underscored: true } +); + +module.exports = PollResult; diff --git a/database/pollResultValue.js b/database/pollResultValue.js new file mode 100644 index 0000000..f25d0ad --- /dev/null +++ b/database/pollResultValue.js @@ -0,0 +1,15 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollResultValue = db.define( + "pollResultValue", + { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + roundNumber: { type: DataTypes.INTEGER, allowNull: false }, + optionText: { type: DataTypes.STRING, allowNull: false }, + votes: { type: DataTypes.INTEGER, allowNull: false }, + }, + { underscored: true } +); + +module.exports = PollResultValue; From 9d4399f885cd9e4151ad8298ceb8be4bbf1d2ce2 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 22:14:38 -0400 Subject: [PATCH 31/70] Schema --- api/index.js | 1 - api/pollResults.js | 0 2 files changed, 1 deletion(-) delete mode 100644 api/pollResults.js diff --git a/api/index.js b/api/index.js index 18de138..527e9dc 100644 --- a/api/index.js +++ b/api/index.js @@ -11,5 +11,4 @@ router.use("/polls", pollsRouter); router.use("/pollOptions", pollOptionsRouter); router.use("/ballots", ballotRouter); - module.exports = router; \ No newline at end of file diff --git a/api/pollResults.js b/api/pollResults.js deleted file mode 100644 index e69de29..0000000 From 35b893ca76a27b1b0b4474b7dd991fbe0af4fea0 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 17 Jul 2025 11:03:18 -0400 Subject: [PATCH 32/70] added poll option editing --- api/polls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index 82fcdf1..378c702 100644 --- a/api/polls.js +++ b/api/polls.js @@ -59,7 +59,7 @@ router.post("/", async (req, res) => { router.patch("/:id", async (req, res) => { try { const [updatedRows] = await Poll.update(req.body, { - where: { id: req.params.id }, + include: [PollOption], }); if (updatedRows === 0) { return res.status(404).send("Poll not found"); From 843e89247e4245c60bc54cf03dde726b9cad45dc Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 17 Jul 2025 11:05:58 -0400 Subject: [PATCH 33/70] fix patch route --- api/polls.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/api/polls.js b/api/polls.js index 378c702..9cbabd6 100644 --- a/api/polls.js +++ b/api/polls.js @@ -56,20 +56,18 @@ router.post("/", async (req, res) => { } }); -router.patch("/:id", async (req, res) => { +router.get("/:id", async (req, res) => { try { - const [updatedRows] = await Poll.update(req.body, { - include: [PollOption], + const poll = await Poll.findByPk(req.params.id, { + include: [PollOption, Ballot], }); - if (updatedRows === 0) { - return res.status(404).send("Poll not found"); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); } - const updatedPoll = await Poll.findByPk(req.params.id); - res.status(200).send(updatedPoll); + res.status(200).send(poll); } catch (error) { - console.error("Error updating poll:", error); - res.status(500).send("Error updating poll"); + res.status(500).json({ error: "Failed to fetch poll by id" }); } -}); +}); module.exports = router; From 14a83f2051f85c08fbd775cdc6aa223a11fd964c Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 22:05:28 -0400 Subject: [PATCH 34/70] Added result related models --- api/pollResults.js | 0 database/index.js | 25 +++++++++++++++++++------ database/pollResult.js | 18 ++++++++++++++++++ database/pollResultValue.js | 15 +++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 api/pollResults.js create mode 100644 database/pollResult.js create mode 100644 database/pollResultValue.js diff --git a/api/pollResults.js b/api/pollResults.js new file mode 100644 index 0000000..e69de29 diff --git a/database/index.js b/database/index.js index ad9e089..a734c72 100644 --- a/database/index.js +++ b/database/index.js @@ -1,10 +1,12 @@ const db = require("./db"); -const User = require("./user"); -const Poll = require("./poll"); -const PollOption = require("./pollOption"); -const Ballot = require("./ballot"); -const BallotRanking = require("./ballotRanking"); +const User = require("./user"); +const Poll = require("./poll"); +const PollOption = require("./pollOption"); +const Ballot = require("./ballot"); +const BallotRanking = require("./ballotRanking"); const PollAllowedUser = require("./pollAllowedUser"); +const PollResultValue = require("./pollResultValue"); +const PollResult = require("./pollResult"); User.hasMany(Poll, { foreignKey: "creator_id" }); Poll.belongsTo(User, { as: "creator", foreignKey: "creator_id" }); @@ -24,6 +26,15 @@ BallotRanking.belongsTo(Ballot, { foreignKey: "ballot_id" }); PollOption.hasMany(BallotRanking, { foreignKey: "option_id" }); BallotRanking.belongsTo(PollOption, { foreignKey: "option_id" }); +Poll.hasOne(PollResult, { foreignKey: "poll_id", onDelete: "CASCADE" }); +PollResult.belongsTo(Poll, { foreignKey: "poll_id" }); + +PollOption.hasMany(PollResultValue, { foreignKey: "option_id" }); +PollResultValue.belongsTo(PollOption, { foreignKey: "option_id" }); + +PollResult.hasMany(PollResultValue, { foreignKey: "poll_result_id" }); +PollResultValue.belongsTo(PollResult, { foreignKey: "poll_result_id" }); + Poll.belongsToMany(User, { through: PollAllowedUser, as: "allowedUsers", @@ -43,4 +54,6 @@ module.exports = { Ballot, BallotRanking, PollAllowedUser, -}; \ No newline at end of file + PollResult, + PollResultValue, +}; diff --git a/database/pollResult.js b/database/pollResult.js new file mode 100644 index 0000000..451b29f --- /dev/null +++ b/database/pollResult.js @@ -0,0 +1,18 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollResult = db.define( + "pollResult", + { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + totalBallots: { type: DataTypes.INTEGER, allowNull: false }, + completedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { underscored: true } +); + +module.exports = PollResult; diff --git a/database/pollResultValue.js b/database/pollResultValue.js new file mode 100644 index 0000000..f25d0ad --- /dev/null +++ b/database/pollResultValue.js @@ -0,0 +1,15 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollResultValue = db.define( + "pollResultValue", + { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + roundNumber: { type: DataTypes.INTEGER, allowNull: false }, + optionText: { type: DataTypes.STRING, allowNull: false }, + votes: { type: DataTypes.INTEGER, allowNull: false }, + }, + { underscored: true } +); + +module.exports = PollResultValue; From 050f80a99baabcc5b5f4cabd2a6c24deaec68b4b Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 16 Jul 2025 22:14:38 -0400 Subject: [PATCH 35/70] Schema --- api/index.js | 1 - api/pollResults.js | 0 2 files changed, 1 deletion(-) delete mode 100644 api/pollResults.js diff --git a/api/index.js b/api/index.js index 18de138..527e9dc 100644 --- a/api/index.js +++ b/api/index.js @@ -11,5 +11,4 @@ router.use("/polls", pollsRouter); router.use("/pollOptions", pollOptionsRouter); router.use("/ballots", ballotRouter); - module.exports = router; \ No newline at end of file diff --git a/api/pollResults.js b/api/pollResults.js deleted file mode 100644 index e69de29..0000000 From e1340932b63a2c4b6b59a902539e507e073eaf31 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 17 Jul 2025 11:14:59 -0400 Subject: [PATCH 36/70] Fixed patch route for poll and poll options --- api/polls.js | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/api/polls.js b/api/polls.js index 9cbabd6..4b7209b 100644 --- a/api/polls.js +++ b/api/polls.js @@ -56,17 +56,43 @@ router.post("/", async (req, res) => { } }); -router.get("/:id", async (req, res) => { +router.patch("/:id", async (req, res) => { try { - const poll = await Poll.findByPk(req.params.id, { - include: [PollOption, Ballot], - }); - if (!poll) { - return res.status(404).json({ error: "Poll not found" }); + const { pollOptions, ...pollData } = req.body; + + const existingPoll = await Poll.findByPk(req.params.id); + if (!existingPoll) { + return res.status(404).send("Poll not found"); } - res.status(200).send(poll); + + if (Object.keys(pollData).length > 0) { + await Poll.update(pollData, { + where: { id: req.params.id }, + }); + } + + if (pollOptions && Array.isArray(pollOptions)) { + await PollOption.destroy({ + where: { poll_id: req.params.id } + }); + + const newOptions = pollOptions.map((option, index) => ({ + text: option.text, + position: option.position || index + 1, + poll_id: req.params.id + })); + + await PollOption.bulkCreate(newOptions); + } + + const updatedPoll = await Poll.findByPk(req.params.id, { + include: [PollOption] + }); + + res.status(200).send(updatedPoll); } catch (error) { - res.status(500).json({ error: "Failed to fetch poll by id" }); + console.error("Error updating poll:", error); + res.status(500).send("Error updating poll"); } }); From 8d3a78ca61b83a7060d0f231a84ef8db0ff1589d Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 17 Jul 2025 11:42:03 -0400 Subject: [PATCH 37/70] Made a patch route for users --- api/users.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/api/users.js b/api/users.js index 329e574..8d73fd7 100644 --- a/api/users.js +++ b/api/users.js @@ -60,4 +60,39 @@ router.post("/", async (req, res) => { } }); +router.patch("/:id", async(req,res) => { + try{ + const{ password, ...userData } = req.body; + + const existingUser= await User.findByPk(req.params.id); + if(!existingUser) { + return res.status(404).send("User not found"); + } + + const updateData = {...userData}; + + if(password){ + updateData.passwordHash = User.hashPassword(password); + } + + const [updatedRows] = await User.update(updateData, { + where: {id: req.params.id}, + }); + + if (updatedRows === 0){ + return res.status(400).send("No changes made to the user"); + } + + const updatedUser = await User.findByPk(req.params.id,{ + include:[Poll, Ballot], + attributes: {exclude: ["passwordHash"]} + }); + + res.status(200).send(updatedUser); + }catch (error){ + console.error("Error updating User: ", error); + res.status(500).send("Error User"); + } +}) + module.exports = router; From f6b9a68ff405c0cfb3061776fef3b013c4e25337 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Thu, 17 Jul 2025 14:24:52 -0400 Subject: [PATCH 38/70] added the graphic art (very important) --- api/ballots.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/ballots.js b/api/ballots.js index 6f54486..ad74145 100644 --- a/api/ballots.js +++ b/api/ballots.js @@ -9,6 +9,24 @@ const { db, } = require("../database"); + // _______ + // | | + // | ✔ | + // |_______| + // || + // \/ + // ______________________ + // | | + // | _____ | + // | | | | + // | | VOTE| | + // | |_____| | + // | | + // |______________________| + // \__________________/ + // || || + // || || + router.get("/", async (req, res) => { try { const ballots = await Ballot.findAll({ From fc7723caa27cd64d2c85834eb1471273b395394f Mon Sep 17 00:00:00 2001 From: tabularasae Date: Thu, 17 Jul 2025 15:36:45 -0400 Subject: [PATCH 39/70] added the ballot rankings to the get polls api --- api/polls.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index 82fcdf1..d00aed4 100644 --- a/api/polls.js +++ b/api/polls.js @@ -21,7 +21,13 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const poll = await Poll.findByPk(req.params.id, { - include: [PollOption, Ballot], + include: [ + PollOption, + { + model: Ballot, + include: [BallotRanking] + } + ], }); res.status(200).send(poll); if (!poll) { From 8487b5eadbe35fb42a9acc905a1bdb81f98e30ac Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 17 Jul 2025 18:46:05 -0400 Subject: [PATCH 40/70] fixed get route for auth/me --- auth/index.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/auth/index.js b/auth/index.js index 07968c5..a57325e 100644 --- a/auth/index.js +++ b/auth/index.js @@ -215,19 +215,27 @@ router.post("/logout", (req, res) => { }); // Get current user route (protected) -router.get("/me", (req, res) => { +router.get("/me", async (req, res) => { const token = req.cookies.token; if (!token) { return res.send({}); } - jwt.verify(token, JWT_SECRET, (err, user) => { - if (err) { - return res.status(403).send({ error: "Invalid or expired token" }); + try { + const decoded = jwt.verify(token, JWT_SECRET); + const user = await User.findByPk(decoded.id, { + attributes: { exclude: ['passwordHash'] } + }); + + if (!user) { + return res.status(404).send({ error: "User not found" }); } - res.send({ user: user }); - }); + + res.send(user); + } catch (err) { + return res.status(403).send({ error: "Invalid or expired token" }); + } }); module.exports = { router, authenticateJWT }; From 37cf426ea4005693262107b4fda26cd74c5a42ad Mon Sep 17 00:00:00 2001 From: tabularasae Date: Thu, 17 Jul 2025 19:06:41 -0400 Subject: [PATCH 41/70] Added advanced eager loading to get all polls api route --- api/polls.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index d00aed4..1d4a0d8 100644 --- a/api/polls.js +++ b/api/polls.js @@ -5,7 +5,13 @@ const { Poll, PollOption, Ballot, BallotRanking } = require("../database"); router.get("/", async (req, res) => { try { const polls = await Poll.findAll({ - include: [PollOption, Ballot], + include: [ + PollOption, + { + model: Ballot, + include: [BallotRanking] + } + ], }); console.log(`Found ${polls.length} polls`); res.status(200).send(polls); From ab78df39043b6d35d7d4ce97dcc962c8fd1d7098 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Fri, 18 Jul 2025 14:30:10 -0400 Subject: [PATCH 42/70] Fixed polls post route --- api/polls.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/api/polls.js b/api/polls.js index 00f525a..0897421 100644 --- a/api/polls.js +++ b/api/polls.js @@ -55,9 +55,26 @@ router.delete("/:id", async (req, res) => { router.post("/", async (req, res) => { try { - const poll = await Poll.create(req.body); - res.status(201).send(poll); + const { pollOptions, ...pollData } = req.body; + const poll = await Poll.create(pollData); + + if (pollOptions && Array.isArray(pollOptions) && pollOptions.length > 0) { + const options = pollOptions.map((option, index) => ({ + text: option.text, + position: option.position || index + 1, + poll_id: poll.id + })); + + await PollOption.bulkCreate(options); + } + + const pollWithOptions = await Poll.findByPk(poll.id, { + include: [PollOption] + }); + + res.status(201).send(pollWithOptions); } catch (error) { + console.error("Error creating poll:", error); res.status(500).json({ error: "Failed to create a poll" }); } }); From d85beb69b4f824d1ae0893cc01391fd118f97766 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Fri, 18 Jul 2025 15:31:11 -0400 Subject: [PATCH 43/70] Changed get route for auth/me to return 404 if no user is found instead of an empty user object --- auth/index.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/auth/index.js b/auth/index.js index a57325e..f5f5969 100644 --- a/auth/index.js +++ b/auth/index.js @@ -219,22 +219,20 @@ router.get("/me", async (req, res) => { const token = req.cookies.token; if (!token) { - return res.send({}); + return res.status(401).json({}); } - try { const decoded = jwt.verify(token, JWT_SECRET); const user = await User.findByPk(decoded.id, { - attributes: { exclude: ['passwordHash'] } + attributes: { exclude: ["passwordHash"] }, }); if (!user) { - return res.status(404).send({ error: "User not found" }); + return res.status(404).json({}); } - - res.send(user); + res.json(user); } catch (err) { - return res.status(403).send({ error: "Invalid or expired token" }); + return res.status(403).json({}); } }); From b22a65fc7235d22b2baf3923c4af408ef79f0f82 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Mon, 21 Jul 2025 10:50:57 -0400 Subject: [PATCH 44/70] Fixed delete route --- api/polls.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/api/polls.js b/api/polls.js index 0897421..493b2b2 100644 --- a/api/polls.js +++ b/api/polls.js @@ -40,16 +40,33 @@ router.get("/:id", async (req, res) => { router.delete("/:id", async (req, res) => { try { - const poll = await Poll.findByPk(req.params.id, { - include: [PollOption, Ballot, BallotRanking], - }); + const poll = await Poll.findByPk(req.params.id); + if (!poll) { - return res.status(404).json({ error: "poll not found" }); + return res.status(404).json({ error: "Poll not found" }); } + await BallotRanking.destroy({ + include: [{ + model: Ballot, + where: { poll_id: req.params.id } + }] + }); + + await Ballot.destroy({ + where: { poll_id: req.params.id } + }); + + await PollOption.destroy({ + where: {poll_id: req.params.id} + }); await poll.destroy(); - res.status(200).json({ message: "Poll deleted successfully" }); + + res.status(200).json({ + message: "Poll and all related data deleted successfully" + }); } catch (error) { - res.status(500).json({ error: "Failed to delete a poll" }); + console.error("Error deleting poll:", error); + res.status(500).json({ error: "Failed to delete poll" }); } }); From 8111aca78598fc771e5fe4748b4818839ba65a30 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Mon, 21 Jul 2025 10:59:18 -0400 Subject: [PATCH 45/70] Better error handling --- api/polls.js | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/api/polls.js b/api/polls.js index 493b2b2..f00635c 100644 --- a/api/polls.js +++ b/api/polls.js @@ -39,34 +39,58 @@ router.get("/:id", async (req, res) => { }); router.delete("/:id", async (req, res) => { + const transaction = await Poll.sequelize.transaction(); + try { - const poll = await Poll.findByPk(req.params.id); + const pollId = req.params.id; + const poll = await Poll.findByPk(pollId); if (!poll) { + await transaction.rollback(); return res.status(404).json({ error: "Poll not found" }); } - await BallotRanking.destroy({ - include: [{ - model: Ballot, - where: { poll_id: req.params.id } - }] + + const ballots = await Ballot.findAll({ + where: { poll_id: pollId }, + transaction }); + if (ballots.length > 0) { + const ballotIds = ballots.map(ballot => ballot.id); + await BallotRanking.destroy({ + where: { ballot_id: ballotIds }, + transaction + }); + } + await Ballot.destroy({ - where: { poll_id: req.params.id } + where: { poll_id: pollId }, + transaction }); await PollOption.destroy({ - where: {poll_id: req.params.id} + where: { poll_id: pollId }, + transaction }); - await poll.destroy(); + + await Poll.destroy({ + where: { id: pollId }, + transaction + }); + + await transaction.commit(); res.status(200).json({ message: "Poll and all related data deleted successfully" }); + } catch (error) { - console.error("Error deleting poll:", error); - res.status(500).json({ error: "Failed to delete poll" }); + await transaction.rollback(); + + res.status(500).json({ + error: "Failed to delete poll", + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); } }); From 6b7baa9d66f1dc3bf37a19f1b0f8232584019b7f Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 12:42:05 -0400 Subject: [PATCH 46/70] added the role to the user token --- app.js | 2 +- auth/index.js | 1 + database/user.js | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 76dde7b..beb5974 100644 --- a/app.js +++ b/app.js @@ -40,7 +40,7 @@ app.use((err, req, res, next) => { const runApp = async () => { try { // we have to keep it true, so that schema always matches the code before we happy with it - await db.sync(); + await db.sync({alter: true}); console.log("✅ Connected to the database"); app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); diff --git a/auth/index.js b/auth/index.js index a57325e..3db5bce 100644 --- a/auth/index.js +++ b/auth/index.js @@ -74,6 +74,7 @@ router.post("/auth0", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + role: user.role, }, JWT_SECRET, { expiresIn: "24h" } diff --git a/database/user.js b/database/user.js index 2653f8e..a6213d4 100644 --- a/database/user.js +++ b/database/user.js @@ -19,6 +19,11 @@ const User = db.define("user", { isUrl: true, }, }, + role: { + type: DataTypes.ENUM("user", "admin"), + allowNull: false, + defaultValue: "user", + }, email: { type: DataTypes.STRING, allowNull: true, From c48696a40a524e4e67219130e8c893b7647aa6eb Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 12:45:12 -0400 Subject: [PATCH 47/70] added the requireAdmin middleware --- auth/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/auth/index.js b/auth/index.js index 3db5bce..7f13198 100644 --- a/auth/index.js +++ b/auth/index.js @@ -23,6 +23,13 @@ const authenticateJWT = (req, res, next) => { }); }; +const requireAdmin = (req, res, next) => { + if (req.user.role !== 'admin') { + return res.status(403).send({ error: 'Admin privileges required' }); + } + next(); +}; + // Auth0 authentication route router.post("/auth0", async (req, res) => { try { @@ -239,4 +246,4 @@ router.get("/me", async (req, res) => { } }); -module.exports = { router, authenticateJWT }; +module.exports = { router, authenticateJWT, requireAdmin }; From 50f59cea7fedd03cb3b4c546fd162bd4b9f8056e Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 12:48:17 -0400 Subject: [PATCH 48/70] added the role return to the /me --- api/admin.js | 14 ++++++++++++++ auth/index.js | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 api/admin.js diff --git a/api/admin.js b/api/admin.js new file mode 100644 index 0000000..a5dabb8 --- /dev/null +++ b/api/admin.js @@ -0,0 +1,14 @@ +const express = require('express'); +const { authenticateJWT, requireAdmin } = require('../auth'); +const router = express.Router(); + +router.get( + '/users', + authenticateJWT, + requireAdmin, + (req, res) => { + res.send({ secretStats: { /* … */ } }); + } +); + +module.exports = router; \ No newline at end of file diff --git a/auth/index.js b/auth/index.js index 7f13198..8b4d447 100644 --- a/auth/index.js +++ b/auth/index.js @@ -24,8 +24,8 @@ const authenticateJWT = (req, res, next) => { }; const requireAdmin = (req, res, next) => { - if (req.user.role !== 'admin') { - return res.status(403).send({ error: 'Admin privileges required' }); + if (req.user.role !== "admin") { + return res.status(403).send({ error: "Admin privileges required" }); } next(); }; @@ -233,7 +233,7 @@ router.get("/me", async (req, res) => { try { const decoded = jwt.verify(token, JWT_SECRET); const user = await User.findByPk(decoded.id, { - attributes: { exclude: ['passwordHash'] } + attributes: { exclude: ["passwordHash"], include: ["role"] }, }); if (!user) { From 83f49a6be5d7f0c137a359c906dbd3430172e981 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 13:00:07 -0400 Subject: [PATCH 49/70] added the admin router --- api/admin.js | 15 +++++++++++---- api/index.js | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/api/admin.js b/api/admin.js index a5dabb8..67028b2 100644 --- a/api/admin.js +++ b/api/admin.js @@ -1,13 +1,20 @@ const express = require('express'); const { authenticateJWT, requireAdmin } = require('../auth'); const router = express.Router(); +const { User, Poll, Ballot } = require("../database"); router.get( '/users', - authenticateJWT, - requireAdmin, - (req, res) => { - res.send({ secretStats: { /* … */ } }); +// authenticateJWT, +// requireAdmin, + async (req, res) => { + try { + const users = await User.findAll({ include: [Poll, Ballot] }); + res.status(200).send(users); + } catch (error) { + console.error("Error fetching users: ", error); + res.status(500).send("Error fetching users"); + } } ); diff --git a/api/index.js b/api/index.js index 527e9dc..af53d41 100644 --- a/api/index.js +++ b/api/index.js @@ -5,10 +5,12 @@ const usersRouter = require("./users"); const pollsRouter = require("./polls"); const pollOptionsRouter = require("./pollOptions"); const ballotRouter = require("./ballots"); +const adminRouter = require("./admin") router.use("/users", usersRouter); router.use("/polls", pollsRouter); router.use("/pollOptions", pollOptionsRouter); router.use("/ballots", ballotRouter); +router.use("/admin", adminRouter) module.exports = router; \ No newline at end of file From 9aada90db1a844f315e81368b2cdf6e20b611f0c Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 14:11:59 -0400 Subject: [PATCH 50/70] added the comment --- auth/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/auth/index.js b/auth/index.js index 3771105..ff1f602 100644 --- a/auth/index.js +++ b/auth/index.js @@ -232,11 +232,8 @@ router.get("/me", async (req, res) => { try { const decoded = jwt.verify(token, JWT_SECRET); const user = await User.findByPk(decoded.id, { -<<<<<<< HEAD + // role is also included attributes: { exclude: ["passwordHash"], include: ["role"] }, -======= - attributes: { exclude: ["passwordHash"] }, ->>>>>>> main }); if (!user) { @@ -244,7 +241,7 @@ router.get("/me", async (req, res) => { } res.json(user); } catch (err) { - return res.status(403).json({}); + return res.status(403).json({});g } }); From d8f02c0a3c8505a52ee1210f44a69b138b066818 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 14:16:48 -0400 Subject: [PATCH 51/70] fixed a typo --- auth/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/index.js b/auth/index.js index ff1f602..d3e0456 100644 --- a/auth/index.js +++ b/auth/index.js @@ -241,7 +241,7 @@ router.get("/me", async (req, res) => { } res.json(user); } catch (err) { - return res.status(403).json({});g + return res.status(403).json({}); } }); From 9f5b8145704d41d3c2c57a2e4d3ea20efb463e94 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 14:40:47 -0400 Subject: [PATCH 52/70] cors enabled for localhost --- api/admin.js | 4 ++-- app.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/admin.js b/api/admin.js index 67028b2..f6b7567 100644 --- a/api/admin.js +++ b/api/admin.js @@ -5,8 +5,8 @@ const { User, Poll, Ballot } = require("../database"); router.get( '/users', -// authenticateJWT, -// requireAdmin, + authenticateJWT, + requireAdmin, async (req, res) => { try { const users = await User.findAll({ include: [Poll, Ballot] }); diff --git a/app.js b/app.js index beb5974..8583af7 100644 --- a/app.js +++ b/app.js @@ -18,7 +18,7 @@ app.use(express.json()); app.use( cors({ - origin: FRONTEND_URL, + origin: [FRONTEND_URL, "http://localhost:3000", "http://127.0.0.1:3000"], credentials: true, }) ); From 8a3a6784475cdb0439533bfccb940a4692db80be Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 15:31:24 -0400 Subject: [PATCH 53/70] Explicitly allowed cross-site cookie sharing --- api/{admin.js => admins.js} | 0 api/index.js | 4 ++-- auth/index.js | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) rename api/{admin.js => admins.js} (100%) diff --git a/api/admin.js b/api/admins.js similarity index 100% rename from api/admin.js rename to api/admins.js diff --git a/api/index.js b/api/index.js index af53d41..46062fe 100644 --- a/api/index.js +++ b/api/index.js @@ -5,12 +5,12 @@ const usersRouter = require("./users"); const pollsRouter = require("./polls"); const pollOptionsRouter = require("./pollOptions"); const ballotRouter = require("./ballots"); -const adminRouter = require("./admin") +const adminRouter = require("./admins") router.use("/users", usersRouter); router.use("/polls", pollsRouter); router.use("/pollOptions", pollOptionsRouter); router.use("/ballots", ballotRouter); -router.use("/admin", adminRouter) +router.use("/admins", adminRouter) module.exports = router; \ No newline at end of file diff --git a/auth/index.js b/auth/index.js index d3e0456..373c1c5 100644 --- a/auth/index.js +++ b/auth/index.js @@ -90,7 +90,9 @@ router.post("/auth0", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", + httpOnly: true, + secure: true, + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -101,6 +103,7 @@ router.post("/auth0", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + role: user.role, }, }); } catch (error) { @@ -143,6 +146,7 @@ router.post("/signup", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + role: user.role, }, JWT_SECRET, { expiresIn: "24h" } @@ -151,7 +155,9 @@ router.post("/signup", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: true, - sameSite: "strict", + httpOnly: true, + secure: true, + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -194,6 +200,7 @@ router.post("/login", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + role: user.role, }, JWT_SECRET, { expiresIn: "24h" } @@ -202,7 +209,9 @@ router.post("/login", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", + httpOnly: true, + secure: true, + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); From e3710a55b88221ef1f962a2dcabbb85c6cafe254 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 20:00:57 -0400 Subject: [PATCH 54/70] hotfix navbar admin visibility --- auth/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/index.js b/auth/index.js index 373c1c5..cfafddd 100644 --- a/auth/index.js +++ b/auth/index.js @@ -210,14 +210,14 @@ router.post("/login", async (req, res) => { httpOnly: true, secure: process.env.NODE_ENV === "production", httpOnly: true, - secure: true, - sameSite: "none", + secure: true, + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); res.send({ message: "Login successful", - user: { id: user.id, username: user.username }, + user: { id: user.id, username: user.username, role: user.role }, }); } catch (error) { console.error("Login error:", error); From 2abab684103ca805bfc846538d9ba7d871633630 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Mon, 21 Jul 2025 21:30:08 -0400 Subject: [PATCH 55/70] admin can close any poll --- api/admins.js | 28 +++++++++++++++++++- api/index.js | 2 +- api/polls.js | 73 ++++++++++++++++++++++++++------------------------- 3 files changed, 65 insertions(+), 38 deletions(-) diff --git a/api/admins.js b/api/admins.js index f6b7567..4ed0393 100644 --- a/api/admins.js +++ b/api/admins.js @@ -1,7 +1,7 @@ const express = require('express'); const { authenticateJWT, requireAdmin } = require('../auth'); const router = express.Router(); -const { User, Poll, Ballot } = require("../database"); +const { User, Poll, Ballot, PollOption } = require("../database"); router.get( '/users', @@ -18,4 +18,30 @@ router.get( } ); +router.patch( + "/polls/:id/close", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id, { + include: [PollOption, Ballot], + }); + if (!poll) return res.status(404).json({ error: "Poll not found" }); + if (poll.status === "closed") + return res.status(400).json({ error: "Poll already closed" }); + + poll.status = "closed"; + poll.isActive = false; + if (!poll.endAt) poll.endAt = new Date(); + await poll.save(); + + res.json(poll); + } catch (err) { + console.error("Error closing poll:", err); + res.status(500).json({ error: "Failed to close poll" }); + } + } +); + module.exports = router; \ No newline at end of file diff --git a/api/index.js b/api/index.js index 46062fe..199d3bb 100644 --- a/api/index.js +++ b/api/index.js @@ -11,6 +11,6 @@ router.use("/users", usersRouter); router.use("/polls", pollsRouter); router.use("/pollOptions", pollOptionsRouter); router.use("/ballots", ballotRouter); -router.use("/admins", adminRouter) +router.use("/admin", adminRouter) module.exports = router; \ No newline at end of file diff --git a/api/polls.js b/api/polls.js index 5ed70d0..6ac79fe 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,5 +1,6 @@ const express = require("express"); const router = express.Router(); +const { authenticateJWT, requireAdmin } = require("../auth"); const { Poll, PollOption, Ballot, BallotRanking } = require("../database"); router.get("/", async (req, res) => { @@ -9,8 +10,8 @@ router.get("/", async (req, res) => { PollOption, { model: Ballot, - include: [BallotRanking] - } + include: [BallotRanking], + }, ], }); console.log(`Found ${polls.length} polls`); @@ -31,8 +32,8 @@ router.get("/:id", async (req, res) => { PollOption, { model: Ballot, - include: [BallotRanking] - } + include: [BallotRanking], + }, ], }); res.status(200).send(poll); @@ -45,12 +46,12 @@ router.get("/:id", async (req, res) => { }); router.delete("/:id", async (req, res) => { - const transaction = await Poll.sequelize.transaction(); - + const transaction = await Poll.sequelize.transaction(); + try { const pollId = req.params.id; const poll = await Poll.findByPk(pollId); - + if (!poll) { await transaction.rollback(); return res.status(404).json({ error: "Poll not found" }); @@ -58,44 +59,44 @@ router.delete("/:id", async (req, res) => { const ballots = await Ballot.findAll({ where: { poll_id: pollId }, - transaction + transaction, }); if (ballots.length > 0) { - const ballotIds = ballots.map(ballot => ballot.id); + const ballotIds = ballots.map((ballot) => ballot.id); await BallotRanking.destroy({ where: { ballot_id: ballotIds }, - transaction + transaction, }); } await Ballot.destroy({ where: { poll_id: pollId }, - transaction + transaction, }); await PollOption.destroy({ where: { poll_id: pollId }, - transaction + transaction, }); await Poll.destroy({ where: { id: pollId }, - transaction + transaction, }); await transaction.commit(); - - res.status(200).json({ - message: "Poll and all related data deleted successfully" + + res.status(200).json({ + message: "Poll and all related data deleted successfully", }); - } catch (error) { await transaction.rollback(); - - res.status(500).json({ + + res.status(500).json({ error: "Failed to delete poll", - details: process.env.NODE_ENV === 'development' ? error.message : undefined + details: + process.env.NODE_ENV === "development" ? error.message : undefined, }); } }); @@ -104,21 +105,21 @@ router.post("/", async (req, res) => { try { const { pollOptions, ...pollData } = req.body; const poll = await Poll.create(pollData); - + if (pollOptions && Array.isArray(pollOptions) && pollOptions.length > 0) { const options = pollOptions.map((option, index) => ({ text: option.text, position: option.position || index + 1, - poll_id: poll.id + poll_id: poll.id, })); await PollOption.bulkCreate(options); } - + const pollWithOptions = await Poll.findByPk(poll.id, { - include: [PollOption] + include: [PollOption], }); - + res.status(201).send(pollWithOptions); } catch (error) { console.error("Error creating poll:", error); @@ -128,37 +129,37 @@ router.post("/", async (req, res) => { router.patch("/:id", async (req, res) => { try { - const { pollOptions, ...pollData } = req.body; - + const { pollOptions, ...pollData } = req.body; + const existingPoll = await Poll.findByPk(req.params.id); if (!existingPoll) { return res.status(404).send("Poll not found"); } - + if (Object.keys(pollData).length > 0) { await Poll.update(pollData, { where: { id: req.params.id }, }); } - + if (pollOptions && Array.isArray(pollOptions)) { await PollOption.destroy({ - where: { poll_id: req.params.id } + where: { poll_id: req.params.id }, }); - + const newOptions = pollOptions.map((option, index) => ({ text: option.text, position: option.position || index + 1, - poll_id: req.params.id + poll_id: req.params.id, })); - + await PollOption.bulkCreate(newOptions); } - + const updatedPoll = await Poll.findByPk(req.params.id, { - include: [PollOption] + include: [PollOption], }); - + res.status(200).send(updatedPoll); } catch (error) { console.error("Error updating poll:", error); From 3ef7fd23a860eade0a2fa7b33b846ccd1b958c9d Mon Sep 17 00:00:00 2001 From: tabularasae Date: Tue, 22 Jul 2025 12:22:06 -0400 Subject: [PATCH 56/70] added the member to user's schema --- database/user.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/database/user.js b/database/user.js index a6213d4..7318ed8 100644 --- a/database/user.js +++ b/database/user.js @@ -41,6 +41,11 @@ const User = db.define("user", { type: DataTypes.STRING, allowNull: true, }, + disabled: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, }); // Instance method to check password From c6d725fe1ee8b3c1b7e15dc1ea5a6fd3236c3c53 Mon Sep 17 00:00:00 2001 From: tabularasae Date: Tue, 22 Jul 2025 12:40:52 -0400 Subject: [PATCH 57/70] admin route to disable the user's account --- api/admins.js | 24 ++++++++++++++++++++++++ auth/index.js | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/api/admins.js b/api/admins.js index 4ed0393..4571f66 100644 --- a/api/admins.js +++ b/api/admins.js @@ -44,4 +44,28 @@ router.patch( } ); +router.patch( + "/users/:id/disable", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const target = await User.findByPk(req.params.id); + if (!target) return res.status(404).json({ error: "User not found" }); + + if (target.role === "admin") { + return res.status(403).json({ error: "Cannot disable an admin account" }); + } + + target.disabled = !target.disabled; + await target.save(); + + res.json({ id: target.id, disabled: target.disabled }); + } catch (err) { + console.error("Error disabling user:", err); + res.status(500).json({ error: "Failed to modify user" }); + } + } +); + module.exports = router; \ No newline at end of file diff --git a/auth/index.js b/auth/index.js index cfafddd..cb68b74 100644 --- a/auth/index.js +++ b/auth/index.js @@ -183,11 +183,16 @@ router.post("/login", async (req, res) => { // Find user const user = await User.findOne({ where: { username } }); - user.checkPassword(password); if (!user) { return res.status(401).send({ error: "Invalid credentials" }); } + if (user.disabled) { + return res + .status(403) + .send({ error: "Account disabled. Contact support." }); + } + // Check password if (!user.checkPassword(password)) { return res.status(401).send({ error: "Invalid credentials" }); From 7fd02ce9ef71db548752ef7244c9b6d9582096dc Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Tue, 22 Jul 2025 13:40:46 -0400 Subject: [PATCH 58/70] Friends and following backedn --- api/follows.js | 172 +++++++++++++++++++++++++++++++++++++++++ api/users.js | 164 ++++++++++++++++++++++++++++++++------- app.js | 4 + database/index.js | 22 ++++++ database/user.js | 14 ++++ database/userFollow.js | 46 +++++++++++ 6 files changed, 393 insertions(+), 29 deletions(-) create mode 100644 api/follows.js create mode 100644 database/userFollow.js diff --git a/api/follows.js b/api/follows.js new file mode 100644 index 0000000..42fe083 --- /dev/null +++ b/api/follows.js @@ -0,0 +1,172 @@ +const express = require("express"); +const router = express.Router(); +const { User, UserFollow } = require("../database"); +const { Op } = require("sequelize"); + +router.post("/", async (req, res) => { + try { + const { follower_id, following_id } = req.body; + + if (follower_id === following_id) { + return res.status(400).json({ error: "Cannot follow yourself" }); + } + + const existingFollow = await UserFollow.findOne({ + where: { follower_id, following_id } + }); + + if (existingFollow) { + return res.status(400).json({ error: "Already following this user" }); + } + + const follow = await UserFollow.create({ follower_id, following_id }); + + res.status(201).json({ + message: "Successfully followed user", + follow + }); + } catch (error) { + console.error("Error following user:", error); + res.status(500).json({ error: "Failed to follow user" }); + } +}); + +router.delete("/", async (req, res) => { + try { + const { follower_id, following_id } = req.body; + + const deletedCount = await UserFollow.destroy({ + where: { follower_id, following_id } + }); + + if (deletedCount === 0) { + return res.status(404).json({ error: "Follow relationship not found" }); + } + + res.json({ message: "Successfully unfollowed user" }); + } catch (error) { + console.error("Error unfollowing user:", error); + res.status(500).json({ error: "Failed to unfollow user" }); + } +}); + +router.get("/:userId/followers", async (req, res) => { + try { + const userId = req.params.userId; + + const user = await User.findByPk(userId, { + include: [{ + model: User, + as: "followers", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + }], + attributes: ["id", "username"] + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ + user: { id: user.id, username: user.username }, + followers: user.followers, + count: user.followers.length + }); + } catch (error) { + console.error("Error fetching followers:", error); + res.status(500).json({ error: "Failed to fetch followers" }); + } +}); + +router.get("/:userId/following", async (req, res) => { + try { + const userId = req.params.userId; + + const user = await User.findByPk(userId, { + include: [{ + model: User, + as: "following", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + }], + attributes: ["id", "username"] + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ + user: { id: user.id, username: user.username }, + following: user.following, + count: user.following.length + }); + } catch (error) { + console.error("Error fetching following:", error); + res.status(500).json({ error: "Failed to fetch following" }); + } +}); + +router.get("/:userId/friends", async (req, res) => { + try { + const userId = req.params.userId; + + const following = await UserFollow.findAll({ + where: { follower_id: userId }, + attributes: ["following_id"] + }); + + const followingIds = following.map(f => f.following_id); + + if (followingIds.length === 0) { + return res.json({ friends: [], count: 0 }); + } + + const mutualFollows = await UserFollow.findAll({ + where: { + follower_id: { [Op.in]: followingIds }, + following_id: userId + }, + include: [{ + model: User, + as: "follower", + attributes: ["id", "username", "imageUrl"] + }] + }); + + const friends = mutualFollows.map(mf => mf.follower); + + res.json({ + friends, + count: friends.length + }); + } catch (error) { + console.error("Error fetching friends:", error); + res.status(500).json({ error: "Failed to fetch friends" }); + } +}); + +router.get("/:userId/status/:targetUserId", async (req, res) => { + try { + const { userId, targetUserId } = req.params; + + const [isFollowing, isFollowed] = await Promise.all([ + UserFollow.findOne({ where: { follower_id: userId, following_id: targetUserId } }), + UserFollow.findOne({ where: { follower_id: targetUserId, following_id: userId } }) + ]); + + const areFriends = !!(isFollowing && isFollowed); + + res.json({ + isFollowing: !!isFollowing, + isFollowedBy: !!isFollowed, + areFriends + }); + } catch (error) { + console.error("Error checking follow status:", error); + res.status(500).json({ error: "Failed to check follow status" }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/users.js b/api/users.js index 8d73fd7..a74640f 100644 --- a/api/users.js +++ b/api/users.js @@ -5,7 +5,25 @@ const { User, Poll, Ballot } = require("../database"); //get all users router.get("/", async (req, res) => { try { - const users = await User.findAll({ include: [Poll, Ballot] }); + const users = await User.findAll({ + include: [ + Poll, + Ballot, + { + model: User, + as: "followers", + attributes: ["id", "username", "imageUrl"], + through: { attributes: [] } + }, + { + model: User, + as: "following", + attributes: ["id", "username", "imageUrl"], + through: { attributes: [] } + } + ], + attributes: { exclude: ['passwordHash'] } + }); res.status(200).send(users); } catch (error) { console.error("Error fetching users: ", error); @@ -17,11 +35,29 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { const user = await User.findByPk(req.params.id, { - include: [Poll, Ballot], + include: [ + Poll, + Ballot, + { + model: User, + as: "followers", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + }, + { + model: User, + as: "following", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + } + ], + attributes: { exclude: ['passwordHash'] } }); + if (!user) { return res.status(404).send("User not found"); } + res.status(200).send(user); } catch (error) { console.error("Error fetching user by ID:", error); @@ -60,39 +96,109 @@ router.post("/", async (req, res) => { } }); -router.patch("/:id", async(req,res) => { - try{ - const{ password, ...userData } = req.body; - - const existingUser= await User.findByPk(req.params.id); - if(!existingUser) { - return res.status(404).send("User not found"); +router.patch("/:id", async (req, res) => { + try { + const userId = req.params.id; + const { username, email, bio, imageUrl } = req.body; + + console.log("Updating user:", userId, req.body); // Debug log + + // Find the user + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: "User not found" }); } - - const updateData = {...userData}; - - if(password){ - updateData.passwordHash = User.hashPassword(password); + + // Check if username is already taken by another user + if (username && username !== user.username) { + const existingUser = await User.findOne({ + where: { username }, + attributes: ['id'] + }); + + if (existingUser && existingUser.id !== parseInt(userId)) { + return res.status(400).json({ error: "Username already taken" }); + } } - - const [updatedRows] = await User.update(updateData, { - where: {id: req.params.id}, + + // Update user fields + const updatedData = {}; + if (username !== undefined) updatedData.username = username.trim(); + if (email !== undefined) updatedData.email = email.trim(); + if (bio !== undefined) updatedData.bio = bio.trim(); + if (imageUrl !== undefined) updatedData.imageUrl = imageUrl.trim(); + + await user.update(updatedData); + + // Return updated user with polls + const updatedUser = await User.findByPk(userId, { + include: [{ + model: Poll, + include: ['pollOptions'] // Include poll options if needed + }], + attributes: { exclude: ['passwordHash'] } + }); + + console.log("User updated successfully:", updatedUser.username); // Debug log + + res.json(updatedUser); + } catch (error) { + console.error("Error updating user:", error); + res.status(500).json({ + error: "Failed to update profile", + details: process.env.NODE_ENV === 'development' ? error.message : undefined }); + } +}); - if (updatedRows === 0){ - return res.status(400).send("No changes made to the user"); +//route to get user stats (followers, following, friend count) +router.get("/:id/stats", async (req, res) => { + try { + const userId = req.params.id; + + const user = await User.findByPk(userId, { + include: [ + { + model: User, + as: "followers", + attributes: ["id"], + through: { attributes: [] } + }, + { + model: User, + as: "following", + attributes: ["id"], + through: { attributes: [] } + }, + { + model: Poll, + attributes: ["id"] + } + ], + attributes: ["id", "username"] + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); } - - const updatedUser = await User.findByPk(req.params.id,{ - include:[Poll, Ballot], - attributes: {exclude: ["passwordHash"]} + + const followingIds = user.following.map(f => f.id); + const followerIds = user.followers.map(f => f.id); + const friendIds = followingIds.filter(id => followerIds.includes(id)); + + res.json({ + userId: user.id, + username: user.username, + followersCount: user.followers.length, + followingCount: user.following.length, + friendsCount: friendIds.length, + pollsCount: user.polls.length }); - - res.status(200).send(updatedUser); - }catch (error){ - console.error("Error updating User: ", error); - res.status(500).send("Error User"); + + } catch (error) { + console.error("Error fetching user stats:", error); + res.status(500).json({ error: "Failed to fetch user stats" }); } -}) +}); module.exports = router; diff --git a/app.js b/app.js index beb5974..3cf878c 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,8 @@ const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); +const followsRouter = require("./api/follows"); + const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -31,6 +33,8 @@ app.use(express.static(path.join(__dirname, "public"))); // serve static files f app.use("/api", apiRouter); // mount api router app.use("/auth", authRouter); // mount auth router +app.use("/api/follows", followsRouter); + // error handling middleware app.use((err, req, res, next) => { console.error(err.stack); diff --git a/database/index.js b/database/index.js index a734c72..228d2c3 100644 --- a/database/index.js +++ b/database/index.js @@ -7,6 +7,7 @@ const BallotRanking = require("./ballotRanking"); const PollAllowedUser = require("./pollAllowedUser"); const PollResultValue = require("./pollResultValue"); const PollResult = require("./pollResult"); +const UserFollow = require("./userFollow"); User.hasMany(Poll, { foreignKey: "creator_id" }); Poll.belongsTo(User, { as: "creator", foreignKey: "creator_id" }); @@ -35,6 +36,12 @@ PollResultValue.belongsTo(PollOption, { foreignKey: "option_id" }); PollResult.hasMany(PollResultValue, { foreignKey: "poll_result_id" }); PollResultValue.belongsTo(PollResult, { foreignKey: "poll_result_id" }); +User.hasMany(UserFollow, { as: "followingRelations", foreignKey: "follower_id" }); +User.hasMany(UserFollow, { as: "followerRelations", foreignKey: "following_id" }); + +UserFollow.belongsTo(User, { as: "follower", foreignKey: "follower_id" }); +UserFollow.belongsTo(User, { as: "following", foreignKey: "following_id" }); + Poll.belongsToMany(User, { through: PollAllowedUser, as: "allowedUsers", @@ -46,6 +53,20 @@ User.belongsToMany(Poll, { foreignKey: "user_id", }); +User.belongsToMany(User, { + through: UserFollow, + as: "following", + foreignKey: "follower_id", + otherKey: "following_id", +}); + +User.belongsToMany(User, { + through: UserFollow, + as: "followers", + foreignKey: "following_id", + otherKey: "follower_id", +}); + module.exports = { db, User, @@ -56,4 +77,5 @@ module.exports = { PollAllowedUser, PollResult, PollResultValue, + UserFollow, }; diff --git a/database/user.js b/database/user.js index a6213d4..dea6b26 100644 --- a/database/user.js +++ b/database/user.js @@ -41,8 +41,22 @@ const User = db.define("user", { type: DataTypes.STRING, allowNull: true, }, + bio: { + type: DataTypes.STRING, + allowNull: true, + }, +},{ + getterMethods: { + followersCount() { + return this.followers ? this.followers.length : 0; + }, + followingCount() { + return this.following ? this.following.length : 0; + } + } }); + // Instance method to check password User.prototype.checkPassword = function (password) { if (!this.passwordHash) { diff --git a/database/userFollow.js b/database/userFollow.js new file mode 100644 index 0000000..d85556f --- /dev/null +++ b/database/userFollow.js @@ -0,0 +1,46 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const UserFollow = db.define("userfollow", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + follower_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + following_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, +}, { + indexes: [ + { + unique: true, + fields: ['follower_id', 'following_id'] + } + ], + validate: { + notSelfFollow() { + if (this.follower_id === this.following_id) { + throw new Error('Users cannot follow themselves'); + } + } + } +}); + +module.exports = UserFollow; \ No newline at end of file From 0bacb9081b9b27f5e70e888311abfdf1d3f5217d Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Tue, 22 Jul 2025 13:51:22 -0400 Subject: [PATCH 59/70] Fix minor bug --- api/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/users.js b/api/users.js index a74640f..9813a83 100644 --- a/api/users.js +++ b/api/users.js @@ -1,6 +1,6 @@ const express = require("express"); const router = express.Router(); -const { User, Poll, Ballot } = require("../database"); +const { User, Poll, Ballot, UserFollow } = require("../database"); //get all users router.get("/", async (req, res) => { From d45c7e8157303f36e3cfcfc440662e132bb2c190 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Tue, 22 Jul 2025 13:52:20 -0400 Subject: [PATCH 60/70] fix up --- api/users.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/api/users.js b/api/users.js index 9813a83..1b6a93d 100644 --- a/api/users.js +++ b/api/users.js @@ -101,15 +101,13 @@ router.patch("/:id", async (req, res) => { const userId = req.params.id; const { username, email, bio, imageUrl } = req.body; - console.log("Updating user:", userId, req.body); // Debug log + console.log("Updating user:", userId, req.body); - // Find the user const user = await User.findByPk(userId); if (!user) { return res.status(404).json({ error: "User not found" }); } - // Check if username is already taken by another user if (username && username !== user.username) { const existingUser = await User.findOne({ where: { username }, @@ -121,7 +119,6 @@ router.patch("/:id", async (req, res) => { } } - // Update user fields const updatedData = {}; if (username !== undefined) updatedData.username = username.trim(); if (email !== undefined) updatedData.email = email.trim(); @@ -130,16 +127,15 @@ router.patch("/:id", async (req, res) => { await user.update(updatedData); - // Return updated user with polls const updatedUser = await User.findByPk(userId, { include: [{ model: Poll, - include: ['pollOptions'] // Include poll options if needed + include: ['pollOptions'] }], attributes: { exclude: ['passwordHash'] } }); - console.log("User updated successfully:", updatedUser.username); // Debug log + console.log("User updated successfully:", updatedUser.username); res.json(updatedUser); } catch (error) { From 2de0fabe8e1860988575425ac7d3b3e5fb206e0a Mon Sep 17 00:00:00 2001 From: tabularasae Date: Wed, 23 Jul 2025 14:37:48 -0400 Subject: [PATCH 61/70] allowed admins to disable the accounts of the regular users --- auth/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/auth/index.js b/auth/index.js index cb68b74..d216e16 100644 --- a/auth/index.js +++ b/auth/index.js @@ -198,6 +198,12 @@ router.post("/login", async (req, res) => { return res.status(401).send({ error: "Invalid credentials" }); } + if (user.disabled) { + return res + .status(403) + .send({ error: "Account disabled. Contact support." }); + } + // Generate JWT token const token = jwt.sign( { From ebe13c59ab3b87226cb55b9ed52fc4b0e02a2eee Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Wed, 23 Jul 2025 15:21:08 -0400 Subject: [PATCH 62/70] Creation of data tables for permission to vote and view, also eager loading permissions, and new routes for permissions --- api/polls.js | 590 ++++++++++++++++++++++++++++----- auth/index.js | 145 +++++--- database/index.js | 76 ++++- database/poll.js | 12 +- database/pollViewPermission.js | 35 ++ database/pollVotePermission.js | 35 ++ database/user.js | 134 ++++---- services/pollPermissions.js | 108 ++++++ 8 files changed, 920 insertions(+), 215 deletions(-) create mode 100644 database/pollViewPermission.js create mode 100644 database/pollVotePermission.js create mode 100644 services/pollPermissions.js diff --git a/api/polls.js b/api/polls.js index 5ed70d0..ee19020 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,20 +1,52 @@ const express = require("express"); const router = express.Router(); -const { Poll, PollOption, Ballot, BallotRanking } = require("../database"); +const { authenticateJWT, requireAuth, requireAdmin } = require("../auth"); +const { Poll, PollOption, Ballot, BallotRanking, User, PollAllowedUser, PollResult, PollResultValue, PollViewPermission, PollVotePermission, db } = require("../database"); +const { checkPollViewPermission, checkPollVotePermission } = require("../services/pollPermissions"); +const { Op } = require("sequelize"); + +router.use(authenticateJWT); router.get("/", async (req, res) => { try { + const userId = req.user?.id; + const polls = await Poll.findAll({ include: [ - PollOption, + { + model: PollOption, + as: "PollOptions", + required: false + }, { model: Ballot, - include: [BallotRanking] + required: false, + include: [ + { + model: BallotRanking, + required: false + } + ], + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"], + required: false } ], }); - console.log(`Found ${polls.length} polls`); - res.status(200).send(polls); + + const accessiblePolls = []; + + for (const poll of polls) { + const canView = await checkPollViewPermission(poll, userId); + if (canView) { + accessiblePolls.push(poll); + } + } + + res.status(200).send(accessiblePolls); } catch (error) { console.error("Error fetching polls:", error); res.status(500).json({ @@ -26,144 +58,524 @@ router.get("/", async (req, res) => { router.get("/:id", async (req, res) => { try { + const userId = req.user?.id; + const poll = await Poll.findByPk(req.params.id, { include: [ - PollOption, + { + model: PollOption, + as: "PollOptions", + required: false, + order: [['position', 'ASC']] + }, { model: Ballot, - include: [BallotRanking] + required: false, + include: [ + { + model: BallotRanking, + required: false, + include: [ + { + model: PollOption, + required: false + } + ] + } + ], + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"], + required: false } ], }); - res.status(200).send(poll); + if (!poll) { return res.status(404).json({ error: "Poll not found" }); } + + const canView = await checkPollViewPermission(poll, userId); + if (!canView) { + return res.status(403).json({ + error: "Access denied", + message: "You don't have permission to view this poll", + requiresLogin: !userId && poll.viewRestriction !== "public" + }); + } + + const canVote = await checkPollVotePermission(poll, userId); + const pollData = poll.toJSON(); + + if (!pollData.PollOptions && pollData.pollOptions) { + pollData.PollOptions = pollData.pollOptions; + delete pollData.pollOptions; + } + + pollData.permissions = { + canView: true, + canVote + }; + + res.status(200).json(pollData); } catch (error) { + console.error("Error fetching poll by ID:", error); res.status(500).json({ error: "Failed to fetch poll by id" }); } }); -router.delete("/:id", async (req, res) => { - const transaction = await Poll.sequelize.transaction(); - +router.get("/search/users", async (req, res) => { try { - const pollId = req.params.id; - const poll = await Poll.findByPk(pollId); + const { q } = req.query; - if (!poll) { - await transaction.rollback(); - return res.status(404).json({ error: "Poll not found" }); + if (!q || q.length < 2) { + return res.json([]); } - const ballots = await Ballot.findAll({ - where: { poll_id: pollId }, - transaction + const users = await User.findAll({ + where: { + username: { + [Op.iLike]: `%${q}%` + } + }, + attributes: ["id", "username", "imageUrl"], + limit: 10 }); - if (ballots.length > 0) { - const ballotIds = ballots.map(ballot => ballot.id); - await BallotRanking.destroy({ - where: { ballot_id: ballotIds }, - transaction - }); - } + res.json(users); + } catch (error) { + console.error("Error searching users:", error); + res.status(500).json({ error: "Failed to search users" }); + } +}); - await Ballot.destroy({ - where: { poll_id: pollId }, - transaction +router.post("/", requireAuth, async (req, res) => { + try { + const { + title, + description, + allowAnonymous, + status, + endAt, + viewRestriction, + voteRestriction, + customViewUsers, + customVoteUsers, + pollOptions, + } = req.body; + + const userId = req.user.id; + + console.log("Creating poll with data:", req.body); + + const result = await db.transaction(async (transaction) => { + const poll = await Poll.create({ + creator_id: userId, + title: title.trim(), + description: description ? description.trim() : null, + allowAnonymous: allowAnonymous || false, + status: status || "draft", + endAt: endAt ? new Date(endAt) : null, + viewRestriction: viewRestriction || "public", + voteRestriction: voteRestriction || "public", + isActive: true + }, { transaction }); + + if (pollOptions && Array.isArray(pollOptions) && pollOptions.length > 0) { + const optionsData = pollOptions.map(option => ({ + poll_id: poll.id, + text: option.text.trim(), + position: option.position || 1 + })); + + await PollOption.bulkCreate(optionsData, { transaction }); + console.log(`Created ${optionsData.length} options for poll ${poll.id}`); + } + + if (viewRestriction === "custom" && customViewUsers && customViewUsers.length > 0) { + const viewPermissions = customViewUsers.map(userId => ({ + poll_id: poll.id, + user_id: userId + })); + await PollViewPermission.bulkCreate(viewPermissions, { transaction }); + } + + if (voteRestriction === "custom" && customVoteUsers && customVoteUsers.length > 0) { + const votePermissions = customVoteUsers.map(userId => ({ + poll_id: poll.id, + user_id: userId + })); + await PollVotePermission.bulkCreate(votePermissions, { transaction }); + } + + return poll; }); - await PollOption.destroy({ - where: { poll_id: pollId }, - transaction + res.status(201).json({ + message: `Poll ${status === 'draft' ? 'saved as draft' : 'published'} successfully`, + poll: result, + id: result.id }); - await Poll.destroy({ - where: { id: pollId }, - transaction + } catch (error) { + console.error("Error creating poll:", error); + res.status(500).json({ + error: "Failed to create poll", + details: process.env.NODE_ENV === 'development' ? error.message : undefined }); + } +}); - await transaction.commit(); +router.put("/:id", requireAuth, async (req, res) => { + try { + const pollId = req.params.id; + const userId = req.user.id; - res.status(200).json({ - message: "Poll and all related data deleted successfully" + const poll = await Poll.findByPk(pollId); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + if (poll.creator_id !== userId) { + return res.status(403).json({ error: "You can only edit your own polls" }); + } + + const { + title, + description, + allowAnonymous, + status, + endAt, + viewRestriction, + voteRestriction, + customViewUsers, + customVoteUsers, + pollOptions + } = req.body; + + console.log("Updating poll with data:", req.body); + + const result = await db.transaction(async (transaction) => { + await poll.update({ + title: title ? title.trim() : poll.title, + description: description !== undefined ? (description ? description.trim() : null) : poll.description, + allowAnonymous: allowAnonymous !== undefined ? allowAnonymous : poll.allowAnonymous, + status: status || poll.status, + endAt: endAt !== undefined ? (endAt ? new Date(endAt) : null) : poll.endAt, + viewRestriction: viewRestriction || poll.viewRestriction, + voteRestriction: voteRestriction || poll.voteRestriction, + }, { transaction }); + + if (pollOptions && Array.isArray(pollOptions)) { + await PollOption.destroy({ + where: { poll_id: pollId }, + transaction + }); + + if (pollOptions.length > 0) { + const optionsData = pollOptions.map((option, index) => ({ + poll_id: pollId, + text: option.text.trim(), + position: option.position || index + 1, + })); + + await PollOption.bulkCreate(optionsData, { transaction }); + } + } + + if (viewRestriction === "custom") { + await PollViewPermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + + if (customViewUsers && Array.isArray(customViewUsers) && customViewUsers.length > 0) { + const viewPermissions = customViewUsers.map(userId => ({ + poll_id: pollId, + user_id: userId + })); + await PollViewPermission.bulkCreate(viewPermissions, { transaction }); + } + } else { + await PollViewPermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + } + + if (voteRestriction === "custom") { + await PollVotePermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + + if (customVoteUsers && Array.isArray(customVoteUsers) && customVoteUsers.length > 0) { + const votePermissions = customVoteUsers.map(userId => ({ + poll_id: pollId, + user_id: userId + })); + await PollVotePermission.bulkCreate(votePermissions, { transaction }); + } + } else { + await PollVotePermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + } + + return poll; }); - + + const updatedPoll = await Poll.findByPk(pollId, { + include: [ + { + model: PollOption, + as: "PollOptions" + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"] + } + ], + }); + + res.status(200).json({ + message: `Poll ${status === 'draft' ? 'saved as draft' : 'updated'} successfully`, + poll: updatedPoll + }); + } catch (error) { - await transaction.rollback(); - + console.error("Error updating poll:", error); res.status(500).json({ - error: "Failed to delete poll", + error: "Failed to update poll", details: process.env.NODE_ENV === 'development' ? error.message : undefined }); } }); -router.post("/", async (req, res) => { +router.delete("/:id", async (req, res) => { try { - const { pollOptions, ...pollData } = req.body; - const poll = await Poll.create(pollData); - - if (pollOptions && Array.isArray(pollOptions) && pollOptions.length > 0) { - const options = pollOptions.map((option, index) => ({ - text: option.text, - position: option.position || index + 1, - poll_id: poll.id - })); - - await PollOption.bulkCreate(options); + const poll = await Poll.findByPk(req.params.id); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); } - - const pollWithOptions = await Poll.findByPk(poll.id, { - include: [PollOption] + + await poll.destroy(); + res.status(200).json({ message: "Poll deleted successfully" }); + } catch (error) { + console.error("Error deleting poll:", error); + res.status(500).json({ error: "Failed to delete poll" }); + } +}); + +router.get("/:pollId/results", async (req, res) => { + try { + const { pollId } = req.params; + + const poll = await Poll.findByPk(pollId, { + include: [ + { + model: PollOption, + include: [ + { + model: PollResultValue, + include: [ + { + model: PollResult, + where: { poll_id: pollId }, + required: false, + }, + ], + }, + ], + }, + ], }); - - res.status(201).send(pollWithOptions); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + res.status(200).send(poll); } catch (error) { - console.error("Error creating poll:", error); - res.status(500).json({ error: "Failed to create a poll" }); + console.error("Error fetching poll results:", error); + res.status(500).json({ error: "Failed to fetch poll results" }); } }); -router.patch("/:id", async (req, res) => { +router.post("/:pollId/vote", requireAuth, async (req, res) => { try { - const { pollOptions, ...pollData } = req.body; - - const existingPoll = await Poll.findByPk(req.params.id); - if (!existingPoll) { - return res.status(404).send("Poll not found"); + const { pollId } = req.params; + const { rankings } = req.body; + const userId = req.user.id; + + const poll = await Poll.findByPk(pollId, { + include: [{ model: PollOption, as: "PollOptions" }] + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); } - - if (Object.keys(pollData).length > 0) { - await Poll.update(pollData, { - where: { id: req.params.id }, + + const canVote = await checkPollVotePermission(poll, userId); + if (!canVote) { + return res.status(403).json({ + error: "Access denied", + message: "You don't have permission to vote on this poll" }); } - - if (pollOptions && Array.isArray(pollOptions)) { - await PollOption.destroy({ - where: { poll_id: req.params.id } + + if (poll.endAt && new Date() > new Date(poll.endAt)) { + return res.status(400).json({ error: "Poll has ended" }); + } + + if (userId) { + const existingBallot = await Ballot.findOne({ + where: { poll_id: pollId, user_id: userId } }); - - const newOptions = pollOptions.map((option, index) => ({ - text: option.text, - position: option.position || index + 1, - poll_id: req.params.id + + if (existingBallot) { + return res.status(400).json({ error: "You have already voted on this poll" }); + } + } + + const ballot = await Ballot.create({ + poll_id: pollId, + user_id: userId, + }); + + if (rankings && Array.isArray(rankings)) { + const ballotRankings = rankings.map(ranking => ({ + ballot_id: ballot.id, + option_id: ranking.option_id, + rank: ranking.rank, })); - - await PollOption.bulkCreate(newOptions); + + await BallotRanking.bulkCreate(ballotRankings); } - - const updatedPoll = await Poll.findByPk(req.params.id, { - include: [PollOption] + + res.status(201).json({ message: "Vote recorded successfully", ballot_id: ballot.id }); + } catch (error) { + console.error("Error recording vote:", error); + res.status(500).json({ error: "Failed to record vote" }); + } +}); + +router.get("/:pollId/my-ballot", requireAuth, async (req, res) => { + try { + const { pollId } = req.params; + const userId = req.user.id; + + const ballot = await Ballot.findOne({ + where: { poll_id: pollId, user_id: userId }, + include: [ + { + model: BallotRanking, + include: [{ model: PollOption }] + }, + ], }); + + if (!ballot) { + return res.status(404).json({ error: "No ballot found for this poll" }); + } + + res.status(200).send(ballot); + } catch (error) { + console.error("Error fetching user ballot:", error); + res.status(500).json({ error: "Failed to fetch ballot" }); + } +}); + +router.post("/:pollId/allowed-users", async (req, res) => { + try { + const { pollId } = req.params; + const { userIds } = req.body; + + const poll = await Poll.findByPk(pollId); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + if (userIds && Array.isArray(userIds)) { + const allowedUsers = userIds.map(userId => ({ + poll_id: pollId, + user_id: userId, + })); + + await PollAllowedUser.bulkCreate(allowedUsers, { + ignoreDuplicates: true, + }); + } + + res.status(201).json({ message: "Users added to allowed list" }); + } catch (error) { + console.error("Error adding allowed users:", error); + res.status(500).json({ error: "Failed to add allowed users" }); + } +}); + +router.get("/:pollId/allowed-users", async (req, res) => { + try { + const { pollId } = req.params; + + const poll = await Poll.findByPk(pollId, { + include: [ + { + model: User, + as: "allowedUsers", + attributes: ["id", "username", "imageUrl"], + }, + ], + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + res.status(200).send(poll.allowedUsers); + } catch (error) { + console.error("Error fetching allowed users:", error); + res.status(500).json({ error: "Failed to fetch allowed users" }); + } +}); + +router.get("/:id/permissions", requireAuth, async (req, res) => { + try { + const pollId = req.params.id; - res.status(200).send(updatedPoll); + const poll = await Poll.findByPk(pollId); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + const viewPermissions = await PollViewPermission.findAll({ + where: { poll_id: pollId }, + include: [{ + model: User, + attributes: ["id", "username", "imageUrl"] + }] + }); + + const votePermissions = await PollVotePermission.findAll({ + where: { poll_id: pollId }, + include: [{ + model: User, + attributes: ["id", "username", "imageUrl"] + }] + }); + + res.json({ + customViewUsers: viewPermissions.map(p => p.User), + customVoteUsers: votePermissions.map(p => p.User) + }); + } catch (error) { - console.error("Error updating poll:", error); - res.status(500).send("Error updating poll"); + console.error("Error fetching poll permissions:", error); + res.status(500).json({ error: "Failed to fetch poll permissions" }); } }); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/auth/index.js b/auth/index.js index d3e0456..939812b 100644 --- a/auth/index.js +++ b/auth/index.js @@ -6,25 +6,43 @@ const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; -// Middleware to authenticate JWT tokens -const authenticateJWT = (req, res, next) => { - const token = req.cookies.token; - - if (!token) { - return res.status(401).send({ error: "Access token required" }); +const authenticateJWT = async (req, res, next) => { + let token = null; + + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); } - - jwt.verify(token, JWT_SECRET, (err, user) => { - if (err) { - return res.status(403).send({ error: "Invalid or expired token" }); + + if (!token && req.cookies && req.cookies.token) { + token = req.cookies.token; + } + + if (token) { + try { + const decoded = jwt.verify(token, JWT_SECRET); + const user = await User.findByPk(decoded.id); + if (user) { + req.user = user; + } + } catch (error) { + console.error("JWT verification failed:", error); } - req.user = user; - next(); - }); + } + + next(); +}; + +// Middleware that requires authentication +const requireAuth = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); + } + next(); }; const requireAdmin = (req, res, next) => { - if (req.user.role !== "admin") { + if (!req.user || req.user.role !== "admin") { return res.status(403).send({ error: "Admin privileges required" }); } next(); @@ -58,8 +76,8 @@ router.post("/auth0", async (req, res) => { const userData = { auth0Id, email: email || null, - username: username || email?.split("@")[0] || `user_${Date.now()}`, // Use email prefix as username if no username provided - passwordHash: null, // Auth0 users don't have passwords + username: username || email?.split("@")[0] || `user_${Date.now()}`, + passwordHash: null, }; // Ensure username is unique @@ -102,6 +120,7 @@ router.post("/auth0", async (req, res) => { auth0Id: user.auth0Id, email: user.email, }, + token: token }); } catch (error) { console.error("Auth0 authentication error:", error); @@ -150,14 +169,20 @@ router.post("/signup", async (req, res) => { res.cookie("token", token, { httpOnly: true, - secure: true, + secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); res.send({ message: "User created successfully", - user: { id: user.id, username: user.username }, + user: { + id: user.id, + username: user.username, + email: user.email, + imageUrl: user.imageUrl + }, + token: token }); } catch (error) { console.error("Signup error:", error); @@ -169,50 +194,59 @@ router.post("/signup", async (req, res) => { router.post("/login", async (req, res) => { try { const { username, password } = req.body; - + if (!username || !password) { - res.status(400).send({ error: "Username and password are required" }); - return; + return res.status(400).json({ error: "Username and password are required" }); } - // Find user + // Find user by username const user = await User.findOne({ where: { username } }); - user.checkPassword(password); if (!user) { - return res.status(401).send({ error: "Invalid credentials" }); + return res.status(401).json({ error: "Invalid credentials" }); } - // Check password - if (!user.checkPassword(password)) { - return res.status(401).send({ error: "Invalid credentials" }); + // Verify password + const isValidPassword = User.comparePassword(password, user.passwordHash); + if (!isValidPassword) { + return res.status(401).json({ error: "Invalid credentials" }); } - - // Generate JWT token + + // Create JWT token const token = jwt.sign( - { - id: user.id, + { + id: user.id, username: user.username, - auth0Id: user.auth0Id, email: user.email, + role: user.role }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: '24h' } ); - - res.cookie("token", token, { + + // Set cookie (for backward compatibility) + res.cookie('token', token, { httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 24 * 60 * 60 * 1000, // 24 hours + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 24 * 60 * 60 * 1000 // 24 hours }); - - res.send({ + + // Also return token in response body + res.json({ message: "Login successful", - user: { id: user.id, username: user.username }, + user: { + id: user.id, + username: user.username, + email: user.email, + imageUrl: user.imageUrl, + role: user.role + }, + token: token }); + } catch (error) { console.error("Login error:", error); - res.sendStatus(500); + res.status(500).json({ error: "Login failed" }); } }); @@ -224,25 +258,38 @@ router.post("/logout", (req, res) => { // Get current user route (protected) router.get("/me", async (req, res) => { - const token = req.cookies.token; + let token = null; + + // Try to get token from Authorization header first + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + } + + // If no token in header, try to get from cookies + if (!token && req.cookies && req.cookies.token) { + token = req.cookies.token; + } if (!token) { - return res.status(401).json({}); + return res.status(401).json({ error: "No token provided" }); } + try { const decoded = jwt.verify(token, JWT_SECRET); const user = await User.findByPk(decoded.id, { - // role is also included - attributes: { exclude: ["passwordHash"], include: ["role"] }, + attributes: { exclude: ["passwordHash"] }, }); if (!user) { - return res.status(404).json({}); + return res.status(404).json({ error: "User not found" }); } + res.json(user); } catch (err) { - return res.status(403).json({}); + console.error("Token verification failed:", err); + return res.status(403).json({ error: "Invalid token" }); } }); -module.exports = { router, authenticateJWT, requireAdmin }; +module.exports = { router, authenticateJWT, requireAuth, requireAdmin }; \ No newline at end of file diff --git a/database/index.js b/database/index.js index 228d2c3..a8bdacc 100644 --- a/database/index.js +++ b/database/index.js @@ -7,41 +7,47 @@ const BallotRanking = require("./ballotRanking"); const PollAllowedUser = require("./pollAllowedUser"); const PollResultValue = require("./pollResultValue"); const PollResult = require("./pollResult"); -const UserFollow = require("./userFollow"); +const UserFollow = require("./userFollow"); +const PollViewPermission = require("./pollViewPermission"); +const PollVotePermission = require("./pollVotePermission"); +// User and Poll relationships User.hasMany(Poll, { foreignKey: "creator_id" }); Poll.belongsTo(User, { as: "creator", foreignKey: "creator_id" }); -Poll.hasMany(PollOption, { foreignKey: "poll_id", onDelete: "CASCADE" }); -PollOption.belongsTo(Poll, { foreignKey: "poll_id" }); +// Poll and PollOption relationships +Poll.hasMany(PollOption, { foreignKey: "poll_id", onDelete: "CASCADE",as: "PollOptions"}); +PollOption.belongsTo(Poll, { foreignKey: "poll_id", as: "Poll"}); +// Poll and Ballot relationships Poll.hasMany(Ballot, { foreignKey: "poll_id", onDelete: "CASCADE" }); Ballot.belongsTo(Poll, { foreignKey: "poll_id" }); -User.hasMany(Ballot, { foreignKey: "user_id" }); // nullable for anonymous +// User and Ballot relationships +User.hasMany(Ballot, { foreignKey: "user_id" }); Ballot.belongsTo(User, { foreignKey: "user_id" }); +// Ballot and BallotRanking relationships Ballot.hasMany(BallotRanking, { foreignKey: "ballot_id", onDelete: "CASCADE" }); BallotRanking.belongsTo(Ballot, { foreignKey: "ballot_id" }); +// PollOption and BallotRanking relationships PollOption.hasMany(BallotRanking, { foreignKey: "option_id" }); BallotRanking.belongsTo(PollOption, { foreignKey: "option_id" }); +// Poll and PollResult relationships Poll.hasOne(PollResult, { foreignKey: "poll_id", onDelete: "CASCADE" }); PollResult.belongsTo(Poll, { foreignKey: "poll_id" }); +// PollOption and PollResultValue relationships PollOption.hasMany(PollResultValue, { foreignKey: "option_id" }); PollResultValue.belongsTo(PollOption, { foreignKey: "option_id" }); +// PollResult and PollResultValue relationships PollResult.hasMany(PollResultValue, { foreignKey: "poll_result_id" }); PollResultValue.belongsTo(PollResult, { foreignKey: "poll_result_id" }); -User.hasMany(UserFollow, { as: "followingRelations", foreignKey: "follower_id" }); -User.hasMany(UserFollow, { as: "followerRelations", foreignKey: "following_id" }); - -UserFollow.belongsTo(User, { as: "follower", foreignKey: "follower_id" }); -UserFollow.belongsTo(User, { as: "following", foreignKey: "following_id" }); - +// Poll AllowedUsers relationships (existing allowed user system) Poll.belongsToMany(User, { through: PollAllowedUser, as: "allowedUsers", @@ -53,6 +59,7 @@ User.belongsToMany(Poll, { foreignKey: "user_id", }); +// User Follow Relationships (mutual following system) User.belongsToMany(User, { through: UserFollow, as: "following", @@ -67,6 +74,51 @@ User.belongsToMany(User, { otherKey: "follower_id", }); +// Additional follow relationships for easier access +User.hasMany(UserFollow, { as: "followingRelations", foreignKey: "follower_id" }); +User.hasMany(UserFollow, { as: "followerRelations", foreignKey: "following_id" }); + +UserFollow.belongsTo(User, { as: "follower", foreignKey: "follower_id" }); +UserFollow.belongsTo(User, { as: "following", foreignKey: "following_id" }); + +// New Poll View Permission Relationships +Poll.belongsToMany(User, { + through: PollViewPermission, + as: "viewAllowedUsers", + foreignKey: "poll_id", +}); +User.belongsToMany(Poll, { + through: PollViewPermission, + as: "viewablePolls", + foreignKey: "user_id", +}); + +// New Poll Vote Permission Relationships +Poll.belongsToMany(User, { + through: PollVotePermission, + as: "voteAllowedUsers", + foreignKey: "poll_id", +}); +User.belongsToMany(Poll, { + through: PollVotePermission, + as: "votablePolls", + foreignKey: "user_id", +}); + +// Direct relationships for permission models +PollViewPermission.belongsTo(Poll, { foreignKey: "poll_id" }); +PollViewPermission.belongsTo(User, { foreignKey: "user_id" }); + +PollVotePermission.belongsTo(Poll, { foreignKey: "poll_id" }); +PollVotePermission.belongsTo(User, { foreignKey: "user_id" }); + +// Additional direct relationships from Poll and User to permission models +Poll.hasMany(PollViewPermission, { foreignKey: "poll_id", onDelete: "CASCADE" }); +Poll.hasMany(PollVotePermission, { foreignKey: "poll_id", onDelete: "CASCADE" }); + +User.hasMany(PollViewPermission, { foreignKey: "user_id", onDelete: "CASCADE" }); +User.hasMany(PollVotePermission, { foreignKey: "user_id", onDelete: "CASCADE" }); + module.exports = { db, User, @@ -78,4 +130,6 @@ module.exports = { PollResult, PollResultValue, UserFollow, -}; + PollViewPermission, + PollVotePermission, +}; \ No newline at end of file diff --git a/database/poll.js b/database/poll.js index 89e6b7c..6e4fbe7 100644 --- a/database/poll.js +++ b/database/poll.js @@ -36,10 +36,18 @@ const Poll = db.define( defaultValue: true, }, publishedAt: DataTypes.DATE, + viewRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", + }, + voteRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", + }, }, { underscored: true, - } + }, ); -module.exports = Poll; +module.exports = Poll; \ No newline at end of file diff --git a/database/pollViewPermission.js b/database/pollViewPermission.js new file mode 100644 index 0000000..0ea360d --- /dev/null +++ b/database/pollViewPermission.js @@ -0,0 +1,35 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollViewPermission = db.define("pollViewPermission", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + poll_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'polls', + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, +}, { + indexes: [ + { + unique: true, + fields: ['poll_id', 'user_id'] + } + ] +}); + +module.exports = PollViewPermission; \ No newline at end of file diff --git a/database/pollVotePermission.js b/database/pollVotePermission.js new file mode 100644 index 0000000..561b95a --- /dev/null +++ b/database/pollVotePermission.js @@ -0,0 +1,35 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollVotePermission = db.define("pollVotePermission", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + poll_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'polls', + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, +}, { + indexes: [ + { + unique: true, + fields: ['poll_id', 'user_id'] + } + ] +}); + +module.exports = PollVotePermission; \ No newline at end of file diff --git a/database/user.js b/database/user.js index dea6b26..0004bf7 100644 --- a/database/user.js +++ b/database/user.js @@ -1,73 +1,79 @@ -const { DataTypes } = require("sequelize"); +const { DataTypes, Model } = require("sequelize"); const db = require("./db"); const bcrypt = require("bcrypt"); -const User = db.define("user", { - username: { - type: DataTypes.STRING, - allowNull: false, - unique: true, - validate: { - len: [3, 20], - }, - }, - imageUrl: { - type: DataTypes.STRING, - allowNull: false, - defaultValue: "https://cdn-icons-png.flaticon.com/512/2528/2528787.png", - validate: { - isUrl: true, - }, - }, - role: { - type: DataTypes.ENUM("user", "admin"), - allowNull: false, - defaultValue: "user", - }, - email: { - type: DataTypes.STRING, - allowNull: true, - unique: true, - validate: { - isEmail: true, - }, - }, - auth0Id: { - type: DataTypes.STRING, - allowNull: true, - unique: true, - }, - passwordHash: { - type: DataTypes.STRING, - allowNull: true, - }, - bio: { - type: DataTypes.STRING, - allowNull: true, - }, -},{ - getterMethods: { - followersCount() { - return this.followers ? this.followers.length : 0; - }, - followingCount() { - return this.following ? this.following.length : 0; - } +class User extends Model { + static hashPassword(password) { + return bcrypt.hashSync(password, 10); } -}); + static comparePassword(password, hash) { + return bcrypt.compareSync(password, hash); + } -// Instance method to check password -User.prototype.checkPassword = function (password) { - if (!this.passwordHash) { - return false; // Auth0 users don't have passwords + checkPassword(password) { + return bcrypt.compareSync(password, this.passwordHash); } - return bcrypt.compareSync(password, this.passwordHash); -}; +} -// Class method to hash password -User.hashPassword = function (password) { - return bcrypt.hashSync(password, 10); -}; +User.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + auth0Id: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + len: [3, 30], + }, + }, + email: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isEmail: true, + }, + }, + passwordHash: { + type: DataTypes.STRING, + allowNull: true, + }, + imageUrl: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: "https://t3.ftcdn.net/jpg/05/16/27/58/360_F_516275801_f3Fsp17x6HQK0xQgDQEELoTuERO4SsWV.jpg", + }, + bio: { + type: DataTypes.TEXT, + allowNull: true, + }, + role: { + type: DataTypes.ENUM("user", "admin"), + defaultValue: "user", + }, + viewRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", + }, + voteRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", + }, + }, + { + sequelize: db, + modelName: "User", + tableName: "users", + } +); -module.exports = User; +module.exports = User; \ No newline at end of file diff --git a/services/pollPermissions.js b/services/pollPermissions.js new file mode 100644 index 0000000..dcf4fcb --- /dev/null +++ b/services/pollPermissions.js @@ -0,0 +1,108 @@ +const { User, UserFollow, PollViewPermission, PollVotePermission } = require("../database"); +const { Op } = require("sequelize"); + +const checkPollViewPermission = async (poll, userId) => { + if (poll.viewRestriction === "public") { + return true; + } + + if (!userId) { + return false; + } + + if (poll.creator_id === userId) { + return true; + } + + switch (poll.viewRestriction) { + case "followers": + const isFollowing = await UserFollow.findOne({ + where: { + follower_id: userId, + following_id: poll.creator_id + } + }); + return !!isFollowing; + + case "friends": + const [userFollowsCreator, creatorFollowsUser] = await Promise.all([ + UserFollow.findOne({ + where: { follower_id: userId, following_id: poll.creator_id } + }), + UserFollow.findOne({ + where: { follower_id: poll.creator_id, following_id: userId } + }) + ]); + return !!(userFollowsCreator && creatorFollowsUser); + + case "custom": + const hasCustomPermission = await PollViewPermission.findOne({ + where: { + poll_id: poll.id, + user_id: userId + } + }); + return !!hasCustomPermission; + + default: + return false; + } +}; + +const checkPollVotePermission = async (poll, userId) => { + const canView = await checkPollViewPermission(poll, userId); + if (!canView) { + return false; + } + + if (poll.voteRestriction === "public") { + return true; + } + + if (!userId) { + return false; + } + + if (poll.creator_id === userId) { + return true; + } + + switch (poll.voteRestriction) { + case "followers": + const isFollowing = await UserFollow.findOne({ + where: { + follower_id: userId, + following_id: poll.creator_id + } + }); + return !!isFollowing; + + case "friends": + const [userFollowsCreator, creatorFollowsUser] = await Promise.all([ + UserFollow.findOne({ + where: { follower_id: userId, following_id: poll.creator_id } + }), + UserFollow.findOne({ + where: { follower_id: poll.creator_id, following_id: userId } + }) + ]); + return !!(userFollowsCreator && creatorFollowsUser); + + case "custom": + const hasCustomPermission = await PollVotePermission.findOne({ + where: { + poll_id: poll.id, + user_id: userId + } + }); + return !!hasCustomPermission; + + default: + return false; + } +}; + +module.exports = { + checkPollViewPermission, + checkPollVotePermission, +}; \ No newline at end of file From d989f1700f72fa70bbfc3aba7e84ce80c9731621 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Wed, 23 Jul 2025 15:28:54 -0400 Subject: [PATCH 63/70] rebase --- database/user.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/database/user.js b/database/user.js index 0004bf7..0fc1519 100644 --- a/database/user.js +++ b/database/user.js @@ -56,6 +56,11 @@ User.init( type: DataTypes.TEXT, allowNull: true, }, + disabled: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, role: { type: DataTypes.ENUM("user", "admin"), defaultValue: "user", From fcbe5c5fd4c6ab048dd3158f38095aa00db13ba2 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Fri, 25 Jul 2025 16:52:03 -0400 Subject: [PATCH 64/70] Hot fix --- api/polls.js | 21 ++++- api/users.js | 2 +- auth/index.js | 22 ++--- package-lock.json | 219 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + 5 files changed, 249 insertions(+), 18 deletions(-) diff --git a/api/polls.js b/api/polls.js index ee19020..546f2ef 100644 --- a/api/polls.js +++ b/api/polls.js @@ -12,6 +12,15 @@ router.get("/", async (req, res) => { const userId = req.user?.id; const polls = await Poll.findAll({ + where: { + [Op.or]: [ + { status: "published" }, // Published polls are visible to everyone (subject to view permissions) + { + status: "draft", + creator_id: userId || -1 // Draft polls only visible to their creator + } + ] + }, include: [ { model: PollOption, @@ -40,9 +49,15 @@ router.get("/", async (req, res) => { const accessiblePolls = []; for (const poll of polls) { - const canView = await checkPollViewPermission(poll, userId); - if (canView) { - accessiblePolls.push(poll); + if (poll.status === "draft") { + if (poll.creator_id === userId) { + accessiblePolls.push(poll); + } + } else { + const canView = await checkPollViewPermission(poll, userId); + if (canView) { + accessiblePolls.push(poll); + } } } diff --git a/api/users.js b/api/users.js index 1b6a93d..283c937 100644 --- a/api/users.js +++ b/api/users.js @@ -130,7 +130,7 @@ router.patch("/:id", async (req, res) => { const updatedUser = await User.findByPk(userId, { include: [{ model: Poll, - include: ['pollOptions'] + include: ['PollOptions'] }], attributes: { exclude: ['passwordHash'] } }); diff --git a/auth/index.js b/auth/index.js index b0ef930..8dc7f08 100644 --- a/auth/index.js +++ b/auth/index.js @@ -57,30 +57,24 @@ router.post("/auth0", async (req, res) => { return res.status(400).send({ error: "Auth0 ID is required" }); } - // Try to find existing user by auth0Id first let user = await User.findOne({ where: { auth0Id } }); if (!user && email) { - // If no user found by auth0Id, try to find by email user = await User.findOne({ where: { email } }); - if (user) { - // Update existing user with auth0Id user.auth0Id = auth0Id; await user.save(); } } if (!user) { - // Create new user if not found const userData = { auth0Id, email: email || null, username: username || email?.split("@")[0] || `user_${Date.now()}`, - passwordHash: null, + passwordHash: null, }; - // Ensure username is unique let finalUsername = userData.username; let counter = 1; while (await User.findOne({ where: { username: finalUsername } })) { @@ -92,7 +86,6 @@ router.post("/auth0", async (req, res) => { user = await User.create(userData); } - // Generate JWT token with auth0Id included const token = jwt.sign( { id: user.id, @@ -105,16 +98,16 @@ router.post("/auth0", async (req, res) => { { expiresIn: "24h" } ); + // Set cookie res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - httpOnly: true, - secure: true, - sameSite: "none", - maxAge: 24 * 60 * 60 * 1000, // 24 hours + sameSite: process.env.NODE_ENV === "production" ? "none" : "strict", + maxAge: 24 * 60 * 60 * 1000, }); - res.send({ + // Return token in response + res.json({ message: "Auth0 authentication successful", user: { id: user.id, @@ -122,12 +115,13 @@ router.post("/auth0", async (req, res) => { auth0Id: user.auth0Id, email: user.email, role: user.role, + imageUrl: user.imageUrl }, token: token }); } catch (error) { console.error("Auth0 authentication error:", error); - res.sendStatus(500); + res.status(500).json({ error: "Authentication failed" }); } }); diff --git a/package-lock.json b/package-lock.json index 7b55c6c..8270d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "auth0": "^4.27.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-jwt": "^8.5.1", "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.2.0", "morgan": "^1.10.0", "pg": "^8.16.2", "sequelize": "^6.37.7", @@ -284,6 +287,25 @@ "react": "*" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -299,12 +321,58 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "license": "MIT" }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/find-cache-dir": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==", "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -320,6 +388,18 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -329,6 +409,27 @@ "@types/node": "*" } }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/validator": { "version": "13.15.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", @@ -419,6 +520,39 @@ "node": ">= 8" } }, + "node_modules/auth0": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.27.0.tgz", + "integrity": "sha512-4FGgjzKCH/f7rQLQVR5dM30asjOObeW3PyHa8bQrS4rKkuv22JoNxox26fb1FZ3hI4zEgbVbPm9x7pHrljZzrw==", + "license": "MIT", + "dependencies": { + "jose": "^4.13.2", + "undici-types": "^6.15.0", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/auth0/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/auth0/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/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -962,6 +1096,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-jwt": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1384,6 +1538,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1417,6 +1580,23 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwks-rsa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", + "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.20", + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -1427,6 +1607,11 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -1451,6 +1636,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1499,6 +1690,28 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -3096,6 +3309,12 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 02f3514..49b9767 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,15 @@ "license": "ISC", "description": "", "dependencies": { + "auth0": "^4.27.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-jwt": "^8.5.1", "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.2.0", "morgan": "^1.10.0", "pg": "^8.16.2", "sequelize": "^6.37.7", From 5edfb23b054589d1f804d90f5a47a5690a3b430f Mon Sep 17 00:00:00 2001 From: tabularasae Date: Thu, 24 Jul 2025 15:46:28 -0400 Subject: [PATCH 65/70] crossSite cookie sharing --- auth/index.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/auth/index.js b/auth/index.js index 8dc7f08..404a8d0 100644 --- a/auth/index.js +++ b/auth/index.js @@ -168,7 +168,9 @@ router.post("/signup", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", + httpOnly: true, + secure: true, + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -222,11 +224,13 @@ router.post("/login", async (req, res) => { ); // Set cookie (for backward compatibility) - res.cookie('token', token, { + res.cookie("token", token, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 24 * 60 * 60 * 1000 // 24 hours + secure: process.env.NODE_ENV === "production", + httpOnly: true, + secure: true, + sameSite: "none", + maxAge: 24 * 60 * 60 * 1000, // 24 hours }); // Also return token in response body From 33dfc2a7eb92d737a22eead1c660d0be9bc7fffa Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Sun, 27 Jul 2025 20:31:23 -0400 Subject: [PATCH 66/70] Hot fix --- api/users.js | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/api/users.js b/api/users.js index 283c937..8194e2c 100644 --- a/api/users.js +++ b/api/users.js @@ -1,6 +1,7 @@ const express = require("express"); const router = express.Router(); const { User, Poll, Ballot, UserFollow } = require("../database"); +const { authenticateJWT } = require("../auth"); //get all users router.get("/", async (req, res) => { @@ -96,7 +97,7 @@ router.post("/", async (req, res) => { } }); -router.patch("/:id", async (req, res) => { +router.patch("/:id", authenticateJWT, async (req, res) => { try { const userId = req.params.id; const { username, email, bio, imageUrl } = req.body; @@ -108,30 +109,48 @@ router.patch("/:id", async (req, res) => { return res.status(404).json({ error: "User not found" }); } - if (username && username !== user.username) { + if (req.user.id !== parseInt(userId) && req.user.role !== 'admin') { + return res.status(403).json({ error: "Unauthorized to update this profile" }); + } + + const updatedData = {}; + + if (username !== undefined) { + const trimmedUsername = username.trim(); + if (trimmedUsername.length < 3) { + return res.status(400).json({ error: "Username must be at least 3 characters long" }); + } + const existingUser = await User.findOne({ - where: { username }, + where: { username: trimmedUsername }, attributes: ['id'] }); if (existingUser && existingUser.id !== parseInt(userId)) { return res.status(400).json({ error: "Username already taken" }); } + + updatedData.username = trimmedUsername; } - const updatedData = {}; - if (username !== undefined) updatedData.username = username.trim(); - if (email !== undefined) updatedData.email = email.trim(); - if (bio !== undefined) updatedData.bio = bio.trim(); - if (imageUrl !== undefined) updatedData.imageUrl = imageUrl.trim(); + if (email !== undefined) { + const trimmedEmail = email.trim(); + updatedData.email = trimmedEmail === '' ? null : trimmedEmail; + } - await user.update(updatedData); + if (bio !== undefined) { + const trimmedBio = bio.trim(); + updatedData.bio = trimmedBio === '' ? null : trimmedBio; + } + if (imageUrl !== undefined) { + const trimmedImageUrl = imageUrl.trim(); + updatedData.imageUrl = trimmedImageUrl === '' ? null : trimmedImageUrl; + } + + await user.update(updatedData); + const updatedUser = await User.findByPk(userId, { - include: [{ - model: Poll, - include: ['PollOptions'] - }], attributes: { exclude: ['passwordHash'] } }); @@ -142,7 +161,7 @@ router.patch("/:id", async (req, res) => { console.error("Error updating user:", error); res.status(500).json({ error: "Failed to update profile", - details: process.env.NODE_ENV === 'development' ? error.message : undefined + details: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); From 8f3b6833c2f6a56b016703005f3949e0226691e4 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Mon, 28 Jul 2025 01:00:29 -0400 Subject: [PATCH 67/70] / --- auth/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/auth/index.js b/auth/index.js index 404a8d0..b064289 100644 --- a/auth/index.js +++ b/auth/index.js @@ -17,6 +17,7 @@ const authenticateJWT = async (req, res, next) => { if (!token && req.cookies && req.cookies.token) { token = req.cookies.token; } + //commit if (token) { try { From 415a41f00302356a8b0395913073126172c8e6f0 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Mon, 28 Jul 2025 01:02:42 -0400 Subject: [PATCH 68/70] hotfix --- auth/index.js | 51 ++++++++++++++++----------------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/auth/index.js b/auth/index.js index b064289..939e420 100644 --- a/auth/index.js +++ b/auth/index.js @@ -195,58 +195,39 @@ router.post("/signup", async (req, res) => { router.post("/login", async (req, res) => { try { const { username, password } = req.body; - - if (!username || !password) { - return res.status(400).json({ error: "Username and password are required" }); - } - // Find user by username const user = await User.findOne({ where: { username } }); if (!user) { return res.status(401).json({ error: "Invalid credentials" }); } - - // Verify password - const isValidPassword = User.comparePassword(password, user.passwordHash); - if (!isValidPassword) { + + // Check if user is disabled + if (user.disabled) { + return res.status(403).json({ error: "Account is disabled. Please contact support." }); + } + + const validPassword = await bcrypt.compare(password, user.passwordHash); + if (!validPassword) { return res.status(401).json({ error: "Invalid credentials" }); } - - // Create JWT token + const token = jwt.sign( - { - id: user.id, - username: user.username, - email: user.email, - role: user.role - }, - JWT_SECRET, - { expiresIn: '24h' } + { id: user.id, username: user.username }, + process.env.JWT_SECRET, + { expiresIn: "24h" } ); - - // Set cookie (for backward compatibility) - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - httpOnly: true, - secure: true, - sameSite: "none", - maxAge: 24 * 60 * 60 * 1000, // 24 hours - }); - - // Also return token in response body + res.json({ - message: "Login successful", + token, user: { id: user.id, username: user.username, email: user.email, + bio: user.bio, imageUrl: user.imageUrl, role: user.role - }, - token: token + } }); - } catch (error) { console.error("Login error:", error); res.status(500).json({ error: "Login failed" }); From c04240e1240d8824b4e2e0394dc903659080675c Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Mon, 28 Jul 2025 01:21:24 -0400 Subject: [PATCH 69/70] admin --- api/admins.js | 113 +++++++++++++++++++++++++++++++++++++++++++++++++ api/ballots.js | 54 ++++++++++++++++++++++- 2 files changed, 165 insertions(+), 2 deletions(-) diff --git a/api/admins.js b/api/admins.js index 4571f66..c234718 100644 --- a/api/admins.js +++ b/api/admins.js @@ -18,6 +18,80 @@ router.get( } ); +router.get( + '/polls', + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const polls = await Poll.findAll({ + include: [ + { + model: PollOption, + as: "PollOptions" + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"] + }, + { + model: Ballot, + required: false + } + ], + order: [["createdAt", "DESC"]] + }); + res.status(200).json(polls); + } catch (error) { + console.error("Error fetching polls for admin:", error); + res.status(500).json({ error: "Failed to fetch polls" }); + } + } +); + +router.patch( + "/polls/:id/disable", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id, { + include: [ + { + model: User, + as: "creator", + attributes: ["id", "username"] + } + ] + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + poll.isActive = !poll.isActive; + + if (!poll.isActive && !poll.endAt) { + poll.endAt = new Date(); + } + + await poll.save(); + + res.json({ + id: poll.id, + isActive: poll.isActive, + title: poll.title, + creator: poll.creator?.username, + message: `Poll ${poll.isActive ? 'enabled' : 'disabled'} successfully` + }); + } catch (err) { + console.error("Error toggling poll status:", err); + res.status(500).json({ error: "Failed to update poll status" }); + } + } +); + router.patch( "/polls/:id/close", authenticateJWT, @@ -44,6 +118,45 @@ router.patch( } ); +router.delete( + "/polls/:id", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id, { + include: [ + { + model: User, + as: "creator", + attributes: ["id", "username"] + } + ] + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + const pollInfo = { + id: poll.id, + title: poll.title, + creator: poll.creator?.username + }; + + await poll.destroy(); + + res.json({ + message: "Poll deleted successfully", + deletedPoll: pollInfo + }); + } catch (err) { + console.error("Error deleting poll:", err); + res.status(500).json({ error: "Failed to delete poll" }); + } + } +); + router.patch( "/users/:id/disable", authenticateJWT, diff --git a/api/ballots.js b/api/ballots.js index ad74145..11d970e 100644 --- a/api/ballots.js +++ b/api/ballots.js @@ -8,6 +8,7 @@ const { BallotRanking, db, } = require("../database"); +const { requireAuth } = require("../auth"); // _______ // | | @@ -83,6 +84,52 @@ router.post("/", async (req, res) => { try { tx = await db.transaction(); + const poll = await Poll.findByPk(pollId); + if (!poll) { + await tx.rollback(); + return res.status(404).json({ error: "Poll not found" }); + } + + if (!poll.isActive) { + await tx.rollback(); + return res.status(403).json({ + error: "This poll has been disabled by an administrator" + }); + } + + if (poll.endAt && new Date() > new Date(poll.endAt)) { + await tx.rollback(); + return res.status(400).json({ error: "Poll has ended" }); + } + + if (poll.status !== "published") { + await tx.rollback(); + return res.status(400).json({ error: "Cannot vote on unpublished polls" }); + } + + if (!poll.allowAnonymous && !userId) { + await tx.rollback(); + return res.status(401).json({ + error: "Please log in to vote on this poll" + }); + } + + if (userId && !poll.allowAnonymous) { + const existingBallot = await Ballot.findOne({ + where: { + user_id: userId, + poll_id: pollId + } + }); + + if (existingBallot) { + await tx.rollback(); + return res.status(400).json({ + error: "You have already voted on this poll" + }); + } + } + const ballot = await Ballot.create( { user_id: userId, @@ -104,7 +151,10 @@ router.post("/", async (req, res) => { include: [{ model: BallotRanking, order: [["rank", "ASC"]] }], }); - return res.status(201).json(result); + return res.status(201).json({ + message: "Vote recorded successfully", + ballot: result + }); } catch (err) { if (tx) await tx.rollback(); console.error("Failed to create ballot:", err); @@ -115,4 +165,4 @@ router.post("/", async (req, res) => { } }); -module.exports = router; +module.exports = router; \ No newline at end of file From b2e75dddb03125aedada60b5ca2a06136fec7641 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Mon, 28 Jul 2025 10:36:02 -0400 Subject: [PATCH 70/70] Hot fix --- api/users.js | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ auth/index.js | 1 + 2 files changed, 104 insertions(+) diff --git a/api/users.js b/api/users.js index 8194e2c..2eb21db 100644 --- a/api/users.js +++ b/api/users.js @@ -3,6 +3,109 @@ const router = express.Router(); const { User, Poll, Ballot, UserFollow } = require("../database"); const { authenticateJWT } = require("../auth"); +router.post("/signup", async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required" }); + } + + if (password.length < 6) { + return res.status(400).json({ error: "Password must be at least 6 characters long" }); + } + + if (username.length < 3 || username.length > 20) { + return res.status(400).json({ error: "Username must be between 3 and 20 characters" }); + } + + // Check if user already exists + const existingUser = await User.findOne({ where: { username } }); + if (existingUser) { + return res.status(409).json({ error: "Username already exists" }); + } + + // Create new user + const passwordHash = await bcrypt.hash(password, 10); + const user = await User.create({ + username, + passwordHash, + role: 'user' // Set default role + }); + const token = jwt.sign( + { + id: user.id, + username: user.username, + role: user.role, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.status(201).json({ + message: "User created successfully", + token, + user: { + id: user.id, + username: user.username, + email: user.email, + bio: user.bio, + imageUrl: user.imageUrl, + role: user.role + } + }); + } catch (error) { + console.error("Signup error:", error); + res.status(500).json({ error: "Failed to create user" }); + } +}); + +router.post("/login", async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required" }); + } + + const user = await User.findOne({ where: { username } }); + if (!user) { + return res.status(401).json({ error: "Invalid credentials" }); + } + + // Check if user is disabled + if (user.disabled) { + return res.status(403).json({ error: "Account is disabled. Please contact support." }); + } + + const validPassword = await bcrypt.compare(password, user.passwordHash); + if (!validPassword) { + return res.status(401).json({ error: "Invalid credentials" }); + } + + const token = jwt.sign( + { id: user.id, username: user.username }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.json({ + token, + user: { + id: user.id, + username: user.username, + email: user.email, + bio: user.bio, + imageUrl: user.imageUrl, + role: user.role + } + }); + } catch (error) { + console.error("Login error:", error); + res.status(500).json({ error: "Login failed" }); + } +}); + //get all users router.get("/", async (req, res) => { try { diff --git a/auth/index.js b/auth/index.js index 939e420..6366d32 100644 --- a/auth/index.js +++ b/auth/index.js @@ -1,5 +1,6 @@ const express = require("express"); const jwt = require("jsonwebtoken"); +const bcrypt = require("bcrypt"); const { User } = require("../database"); const router = express.Router();