From b9a969aae094dcc8d4856a3f155f0ec9015b1316 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Sat, 12 Jul 2025 10:53:30 -0400 Subject: [PATCH 001/148] chore: added model folder for better stucture --- database/{ => models}/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename database/{ => models}/user.js (96%) diff --git a/database/user.js b/database/models/user.js similarity index 96% rename from database/user.js rename to database/models/user.js index 755c757..4fe3d9a 100644 --- a/database/user.js +++ b/database/models/user.js @@ -1,5 +1,5 @@ const { DataTypes } = require("sequelize"); -const db = require("./db"); +const db = require("../db"); const bcrypt = require("bcrypt"); const User = db.define("user", { From 49d5bfb93ce0a6f11a105378085d6bc93a034a98 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Sun, 13 Jul 2025 14:05:48 -0400 Subject: [PATCH 002/148] feat: user model addeed adn export to db --- database/index.js | 2 +- database/models/user.js | 44 +++++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/database/index.js b/database/index.js index e498df6..34e8404 100644 --- a/database/index.js +++ b/database/index.js @@ -1,5 +1,5 @@ const db = require("./db"); -const User = require("./user"); +const User = require("./models/User"); module.exports = { db, diff --git a/database/models/user.js b/database/models/user.js index 4fe3d9a..2ab73f9 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -3,31 +3,59 @@ const db = require("../db"); const bcrypt = require("bcrypt"); const User = db.define("user", { - username: { + firstName: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: true, + len: [1, 50], + }, + }, + lastName: { type: DataTypes.STRING, allowNull: false, - unique: true, validate: { - len: [3, 20], + notEmpty: true, + len: [1, 50], }, }, email: { type: DataTypes.STRING, - allowNull: true, + allowNull: false, unique: true, validate: { isEmail: true, + notEmpty: true, }, }, - auth0Id: { + passwordHash: { type: DataTypes.STRING, - allowNull: true, - unique: true, + allowNull: false, + validate: { + notEmpty: true, + }, }, - passwordHash: { + img: { type: DataTypes.STRING, allowNull: true, + validate: { + isUrl: true, + }, + }, + isAdmin: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + isDisable: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, }, +}, { + timestamps: true, + createdAt: 'created_at', + updatedAt: false, }); // Instance method to check password From 82e325b70aab1e8f55ed7178f99833f9f4c3ac5f Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Sun, 13 Jul 2025 19:22:24 -0400 Subject: [PATCH 003/148] feat: added auth0Id to support 2 methods of login/Hailia's feedback --- database/models/user.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/database/models/user.js b/database/models/user.js index 2ab73f9..f6b1f46 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -35,6 +35,11 @@ const User = db.define("user", { notEmpty: true, }, }, + auth0Id: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, img: { type: DataTypes.STRING, allowNull: true, From 8711ff07339138e78d95576262ffc047359a5501 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Sun, 13 Jul 2025 19:26:04 -0400 Subject: [PATCH 004/148] fix: changed passwordHash allowNull to true --- database/models/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/models/user.js b/database/models/user.js index f6b1f46..eba5ff0 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -30,7 +30,7 @@ const User = db.define("user", { }, passwordHash: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, validate: { notEmpty: true, }, From ca01929446f9002d408692828d692688d0663d5c Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sun, 13 Jul 2025 21:46:10 -0400 Subject: [PATCH 005/148] feat-define Poll model --- database/models/poll.js | 36 ++++++++++++++++++++++++++++++++++++ package-lock.json | 4 ++-- 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 database/models/poll.js diff --git a/database/models/poll.js b/database/models/poll.js new file mode 100644 index 0000000..6e89be6 --- /dev/null +++ b/database/models/poll.js @@ -0,0 +1,36 @@ +const {DataTypes} = require('sequelize'); +const db = require('./db'); + +// define the Poll model + +const Poll = db.define("poll", { + title: { + type: DataTypes.STRING + }, + description: { + type: DataTypes.TEXT + }, + participants: { + type: DataTypes.INTEGER + }, + status: { + type: DataTypes.STRING // draft, published , ended + }, + deadline: { + type: DataTypes.TIME + }, + authRequired: { + type: DataTypes.BOOLEAN, // allow only user votes if true + default: false + }, + isDisabled: { + type: DataTypes.BOOLEAN, // if true poll is disabled by admin + default: false + }, + restricted: { + type: DataTypes.BOOLEAN, // only specic users can parcipate if true + default: false + } +}); + +module.exports = Poll; \ No newline at end of file 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 22b77d4564e138754499b0c7b139739a62503c2f Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sun, 13 Jul 2025 23:17:14 -0400 Subject: [PATCH 006/148] feat-defined Vote model --- database/models/poll.js | 2 +- database/models/vote.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 database/models/vote.js diff --git a/database/models/poll.js b/database/models/poll.js index 6e89be6..731d6c4 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -1,5 +1,5 @@ const {DataTypes} = require('sequelize'); -const db = require('./db'); +const db = require('../db'); // define the Poll model diff --git a/database/models/vote.js b/database/models/vote.js new file mode 100644 index 0000000..644f195 --- /dev/null +++ b/database/models/vote.js @@ -0,0 +1,22 @@ +const {DataTypes} = require('sequelize'); +const db = require('../db'); + +// define the Vote model + +const Vote = db.define("vote", { + submitted: { + type: DataTypes.BOOLEAN, + defaultValue: false // if false; vote has not yet been submited; ballot can still be edited + }, + voterToken: { + type: DataTypes.STRING, + allowNull: true, // this allows us to uniquely identify a guest . we can track if they voted. + }, + ipAddress: { + type: DataTypes.STRING, + allowNull: true, // same as voter token we can use either + }, + timestamps: true +}) + +module.export = Vote; \ No newline at end of file From 4d92ca70ee8e11d25561342ca2aad07b5c437f08 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sun, 13 Jul 2025 23:23:09 -0400 Subject: [PATCH 007/148] fix-fixed model attributes --- database/models/poll.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/database/models/poll.js b/database/models/poll.js index 731d6c4..b0f102d 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -5,32 +5,41 @@ const db = require('../db'); const Poll = db.define("poll", { title: { - type: DataTypes.STRING + type: DataTypes.STRING, + allowNull: false, }, description: { - type: DataTypes.TEXT + type: DataTypes.TEXT, + allowNull: true, }, participants: { - type: DataTypes.INTEGER + type: DataTypes.INTEGER, + defaultValue: 0, }, status: { - type: DataTypes.STRING // draft, published , ended + type: DataTypes.ENUM, // draft, published , ended + allowNull: false, }, deadline: { - type: DataTypes.TIME + type: DataTypes.TIMESTAMP, + allowNull: false, }, authRequired: { type: DataTypes.BOOLEAN, // allow only user votes if true - default: false + default: false, + allowNull: false, }, isDisabled: { type: DataTypes.BOOLEAN, // if true poll is disabled by admin - default: false + default: false, + allowNull: false, }, restricted: { type: DataTypes.BOOLEAN, // only specic users can parcipate if true - default: false - } + default: false, + allowNull: false, + }, + timestamps: true }); module.exports = Poll; \ No newline at end of file From 25a36ace51f39969f17508fa418e307dc2d3c7bb Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sun, 13 Jul 2025 23:48:27 -0400 Subject: [PATCH 008/148] chore-exported poll model from index --- database/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database/index.js b/database/index.js index 34e8404..d6873b0 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,9 @@ const db = require("./db"); const User = require("./models/User"); +const Poll = require("./models/poll"); module.exports = { db, User, + Poll }; From 7e534ad958aca36c69f6ff105611e46346fb08c8 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Mon, 14 Jul 2025 00:06:43 -0400 Subject: [PATCH 009/148] feat-Poll seed data --- database/seed.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index e58b595..1fe1699 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,5 @@ const db = require("./db"); -const { User } = require("./index"); +const { User, Poll } = require("./index"); const seed = async () => { try { @@ -12,6 +12,59 @@ const seed = async () => { { username: "user2", passwordHash: User.hashPassword("user222") }, ]); + const poll = await Poll.bulkCreate([ + { + title: "Best Anime?", + description: "Rank your favorite animes!", + participants: 0, + status: "published", + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now + authRequired: false, + isDisabled: false, + restricted: false, + }, + { + title: "Best Movie?", + description: "Rank your favorite movies!", + participants: 0, + status: "published", + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now + authRequired: false, + isDisabled: false, + restricted: false, + }, + { + title: "Best BBQ Item?", + description: "Rank your favorite BBQ food!", + participants: 0, + status: "published", + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now + authRequired: false, + isDisabled: false, + restricted: false, + }, + { + title: "authRequired true", + description: "?", + participants: 0, + status: "published", + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now + authRequired: true, + isDisabled: false, + restricted: false, + }, + { + title: "restricted true", + description: "Rank your favorite anime of all time!", + participants: 0, + status: "published", + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now + authRequired: false, + isDisabled: false, + restricted: true, + }, + ]); + console.log(`๐Ÿ‘ค Created ${users.length} users`); // Create more seed data here once you've created your models From 8257073cb787cf3da6ab676b3332b8b80d81bcfd Mon Sep 17 00:00:00 2001 From: rend1027 Date: Mon, 14 Jul 2025 00:09:11 -0400 Subject: [PATCH 010/148] refactor-added console.log --- database/seed.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index 1fe1699..ce3fc60 100644 --- a/database/seed.js +++ b/database/seed.js @@ -12,7 +12,7 @@ const seed = async () => { { username: "user2", passwordHash: User.hashPassword("user222") }, ]); - const poll = await Poll.bulkCreate([ + const polls = await Poll.bulkCreate([ { title: "Best Anime?", description: "Rank your favorite animes!", @@ -66,6 +66,7 @@ const seed = async () => { ]); console.log(`๐Ÿ‘ค Created ${users.length} users`); + console.log(`Created ${polls.length} polls`) // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! From e56855918cab3c5ee4187046f23285aee578ae0c Mon Sep 17 00:00:00 2001 From: rend1027 Date: Mon, 14 Jul 2025 00:10:52 -0400 Subject: [PATCH 011/148] fix-User model import --- database/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/index.js b/database/index.js index d6873b0..d2c3118 100644 --- a/database/index.js +++ b/database/index.js @@ -1,5 +1,5 @@ const db = require("./db"); -const User = require("./models/User"); +const User = require("./models/user"); const Poll = require("./models/poll"); module.exports = { From da88cc7b501430c518aeae839eb364c7a6ebdafe Mon Sep 17 00:00:00 2001 From: rend1027 Date: Mon, 14 Jul 2025 00:20:20 -0400 Subject: [PATCH 012/148] refactor-cleaned poll data --- database/seed.js | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/database/seed.js b/database/seed.js index ce3fc60..f79ea62 100644 --- a/database/seed.js +++ b/database/seed.js @@ -18,49 +18,35 @@ const seed = async () => { description: "Rank your favorite animes!", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now - authRequired: false, - isDisabled: false, - restricted: false, + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { title: "Best Movie?", description: "Rank your favorite movies!", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now - authRequired: false, - isDisabled: false, - restricted: false, + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { title: "Best BBQ Item?", description: "Rank your favorite BBQ food!", - participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now - authRequired: false, - isDisabled: false, - restricted: false, + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { title: "authRequired true", description: "?", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), authRequired: true, - isDisabled: false, - restricted: false, }, { title: "restricted true", description: "Rank your favorite anime of all time!", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days from now - authRequired: false, - isDisabled: false, + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), restricted: true, }, ]); From 397296958e6439696b91222bb54748b6f5dcc4e3 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 11:02:47 -0400 Subject: [PATCH 013/148] feat- defined pollOption Model --- database/models/pollOption.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 database/models/pollOption.js diff --git a/database/models/pollOption.js b/database/models/pollOption.js new file mode 100644 index 0000000..a78d4c6 --- /dev/null +++ b/database/models/pollOption.js @@ -0,0 +1,24 @@ +const { DataTypes } = require('sequelize'); +const db = require('../db'); + +// Table Poll_Option { +// id PK +// option_text string +// position integer ("?") +// poll_id FK +// created_at timestamp +// } +const pollOption = db.define("poll", { + optionText: { + type: DataTypes.STRING, + allowNull: false, + }, + position: { + type: DataTypes.INTEGER, + allowNull: true, + }, + timestamps: true, + +}) + +module.exports = pollOption; \ No newline at end of file From 9b44adea8682f1abe7a3786a8c92c49eabc8d3d2 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 11:56:22 -0400 Subject: [PATCH 014/148] feat- created poll_option data --- database/index.js | 4 +- database/models/pollOption.js | 9 +++- database/seed.js | 94 +++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/database/index.js b/database/index.js index d2c3118..639205b 100644 --- a/database/index.js +++ b/database/index.js @@ -1,9 +1,11 @@ const db = require("./db"); const User = require("./models/user"); const Poll = require("./models/poll"); +const PollOption = require("./models/pollOption") module.exports = { db, User, - Poll + Poll, + PollOption, }; diff --git a/database/models/pollOption.js b/database/models/pollOption.js index a78d4c6..ce8743f 100644 --- a/database/models/pollOption.js +++ b/database/models/pollOption.js @@ -18,7 +18,14 @@ const pollOption = db.define("poll", { allowNull: true, }, timestamps: true, - + poll_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'poll', // refers to poll table + key: 'id' + } + } }) module.exports = pollOption; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index f79ea62..5d7a002 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,5 @@ const db = require("./db"); -const { User, Poll } = require("./index"); +const { User, Poll, PollOption } = require("./index"); const seed = async () => { try { @@ -12,6 +12,8 @@ const seed = async () => { { username: "user2", passwordHash: User.hashPassword("user222") }, ]); + + const polls = await Poll.bulkCreate([ { title: "Best Anime?", @@ -25,20 +27,20 @@ const seed = async () => { description: "Rank your favorite movies!", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { title: "Best BBQ Item?", description: "Rank your favorite BBQ food!", status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { title: "authRequired true", description: "?", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), authRequired: true, }, { @@ -47,10 +49,92 @@ const seed = async () => { participants: 0, status: "published", deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), - restricted: true, + restricted: true, }, ]); + + + const PollOptions = await PollOption.bulkCreate([ + { + optionText: "Demon Slayer", + position: 1, + poll_id: + }, + { + optionText: "One Piece", + position: 2, + }, + { + optionText: "AOT", + position: 3, + }, + { + optionText: "Naruto", + position: 4, + }, + { + optionText: "Devil May Cry", + position: 5, + }, + { + optionText: "Castlevania", + position: 6, + }, + { + optionText: "" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + { + optionText: "One Piece" + }, + + ]) + console.log(`๐Ÿ‘ค Created ${users.length} users`); console.log(`Created ${polls.length} polls`) From 432728a7ed1ea37d83a734c8f435fd753c933450 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 13:13:49 -0400 Subject: [PATCH 015/148] refactor- modified poll data to be more dinamic --- database/seed.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/database/seed.js b/database/seed.js index 5d7a002..b93f045 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,3 +1,4 @@ +const { Pool } = require("pg"); const db = require("./db"); const { User, Poll, PollOption } = require("./index"); @@ -12,46 +13,59 @@ const seed = async () => { { username: "user2", passwordHash: User.hashPassword("user222") }, ]); + // deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), - - const polls = await Poll.bulkCreate([ + const pollData = [ { + key: "anime", title: "Best Anime?", description: "Rank your favorite animes!", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + }, { + key: "movie", title: "Best Movie?", description: "Rank your favorite movies!", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { + key: "bbq", title: "Best BBQ Item?", description: "Rank your favorite BBQ food!", status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), }, { + key: "authRequired", title: "authRequired true", description: "?", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), authRequired: true, }, { + key: "restricited", title: "restricted true", description: "Rank your favorite anime of all time!", participants: 0, status: "published", - deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), restricted: true, }, - ]); + ]; + + const createdPoll = {}; + const deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); + + for (const poll of pollData) { + const created = await Poll.create({ + ...poll, + deadline, + user_id: someUser_id, + }) + createdPoll[poll.key] = created; + } @@ -59,7 +73,6 @@ const seed = async () => { { optionText: "Demon Slayer", position: 1, - poll_id: }, { optionText: "One Piece", From 15b7bf8d9f88c357a372463411a1811651edd74a Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 13:59:13 -0400 Subject: [PATCH 016/148] feat-created poll_option data --- database/seed.js | 66 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/database/seed.js b/database/seed.js index b93f045..137b1de 100644 --- a/database/seed.js +++ b/database/seed.js @@ -55,7 +55,7 @@ const seed = async () => { }, ]; - const createdPoll = {}; + const createdPolls = {}; const deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); for (const poll of pollData) { @@ -64,8 +64,9 @@ const seed = async () => { deadline, user_id: someUser_id, }) - createdPoll[poll.key] = created; - } + createdPolls[poll.key] = created; + console.log(createdPolls.anime.id) + }; @@ -73,77 +74,98 @@ const seed = async () => { { optionText: "Demon Slayer", position: 1, + poll_id: createdPolls.anime.id, }, { optionText: "One Piece", position: 2, + poll_id: createdPolls.anime.id, }, { optionText: "AOT", position: 3, + poll_id: createdPolls.anime.id, }, { optionText: "Naruto", position: 4, + poll_id: createdPolls.anime.id, }, { optionText: "Devil May Cry", position: 5, + poll_id: createdPolls.anime.id, }, { optionText: "Castlevania", position: 6, + poll_id: createdPolls.anime.id, }, { - optionText: "" - }, - { - optionText: "One Piece" + optionText: "Die Hard", + poll_id: createdPolls.movie.id }, { - optionText: "One Piece" + optionText: "Die Hard 2", + poll_id: createdPolls.movie.id, }, { - optionText: "One Piece" + optionText: "Twilight", + poll_id: createdPolls.movie.id, }, { - optionText: "One Piece" + optionText: "Spiderverse", + poll_id: createdPolls.movie.id, }, { - optionText: "One Piece" + optionText: "Pork Ribs", + poll_id: createdPolls.bbq.id, }, { - optionText: "One Piece" + optionText: "Hot Dog", + poll_id: createdPolls.bbq.id, }, { - optionText: "One Piece" + optionText: "Cheeseburger", + poll_id: createdPolls.bbq.id, }, { - optionText: "One Piece" + optionText: "Suasage", + poll_Id: createdPolls.bbq.id, }, { - optionText: "One Piece" + optionText: "a", + poll_id: createdPolls.authRequired.id, }, { - optionText: "One Piece" + optionText: "b", + poll_id: createdPolls.authRequired.id, }, { - optionText: "One Piece" + optionText: "c", + poll_id: createdPolls.authRequired.id, }, { - optionText: "One Piece" + optionText: "d", + poll_id: createdPolls.authRequired.id, }, { - optionText: "One Piece" + optionText: "1", + poll_id: createdPolls.restricited.id, }, { - optionText: "One Piece" + optionText: "2", + poll_id: createdPolls.restricited.id, }, { - optionText: "One Piece" + optionText: "3", + poll_id: createdPolls.restricited.id, + }, { - optionText: "One Piece" + optionText: "4", + poll_id: createdPolls.restricited.id, + }, ]) From 9f55ed87133a95f24de75dc841f77e8540dbe965 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 14:35:03 -0400 Subject: [PATCH 017/148] refactor- made poll_option data more dinamic --- database/seed.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/database/seed.js b/database/seed.js index 137b1de..56cd4e8 100644 --- a/database/seed.js +++ b/database/seed.js @@ -22,6 +22,7 @@ const seed = async () => { description: "Rank your favorite animes!", participants: 0, status: "published", + userkey: "user1", }, { @@ -30,12 +31,14 @@ const seed = async () => { description: "Rank your favorite movies!", participants: 0, status: "published", + userKey: "user2", }, { key: "bbq", title: "Best BBQ Item?", description: "Rank your favorite BBQ food!", status: "published", + userKey: "user1" }, { key: "authRequired", @@ -44,6 +47,8 @@ const seed = async () => { participants: 0, status: "published", authRequired: true, + userKey: "user2" + }, { key: "restricited", @@ -52,17 +57,25 @@ const seed = async () => { participants: 0, status: "published", restricted: true, + userKey: "user1" + }, ]; const createdPolls = {}; const deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); + const userMap = { + admin: users[0], + user1: users[1], + user2: users[2], + }; + for (const poll of pollData) { const created = await Poll.create({ ...poll, deadline, - user_id: someUser_id, + user_id: userMap[poll.userKey].id, }) createdPolls[poll.key] = created; console.log(createdPolls.anime.id) @@ -131,7 +144,7 @@ const seed = async () => { }, { optionText: "Suasage", - poll_Id: createdPolls.bbq.id, + poll_id: createdPolls.bbq.id, }, { optionText: "a", @@ -171,7 +184,7 @@ const seed = async () => { ]) console.log(`๐Ÿ‘ค Created ${users.length} users`); - console.log(`Created ${polls.length} polls`) + console.log(`Created ${createdPolls.length} polls`) // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! From e94a08ca49f3e706e6eb88c34e6dc6f936ab5c33 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 15:28:49 -0400 Subject: [PATCH 018/148] optimized user_model --- database/models/user.js | 16 ++++++++++++---- database/seed.js | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/database/models/user.js b/database/models/user.js index eba5ff0..906f89b 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -3,9 +3,17 @@ const db = require("../db"); const bcrypt = require("bcrypt"); const User = db.define("user", { + userName: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + validate: { + len: [1, 50] + } + }, firstName: { type: DataTypes.STRING, - allowNull: false, + // allowNull: false, validate: { notEmpty: true, len: [1, 50], @@ -13,7 +21,7 @@ const User = db.define("user", { }, lastName: { type: DataTypes.STRING, - allowNull: false, + // allowNull: false, validate: { notEmpty: true, len: [1, 50], @@ -21,7 +29,7 @@ const User = db.define("user", { }, email: { type: DataTypes.STRING, - allowNull: false, + // allowNull: false, unique: true, validate: { isEmail: true, @@ -60,7 +68,7 @@ const User = db.define("user", { }, { timestamps: true, createdAt: 'created_at', - updatedAt: false, + updatedAt: false, }); // Instance method to check password diff --git a/database/seed.js b/database/seed.js index 56cd4e8..e84c7f7 100644 --- a/database/seed.js +++ b/database/seed.js @@ -8,9 +8,18 @@ const seed = async () => { await db.sync({ force: true }); // Drop and recreate tables const users = await User.bulkCreate([ - { username: "admin", passwordHash: User.hashPassword("admin123") }, - { username: "user1", passwordHash: User.hashPassword("user111") }, - { username: "user2", passwordHash: User.hashPassword("user222") }, + { + username: "admin", + passwordHash: User.hashPassword("admin123"), + }, + { + username: "user1", + passwordHash: User.hashPassword("user111") + }, + { + username: "user2", + passwordHash: User.hashPassword("user222") + }, ]); // deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), @@ -22,7 +31,7 @@ const seed = async () => { description: "Rank your favorite animes!", participants: 0, status: "published", - userkey: "user1", + userKey: "user1", }, { @@ -184,7 +193,7 @@ const seed = async () => { ]) console.log(`๐Ÿ‘ค Created ${users.length} users`); - console.log(`Created ${createdPolls.length} polls`) + console.log(`Created ${Object.keys(createdPolls).length} polls`); // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! From 9b3c5a9ff69ad61eef125d45c2c339b2b3d018cc Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 20:30:24 -0400 Subject: [PATCH 019/148] feat- associations --- database/index.js | 25 ++++++++++++++++++++++++- database/models/poll.js | 12 ++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/database/index.js b/database/index.js index 639205b..eb37a68 100644 --- a/database/index.js +++ b/database/index.js @@ -1,8 +1,31 @@ const db = require("./db"); const User = require("./models/user"); const Poll = require("./models/poll"); -const PollOption = require("./models/pollOption") +const PollOption = require("./models/pollOption"); +const pollOption = require("./models/pollOption"); + +//One to many - user has many polls +User.hasMany(Poll, { + foreignKey: 'userId', + // onDelete: 'CASCADE' deletes poll is user is deleted +}); + +// One to one - Each Poll belongs to one user +Poll.belongsTo(User, { + foreignKey: 'userId', +}); + +// One to many - one Poll has many options +Poll.hasMany(PollOption, { + foreignKey: 'pollId', + onDelete: "CASCASDE", // delete poll_options if poll is deleted +}); + +// one to one - Each pollOption belongs to one Poll +PollOption.belongsTo(Poll, { + foreignKey: "pollId" +}) module.exports = { db, User, diff --git a/database/models/poll.js b/database/models/poll.js index b0f102d..f8bbbf0 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -1,4 +1,4 @@ -const {DataTypes} = require('sequelize'); +const { DataTypes } = require('sequelize'); const db = require('../db'); // define the Poll model @@ -21,7 +21,7 @@ const Poll = db.define("poll", { allowNull: false, }, deadline: { - type: DataTypes.TIMESTAMP, + type: DataTypes.DATE, allowNull: false, }, authRequired: { @@ -39,7 +39,11 @@ const Poll = db.define("poll", { default: false, allowNull: false, }, - timestamps: true -}); +}, + { + timestamps: true, + createdAt: "created at", + + }); module.exports = Poll; \ No newline at end of file From e1bcb9c545bdac569fc8b9c5380c115978f0b307 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 20:35:09 -0400 Subject: [PATCH 020/148] refactor- change DB name --- database/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/db.js b/database/db.js index b251a9d..bba1d5f 100644 --- a/database/db.js +++ b/database/db.js @@ -3,7 +3,7 @@ const { Sequelize } = require("sequelize"); const pg = require("pg"); // Feel free to rename the database to whatever you want! -const dbName = "capstone-1"; +const dbName = "capstone_1"; const db = new Sequelize( process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, From 2a3b0e8cf3a945e839043ec5e8f0dddbcd5de402 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 21:34:42 -0400 Subject: [PATCH 021/148] fixed-bugs with model attributes --- database/index.js | 4 +-- database/models/poll.js | 12 +++----- database/models/pollOption.js | 15 ++++------ database/models/user.js | 11 +++---- database/models/vote.js | 11 ++++--- database/seed.js | 54 +++++++++++++++++------------------ 6 files changed, 50 insertions(+), 57 deletions(-) diff --git a/database/index.js b/database/index.js index eb37a68..10624b5 100644 --- a/database/index.js +++ b/database/index.js @@ -2,8 +2,6 @@ const db = require("./db"); const User = require("./models/user"); const Poll = require("./models/poll"); const PollOption = require("./models/pollOption"); -const pollOption = require("./models/pollOption"); - //One to many - user has many polls User.hasMany(Poll, { @@ -19,7 +17,7 @@ Poll.belongsTo(User, { // One to many - one Poll has many options Poll.hasMany(PollOption, { foreignKey: 'pollId', - onDelete: "CASCASDE", // delete poll_options if poll is deleted + onDelete: "CASCADE", // delete poll_options if poll is deleted }); // one to one - Each pollOption belongs to one Poll diff --git a/database/models/poll.js b/database/models/poll.js index f8bbbf0..5031a1f 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -10,14 +10,14 @@ const Poll = db.define("poll", { }, description: { type: DataTypes.TEXT, - allowNull: true, + // allowNull: true, }, participants: { type: DataTypes.INTEGER, defaultValue: 0, }, status: { - type: DataTypes.ENUM, // draft, published , ended + type: DataTypes.ENUM("draft", "published", "ended"), allowNull: false, }, deadline: { @@ -27,23 +27,19 @@ const Poll = db.define("poll", { authRequired: { type: DataTypes.BOOLEAN, // allow only user votes if true default: false, - allowNull: false, }, isDisabled: { type: DataTypes.BOOLEAN, // if true poll is disabled by admin default: false, - allowNull: false, }, restricted: { type: DataTypes.BOOLEAN, // only specic users can parcipate if true default: false, - allowNull: false, }, }, { timestamps: true, - createdAt: "created at", - - }); + } +); module.exports = Poll; \ No newline at end of file diff --git a/database/models/pollOption.js b/database/models/pollOption.js index ce8743f..0e56250 100644 --- a/database/models/pollOption.js +++ b/database/models/pollOption.js @@ -8,7 +8,7 @@ const db = require('../db'); // poll_id FK // created_at timestamp // } -const pollOption = db.define("poll", { +const pollOption = db.define("pollOption", { optionText: { type: DataTypes.STRING, allowNull: false, @@ -17,15 +17,10 @@ const pollOption = db.define("poll", { type: DataTypes.INTEGER, allowNull: true, }, - timestamps: true, - poll_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: 'poll', // refers to poll table - key: 'id' - } +}, + { + timestamps: true, } -}) +) module.exports = pollOption; \ No newline at end of file diff --git a/database/models/user.js b/database/models/user.js index 906f89b..6e82a40 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -65,11 +65,12 @@ const User = db.define("user", { allowNull: false, defaultValue: false, }, -}, { - timestamps: true, - createdAt: 'created_at', - updatedAt: false, -}); +}, + { + timestamps: true, + createdAt: 'created_at', + } +); // Instance method to check password User.prototype.checkPassword = function (password) { diff --git a/database/models/vote.js b/database/models/vote.js index 644f195..a0f1695 100644 --- a/database/models/vote.js +++ b/database/models/vote.js @@ -1,4 +1,4 @@ -const {DataTypes} = require('sequelize'); +const { DataTypes } = require('sequelize'); const db = require('../db'); // define the Vote model @@ -16,7 +16,10 @@ const Vote = db.define("vote", { type: DataTypes.STRING, allowNull: true, // same as voter token we can use either }, - timestamps: true -}) +}, + { + timestamps: true, + } +); -module.export = Vote; \ No newline at end of file +module.exports = Vote; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index e84c7f7..e6ec5f1 100644 --- a/database/seed.js +++ b/database/seed.js @@ -9,15 +9,15 @@ const seed = async () => { const users = await User.bulkCreate([ { - username: "admin", + userName: "admin", passwordHash: User.hashPassword("admin123"), }, { - username: "user1", + userName: "user1", passwordHash: User.hashPassword("user111") }, { - username: "user2", + userName: "user2", passwordHash: User.hashPassword("user222") }, ]); @@ -72,7 +72,7 @@ const seed = async () => { ]; const createdPolls = {}; - const deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); + const deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); // 3days const userMap = { admin: users[0], @@ -87,7 +87,7 @@ const seed = async () => { user_id: userMap[poll.userKey].id, }) createdPolls[poll.key] = created; - console.log(createdPolls.anime.id) + // console.log(createdPolls.anime.id) }; @@ -96,97 +96,97 @@ const seed = async () => { { optionText: "Demon Slayer", position: 1, - poll_id: createdPolls.anime.id, + pollId: createdPolls.anime.id, }, { optionText: "One Piece", position: 2, - poll_id: createdPolls.anime.id, + pollId: createdPolls.anime.id, }, { optionText: "AOT", position: 3, - poll_id: createdPolls.anime.id, + pollId: createdPolls.anime.id, }, { optionText: "Naruto", position: 4, - poll_id: createdPolls.anime.id, + pollId: createdPolls.anime.id, }, { optionText: "Devil May Cry", position: 5, - poll_id: createdPolls.anime.id, + poll_Id: createdPolls.anime.id, }, { optionText: "Castlevania", position: 6, - poll_id: createdPolls.anime.id, + pollId: createdPolls.anime.id, }, { optionText: "Die Hard", - poll_id: createdPolls.movie.id + pollId: createdPolls.movie.id }, { optionText: "Die Hard 2", - poll_id: createdPolls.movie.id, + pollId: createdPolls.movie.id, }, { optionText: "Twilight", - poll_id: createdPolls.movie.id, + pollId: createdPolls.movie.id, }, { optionText: "Spiderverse", - poll_id: createdPolls.movie.id, + pollId: createdPolls.movie.id, }, { optionText: "Pork Ribs", - poll_id: createdPolls.bbq.id, + pollId: createdPolls.bbq.id, }, { optionText: "Hot Dog", - poll_id: createdPolls.bbq.id, + pollId: createdPolls.bbq.id, }, { optionText: "Cheeseburger", - poll_id: createdPolls.bbq.id, + pollId: createdPolls.bbq.id, }, { optionText: "Suasage", - poll_id: createdPolls.bbq.id, + pollId: createdPolls.bbq.id, }, { optionText: "a", - poll_id: createdPolls.authRequired.id, + pollId: createdPolls.authRequired.id, }, { optionText: "b", - poll_id: createdPolls.authRequired.id, + pollId: createdPolls.authRequired.id, }, { optionText: "c", - poll_id: createdPolls.authRequired.id, + pollId: createdPolls.authRequired.id, }, { optionText: "d", - poll_id: createdPolls.authRequired.id, + pollId: createdPolls.authRequired.id, }, { optionText: "1", - poll_id: createdPolls.restricited.id, + pollId: createdPolls.restricited.id, }, { optionText: "2", - poll_id: createdPolls.restricited.id, + pollId: createdPolls.restricited.id, }, { optionText: "3", - poll_id: createdPolls.restricited.id, + pollId: createdPolls.restricited.id, }, { optionText: "4", - poll_id: createdPolls.restricited.id, + pollId: createdPolls.restricited.id, }, From 697ceaa6854a9205b50695d8e29fc6a00dba066a Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 21:58:07 -0400 Subject: [PATCH 022/148] fixed-Poll FK --- database/seed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index e6ec5f1..2110406 100644 --- a/database/seed.js +++ b/database/seed.js @@ -84,7 +84,7 @@ const seed = async () => { const created = await Poll.create({ ...poll, deadline, - user_id: userMap[poll.userKey].id, + userId: userMap[poll.userKey].id, }) createdPolls[poll.key] = created; // console.log(createdPolls.anime.id) From adfb64596b2ecfd1e61df597b02bf2c88f378f0c Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 22:09:30 -0400 Subject: [PATCH 023/148] feat-votingRank model --- database/models/votingRank.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 database/models/votingRank.js diff --git a/database/models/votingRank.js b/database/models/votingRank.js new file mode 100644 index 0000000..e69de29 From a1ba0b463917682c6dec27230273926326272d73 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 23:23:59 -0400 Subject: [PATCH 024/148] defined VotingRank model --- database/index.js | 3 ++- database/models/votingRank.js | 13 +++++++++++++ database/seed.js | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/database/index.js b/database/index.js index 10624b5..9c1b570 100644 --- a/database/index.js +++ b/database/index.js @@ -2,7 +2,7 @@ const db = require("./db"); const User = require("./models/user"); const Poll = require("./models/poll"); const PollOption = require("./models/pollOption"); - +const VotingRank = require("./models/votingRank"); //One to many - user has many polls User.hasMany(Poll, { foreignKey: 'userId', @@ -29,4 +29,5 @@ module.exports = { User, Poll, PollOption, + VotingRank, }; diff --git a/database/models/votingRank.js b/database/models/votingRank.js index e69de29..05aa1b2 100644 --- a/database/models/votingRank.js +++ b/database/models/votingRank.js @@ -0,0 +1,13 @@ +const { DataTypes } = require("sequelize"); +const db = require("../db"); + +// define the votingRank model + +const VotingRank = db.define("votingRank", { + rank: { + type: DataTypes.INTEGER, + allowNull: false, + } +}) + +module.exports = VotingRank; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index 2110406..c789900 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,6 +1,6 @@ const { Pool } = require("pg"); const db = require("./db"); -const { User, Poll, PollOption } = require("./index"); +const { User, Poll, PollOption, VotingRank } = require("./index"); const seed = async () => { try { From 7c4cc319cfe76847e313467e363f1194705afbca Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 00:56:46 -0400 Subject: [PATCH 025/148] feat- association --- database/index.js | 54 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/database/index.js b/database/index.js index 9c1b570..c91f4ef 100644 --- a/database/index.js +++ b/database/index.js @@ -3,31 +3,81 @@ const User = require("./models/user"); const Poll = require("./models/poll"); const PollOption = require("./models/pollOption"); const VotingRank = require("./models/votingRank"); +const Vote = require("./models/vote"); + //One to many - user has many polls User.hasMany(Poll, { foreignKey: 'userId', // onDelete: 'CASCADE' deletes poll is user is deleted }); -// One to one - Each Poll belongs to one user + +// many to one - Each Poll belongs to one user Poll.belongsTo(User, { foreignKey: 'userId', }); + // One to many - one Poll has many options Poll.hasMany(PollOption, { foreignKey: 'pollId', onDelete: "CASCADE", // delete poll_options if poll is deleted }); -// one to one - Each pollOption belongs to one Poll + +// many to one - Each pollOption belongs to one Poll PollOption.belongsTo(Poll, { foreignKey: "pollId" +}); + + +// one to many- each user can submit many votes +User.hasMany(Vote, { + foreignKey: "userId", +}) + + +// one to one- Each vote(ballot) belongs to a user +Vote.belongsTo(User, { + foreignKey: "userId", +}) + + +// many to one - Each vote(ballot) belongs to a poll +Vote.belongsTo(Poll, { + foreignKey: "pollId" }) + + +// one to many- each vote(ballot) can have many ranked options +Vote.hasMany(VotingRank, { + foreignKey: "voteRankId", +}) + + +//one to many- A vote(ballot) can have many ranked options +VotingRank.belongsTo(Vote, { + foreignKey: 'voteId', +}); + + +// many to one - each rank entry belongs to one vote(ballot) +VotingRank.belongsTo(Vote, { + foreignKey: "voteId", +}) + + +// many to one- Each voteRank belongs to one Polloption +VotingRank.belongsTo(PollOption, { + foreignKey: 'pollOptionId', +}) + + module.exports = { db, User, Poll, PollOption, + Vote, VotingRank, }; From 85544fb80e614291b11a4300c74d2b98f535efbe Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 11:09:46 -0400 Subject: [PATCH 026/148] feat- created Vote data --- database/seed.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/database/seed.js b/database/seed.js index c789900..1517bd3 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,6 +1,7 @@ const { Pool } = require("pg"); const db = require("./db"); -const { User, Poll, PollOption, VotingRank } = require("./index"); +const { User, Poll, PollOption, Vote, VotingRank } = require("./index"); +const pollOption = require("./models/pollOption"); const seed = async () => { try { @@ -190,11 +191,38 @@ const seed = async () => { }, + ]); + + + + const votes = await Vote.bulkCreate([ + { + userId: users[1].id, + pollId: createdPolls.anime.id + }, + { + userId: users[2].id, + pollId: createdPolls.movie.id + }, + { + userId: users[1].id, + pollId: createdPolls.bbq.id + }, + { + userId: users[2].id, + pollId: createdPolls.authRequired.id + }, + { + userId: users[1].id, + pollId: createdPolls.restricited.id + }, ]) + + console.log(`๐Ÿ‘ค Created ${users.length} users`); console.log(`Created ${Object.keys(createdPolls).length} polls`); - + console.log(`Created ${pollOption.length} poll options`); // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! From f252e90ead17a78be3d51af88e7fbda8f587c7e3 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 11:38:55 -0400 Subject: [PATCH 027/148] feat- created voteRank data --- database/seed.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index 1517bd3..e8a2c06 100644 --- a/database/seed.js +++ b/database/seed.js @@ -194,7 +194,7 @@ const seed = async () => { ]); - + // vote ---> envelope const votes = await Vote.bulkCreate([ { userId: users[1].id, @@ -218,6 +218,44 @@ const seed = async () => { }, ]) + const optionMap = {}; + PollOptions.forEach((option) => { + optionMap[option.optionText] = option; + }); + + const ranks = await VotingRank.bulkCreate([ + { + voteId: votes[0].id, + pollOptionId: optionMap["Demon Slayer"].id, + rank: 1, + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["OnePiece"].id, + rank: 3, + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["AOT"].id, + rank: 4, + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["Devil May Cry"].id, + rank: 6 + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["Castlevania"].id, + rank: 5 + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["Naruto"].id, + rank: 2 + }, + ]) + console.log(`๐Ÿ‘ค Created ${users.length} users`); From bdc266cfe0c4167fa35392ec850edf3a0ad31355 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 11:52:22 -0400 Subject: [PATCH 028/148] fixed- bug in rank data --- database/seed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index e8a2c06..6a5a272 100644 --- a/database/seed.js +++ b/database/seed.js @@ -231,7 +231,7 @@ const seed = async () => { }, { voteId: votes[0].id, - pollOptionId: optionMap["OnePiece"].id, + pollOptionId: optionMap["One Piece"].id, rank: 3, }, { From fd521c7cae7cb05766149e39f4cbdbeb08a6e1d0 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Tue, 15 Jul 2025 11:59:20 -0400 Subject: [PATCH 029/148] add username-based signup endpoint with validation --- auth/index.js | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/auth/index.js b/auth/index.js index 07968c5..5e9b33c 100644 --- a/auth/index.js +++ b/auth/index.js @@ -1,6 +1,7 @@ const express = require("express"); const jwt = require("jsonwebtoken"); const { User } = require("../database"); +const { Op } = require("sequelize"); const router = express.Router(); @@ -156,6 +157,92 @@ router.post("/signup", async (req, res) => { res.sendStatus(500); } }); +// Helper function to check if input looks like email +const looksLikeEmail = (input) => { + return input.includes("@"); +}; + +// Signup with username and password +router.post("/signup/username", async (req, res) => { + try { + const { username, password, firstName, lastName } = req.body; + + if (!username || !password) { + return res + .status(400) + .send({ error: "Username and password are required" }); + } + + if (password.length < 6) { + return res + .status(400) + .send({ error: "Password must be at least 6 characters long" }); + } + + // Check if input looks like email + if (looksLikeEmail(username)) { + return res + .status(400) + .send({ + error: + "Username cannot be an email address. Please use a regular username.", + }); + } + + // Check if username already exists (check both userName and email fields to prevent duplicates) + const existingUser = await User.findOne({ + where: { + [Op.or]: [{ userName: username }, { email: username }], + }, + }); + + if (existingUser) { + return res.status(409).send({ error: "Username already exists" }); + } + + // Create new user + const passwordHash = User.hashPassword(password); + const userData = { + userName: username, + passwordHash, + firstName: firstName || null, + lastName: lastName || null, + }; + + const user = await User.create(userData); + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + userName: user.userName, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.send({ + message: "User created successfully with username", + user: { + id: user.id, + userName: user.userName, + firstName: user.firstName, + lastName: user.lastName, + }, + }); + } catch (error) { + console.error("Username signup error:", error); + res.status(500).send({ error: "Internal server error" }); + } +}); // Login route router.post("/login", async (req, res) => { From eca4f0db85f51b8603879eea112140091fc90a1b Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 12:28:09 -0400 Subject: [PATCH 030/148] feat- added attributes to VotingRank --- database/models/votingRank.js | 8 ++++++++ database/seed.js | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/database/models/votingRank.js b/database/models/votingRank.js index 05aa1b2..d5a03c2 100644 --- a/database/models/votingRank.js +++ b/database/models/votingRank.js @@ -4,6 +4,14 @@ const db = require("../db"); // define the votingRank model const VotingRank = db.define("votingRank", { + voteId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + pollOptionId: { + type: DataTypes.INTEGER, + allowNull: false, + }, rank: { type: DataTypes.INTEGER, allowNull: false, diff --git a/database/seed.js b/database/seed.js index 6a5a272..160746a 100644 --- a/database/seed.js +++ b/database/seed.js @@ -117,7 +117,7 @@ const seed = async () => { { optionText: "Devil May Cry", position: 5, - poll_Id: createdPolls.anime.id, + pollId: createdPolls.anime.id, }, { optionText: "Castlevania", @@ -218,6 +218,7 @@ const seed = async () => { }, ]) + const optionMap = {}; PollOptions.forEach((option) => { optionMap[option.optionText] = option; @@ -254,13 +255,13 @@ const seed = async () => { pollOptionId: optionMap["Naruto"].id, rank: 2 }, - ]) + ]); console.log(`๐Ÿ‘ค Created ${users.length} users`); console.log(`Created ${Object.keys(createdPolls).length} polls`); - console.log(`Created ${pollOption.length} poll options`); + console.log(`๐Ÿงพ Created ${PollOptions.length} poll options`); // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! From b5b23bd567a589b5fa6abea59c4a1ccbbc89a887 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 12:34:57 -0400 Subject: [PATCH 031/148] feat- added attr to Vote model --- database/models/vote.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/database/models/vote.js b/database/models/vote.js index a0f1695..e078ef9 100644 --- a/database/models/vote.js +++ b/database/models/vote.js @@ -6,20 +6,26 @@ const db = require('../db'); const Vote = db.define("vote", { submitted: { type: DataTypes.BOOLEAN, - defaultValue: false // if false; vote has not yet been submited; ballot can still be edited + defaultValue: false, }, voterToken: { type: DataTypes.STRING, - allowNull: true, // this allows us to uniquely identify a guest . we can track if they voted. + allowNull: true, }, ipAddress: { type: DataTypes.STRING, - allowNull: true, // same as voter token we can use either + allowNull: true, }, -}, - { - timestamps: true, - } -); + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + pollId: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}, { + timestamps: true, +}); module.exports = Vote; \ No newline at end of file From 4e9d505719630b1a9c8bc116fd0956b6d5c868de Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 13:15:57 -0400 Subject: [PATCH 032/148] fixed- association bugs --- database/index.js | 8 +------- database/seed.js | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/database/index.js b/database/index.js index c91f4ef..20775f3 100644 --- a/database/index.js +++ b/database/index.js @@ -51,16 +51,10 @@ Vote.belongsTo(Poll, { // one to many- each vote(ballot) can have many ranked options Vote.hasMany(VotingRank, { - foreignKey: "voteRankId", + foreignKey: "voteId", }) -//one to many- A vote(ballot) can have many ranked options -VotingRank.belongsTo(Vote, { - foreignKey: 'voteId', -}); - - // many to one - each rank entry belongs to one vote(ballot) VotingRank.belongsTo(Vote, { foreignKey: "voteId", diff --git a/database/seed.js b/database/seed.js index 160746a..661f859 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,7 +1,6 @@ const { Pool } = require("pg"); const db = require("./db"); const { User, Poll, PollOption, Vote, VotingRank } = require("./index"); -const pollOption = require("./models/pollOption"); const seed = async () => { try { From ced402c196099411f762881fd15c8b6ed23dfde8 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 13:43:50 -0400 Subject: [PATCH 033/148] create- restricted_poll_access model --- database/restrictedPollAccess.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 database/restrictedPollAccess.js diff --git a/database/restrictedPollAccess.js b/database/restrictedPollAccess.js new file mode 100644 index 0000000..e69de29 From c0cbdc9b7a599c386b162368d04dcc4e4310640b Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Tue, 15 Jul 2025 13:46:04 -0400 Subject: [PATCH 034/148] : feat: email-based sign up and added validations --- auth/index.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/auth/index.js b/auth/index.js index 5e9b33c..ff1d91d 100644 --- a/auth/index.js +++ b/auth/index.js @@ -244,6 +244,88 @@ router.post("/signup/username", async (req, res) => { } }); +// Helper function to validate email format +const isValidEmail = (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +// Signup with email and password +router.post("/signup/email", async (req, res) => { + try { + const { email, password, firstName, lastName } = req.body; + + if (!email || !password) { + return res.status(400).send({ error: "Email and password are required" }); + } + + if (password.length < 6) { + return res.status(400).send({ error: "Password must be at least 6 characters long" }); + } + + // Validate email format + if (!isValidEmail(email)) { + return res.status(400).send({ error: "Please provide a valid email address" }); + } + + // Check if email already exists (check both email and userName fields to prevent duplicates) + const existingUser = await User.findOne({ + where: { + [Op.or]: [ + { email: email }, + { userName: email } + ] + } + }); + + if (existingUser) { + return res.status(409).send({ error: "Email already exists" }); + } + + // Create new user + const passwordHash = User.hashPassword(password); + const userData = { + email: email, + passwordHash, + firstName: firstName || null, + lastName: lastName || null + }; + + const user = await User.create(userData); + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + userName: user.userName, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.send({ + message: "User created successfully with email", + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }, + }); + } catch (error) { + console.error("Email signup error:", error); + res.status(500).send({ error: "Internal server error" }); + } +}); + // Login route router.post("/login", async (req, res) => { try { From 76f36fa361fc2bef6cb471b14d2c04275a47d5e9 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 14:08:06 -0400 Subject: [PATCH 035/148] feat- defined restricted_poll_access model --- database/index.js | 2 ++ database/models/restrictedPollAccess.js | 25 +++++++++++++++++++++++++ database/restrictedPollAccess.js | 0 3 files changed, 27 insertions(+) create mode 100644 database/models/restrictedPollAccess.js delete mode 100644 database/restrictedPollAccess.js diff --git a/database/index.js b/database/index.js index 20775f3..aa9416d 100644 --- a/database/index.js +++ b/database/index.js @@ -4,6 +4,7 @@ const Poll = require("./models/poll"); const PollOption = require("./models/pollOption"); const VotingRank = require("./models/votingRank"); const Vote = require("./models/vote"); +const RestrictedPollAccess = require('./models/restrictedPollAccess') //One to many - user has many polls User.hasMany(Poll, { @@ -74,4 +75,5 @@ module.exports = { PollOption, Vote, VotingRank, + RestrictedPollAccess, }; diff --git a/database/models/restrictedPollAccess.js b/database/models/restrictedPollAccess.js new file mode 100644 index 0000000..3c70587 --- /dev/null +++ b/database/models/restrictedPollAccess.js @@ -0,0 +1,25 @@ +const { DataTypes } = require("sequelize"); +const db = require("../db"); + +// Table Restricted_Poll_Access { +// id PK +// poll_id FK [ref: > Poll.id] +// user_id FK [ref: > User.id] +// created_at timestamp +// } +const RestrictedPollAccess = db.define("restrictedPollAccess", { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + pollId: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}, + { + timestamps: true, + } +) + +module.exports = RestrictedPollAccess; \ No newline at end of file diff --git a/database/restrictedPollAccess.js b/database/restrictedPollAccess.js deleted file mode 100644 index e69de29..0000000 From 0d38a23290c1364c412c17105463784e058a3949 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 14:20:59 -0400 Subject: [PATCH 036/148] chore: credentials and callback url added to .env --- package-lock.json | 41 +++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 42 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1b30289..2bf87b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@react-oauth/google": "^0.12.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -26,6 +27,16 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@react-oauth/google": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz", + "integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1336,6 +1347,29 @@ "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/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1397,6 +1431,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", diff --git a/package.json b/package.json index 7e0a0af..e346ee0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "license": "ISC", "description": "", "dependencies": { + "@react-oauth/google": "^0.12.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", From d6dcc18ebedcd641716d0edcfd2c77f5a3b46f05 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 14:35:30 -0400 Subject: [PATCH 037/148] feat- defined associations for restricted --- database/index.js | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/database/index.js b/database/index.js index aa9416d..f046fab 100644 --- a/database/index.js +++ b/database/index.js @@ -6,6 +6,8 @@ const VotingRank = require("./models/votingRank"); const Vote = require("./models/vote"); const RestrictedPollAccess = require('./models/restrictedPollAccess') +//----- User Model-------- + //One to many - user has many polls User.hasMany(Poll, { foreignKey: 'userId', @@ -13,43 +15,52 @@ User.hasMany(Poll, { }); + +//----- Poll Model-------- + // many to one - Each Poll belongs to one user Poll.belongsTo(User, { foreignKey: 'userId', }); + +//----- PollOption Model-------- + // One to many - one Poll has many options Poll.hasMany(PollOption, { foreignKey: 'pollId', onDelete: "CASCADE", // delete poll_options if poll is deleted }); - // many to one - Each pollOption belongs to one Poll PollOption.belongsTo(Poll, { foreignKey: "pollId" }); + +//----- Vote Model-------- + // one to many- each user can submit many votes User.hasMany(Vote, { foreignKey: "userId", }) - // one to one- Each vote(ballot) belongs to a user Vote.belongsTo(User, { foreignKey: "userId", }) - // many to one - Each vote(ballot) belongs to a poll Vote.belongsTo(Poll, { foreignKey: "pollId" }) + +//----- Voting Rank Model-------- + // one to many- each vote(ballot) can have many ranked options Vote.hasMany(VotingRank, { foreignKey: "voteId", @@ -68,6 +79,27 @@ VotingRank.belongsTo(PollOption, { }) + +//----- Restricted Access Model-------- + +//A user can be restricted to many polls +User.belongsToMany(Poll, { + through: RestrictedPollAccess, + as: "restrictedPolls", + foreignKey: "userId" +}) + +// A poll can belong to many users through Restrictred access +Poll.belongsToMany(User, { + through: RestrictedPollAccess, + as: "restictedUsers", + foreignKey: "pollId", +}) + +// + + + module.exports = { db, User, From 810c2f0e65e35108c97c1f9233776897db4ec4f2 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 15:54:11 -0400 Subject: [PATCH 038/148] feat(api)-poll router --- api/index.js | 2 ++ api/poll.js | 0 2 files changed, 2 insertions(+) create mode 100644 api/poll.js diff --git a/api/index.js b/api/index.js index f08162e..7e96c0d 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,9 @@ const express = require("express"); const router = express.Router(); +const pollRouter = express.Router(); const testDbRouter = require("./test-db"); router.use("/test-db", testDbRouter); +router.use("/poll", pollRouter) module.exports = router; diff --git a/api/poll.js b/api/poll.js new file mode 100644 index 0000000..e69de29 From c5d2a0213dc57243c88be32222c467451ff52b3a Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Tue, 15 Jul 2025 20:00:31 -0400 Subject: [PATCH 039/148] fix(auth): update signup/login routes for flexible input and model consistency --- auth/index.js | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/auth/index.js b/auth/index.js index ff1d91d..47dfd1a 100644 --- a/auth/index.js +++ b/auth/index.js @@ -120,21 +120,20 @@ router.post("/signup", async (req, res) => { } // Check if user already exists - const existingUser = await User.findOne({ where: { username } }); + const existingUser = await User.findOne({ where: { userName: username } }); if (existingUser) { return res.status(409).send({ error: "Username already exists" }); } // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ username, passwordHash }); + const user = await User.create({ userName: username, passwordHash }); // Generate JWT token const token = jwt.sign( { id: user.id, - username: user.username, - auth0Id: user.auth0Id, + userName: user.userName, email: user.email, }, JWT_SECRET, @@ -143,18 +142,18 @@ 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 }, }); } catch (error) { console.error("Signup error:", error); - res.sendStatus(500); + res.status(500).send({ error: "Internal server error" }); } }); // Helper function to check if input looks like email @@ -268,7 +267,7 @@ router.post("/signup/email", async (req, res) => { return res.status(400).send({ error: "Please provide a valid email address" }); } - // Check if email already exists (check both email and userName fields to prevent duplicates) + // Check if email already exists (check both email and userName fields to prevennt ani duplicates) const existingUser = await User.findOne({ where: { [Op.or]: [ @@ -332,13 +331,19 @@ router.post("/login", async (req, res) => { const { username, password } = req.body; if (!username || !password) { - res.status(400).send({ error: "Username and password are required" }); - return; + return res.status(400).send({ error: "Username and password are required" }); } - // Find user - const user = await User.findOne({ where: { username } }); - user.checkPassword(password); + // Find user by username or email + const user = await User.findOne({ + where: { + [Op.or]: [ + { userName: username }, + { email: username } + ] + } + }); + if (!user) { return res.status(401).send({ error: "Invalid credentials" }); } @@ -352,8 +357,7 @@ router.post("/login", async (req, res) => { const token = jwt.sign( { id: user.id, - username: user.username, - auth0Id: user.auth0Id, + userName: user.userName, email: user.email, }, JWT_SECRET, @@ -369,11 +373,17 @@ router.post("/login", async (req, res) => { res.send({ message: "Login successful", - user: { id: user.id, username: user.username }, + user: { + id: user.id, + userName: user.userName, + email: user.email, + firstName: user.firstName, + lastName: user.lastName + }, }); } catch (error) { console.error("Login error:", error); - res.sendStatus(500); + res.status(500).send({ error: "Internal server error" }); } }); From 9f32d936201ffc48589395943143cf0d348230e8 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 20:24:13 -0400 Subject: [PATCH 040/148] chore: routes/auth created --- auth/index.js | 1 + auth/routes.js | 0 2 files changed, 1 insertion(+) create mode 100644 auth/routes.js diff --git a/auth/index.js b/auth/index.js index 07968c5..da92618 100644 --- a/auth/index.js +++ b/auth/index.js @@ -230,4 +230,5 @@ router.get("/me", (req, res) => { }); }); + module.exports = { router, authenticateJWT }; diff --git a/auth/routes.js b/auth/routes.js new file mode 100644 index 0000000..e69de29 From d70e8644ff73654faa90b4c9b6c864f812bb4087 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 21:57:33 -0400 Subject: [PATCH 041/148] feat: defined google strategy --- auth/routes.js | 55 ++++++++++++++++ package-lock.json | 157 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + 3 files changed, 215 insertions(+) diff --git a/auth/routes.js b/auth/routes.js index e69de29..58fd864 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -0,0 +1,55 @@ +const express = require('express'); +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const { User } = require("../database"); +require('../passport'); + +const router = express.Router(); + +passport.use(new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL + }, async (accessToken, refreshToken, profile, done) => { + try { + const user = User.findOne({where: {googleId: profile.id}}); + + if (!user) { + // Try by email + const email = profile.emails?.[0]?.value; + if (email) { + user = await User.findOne({ where: { email } }); + + if (user) { + user.googleId = profile.id; + await user.save(); + } + } + } + + if (!user) { + // Create new user + const email = profile.emails?.[0]?.value || null; + const username = email ? email.split('@')[0] : `user_${Date.now()}`; + + // Ensure username is unique + const finalUsername = username; + const counter = 1; + while (await User.findOne({ where: { username: finalUsername } })) { + finalUsername = `${username}_${counter}`; + counter++; + } + + user = await User.create({ + googleId: profile.id, + email, + username: finalUsername, + passwordHash: null, + }); + } + + return done(null, profile); + } catch (error) { + return done(err, null); + } + })); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2bf87b8..a8d821b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,11 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-session": "^1.18.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "pg": "^8.16.2", "sequelize": "^6.37.7" }, @@ -101,6 +104,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -506,6 +518,46 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1078,6 +1130,12 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1138,6 +1196,64 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -1147,6 +1263,11 @@ "node": ">=16" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg": { "version": "8.16.2", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz", @@ -1323,6 +1444,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1732,6 +1862,24 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1754,6 +1902,15 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/package.json b/package.json index e346ee0..cc6c7b5 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,11 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-session": "^1.18.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "pg": "^8.16.2", "sequelize": "^6.37.7" }, From 02e2f89fad33c9c96edf9ee5f0c89ab7603ee34f Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 22:09:21 -0400 Subject: [PATCH 042/148] feat: GET /auth/google route added --- auth/routes.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/auth/routes.js b/auth/routes.js index 58fd864..a0fcec2 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -52,4 +52,9 @@ passport.use(new GoogleStrategy({ } catch (error) { return done(err, null); } + })); + + //OAuth flow + router.get("/auth/google", passport.authenticate("google", { + scope: ["profile", "email"] })); \ No newline at end of file From 86b55209b5f35029fc9e1c77b55e92378314c79d Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 22:35:38 -0400 Subject: [PATCH 043/148] feat: handle callback route + Issue JWT token and redirect to frontend --- auth/routes.js | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/auth/routes.js b/auth/routes.js index a0fcec2..727c2e1 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -6,13 +6,15 @@ require('../passport'); const router = express.Router(); +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + passport.use(new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: process.env.GOOGLE_CALLBACK_URL }, async (accessToken, refreshToken, profile, done) => { try { - const user = User.findOne({where: {googleId: profile.id}}); + const user = await User.findOne({where: {googleId: profile.id}}); if (!user) { // Try by email @@ -33,8 +35,8 @@ passport.use(new GoogleStrategy({ const username = email ? email.split('@')[0] : `user_${Date.now()}`; // Ensure username is unique - const finalUsername = username; - const counter = 1; + let finalUsername = username; + let counter = 1; while (await User.findOne({ where: { username: finalUsername } })) { finalUsername = `${username}_${counter}`; counter++; @@ -48,7 +50,7 @@ passport.use(new GoogleStrategy({ }); } - return done(null, profile); + return done(null, user); } catch (error) { return done(err, null); } @@ -57,4 +59,39 @@ passport.use(new GoogleStrategy({ //OAuth flow router.get("/auth/google", passport.authenticate("google", { scope: ["profile", "email"] - })); \ No newline at end of file + })); + + //Handle callback + router.get("/auth/google/callback", passport.authenticate("google", {session: false}), + async (req,res) => { + try { + const user = req.user; + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + username: user.username, + auth0Id: user.auth0Id, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.redirect(`${process.env.FRONTEND_URL}/dashboard`); + } catch (error) { + res.redirect(`${process.env.FRONTEND_URL}/login?error=google&message=Google%20login%20failed`); + + } + + }); + + module.exports = router; \ No newline at end of file From 69ec872ece7c9d7b875cd6523677c2d010664e7c Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 22:46:28 -0400 Subject: [PATCH 044/148] chore: google router mounted --- app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.js b/app.js index 5857036..c2504ce 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,7 @@ const apiRouter = require("./api"); const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); +const oAuthRouter = require("./auth/routes") const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -30,6 +31,7 @@ app.use(morgan("dev")); // logging middleware app.use(express.static(path.join(__dirname, "public"))); // serve static files from public folder app.use("/api", apiRouter); // mount api router app.use("/auth", authRouter); // mount auth router +app.use("/auth", oAuthRouter); //mount oAuth router // error handling middleware app.use((err, req, res, next) => { From 1a3feeafffb1988fa9a758b5b8f0c79234e63358 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Wed, 16 Jul 2025 00:54:53 -0400 Subject: [PATCH 045/148] refactor: added require jwt + changed const user to let + err->error/Tran's feedback --- auth/routes.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/auth/routes.js b/auth/routes.js index 727c2e1..f720e65 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -1,8 +1,9 @@ const express = require('express'); +const jwt = require('jsonwebtoken'); const passport = require('passport'); const GoogleStrategy = require('passport-google-oauth20').Strategy; const { User } = require("../database"); -require('../passport'); + const router = express.Router(); @@ -14,7 +15,7 @@ passport.use(new GoogleStrategy({ callbackURL: process.env.GOOGLE_CALLBACK_URL }, async (accessToken, refreshToken, profile, done) => { try { - const user = await User.findOne({where: {googleId: profile.id}}); + let user = await User.findOne({where: {googleId: profile.id}}); if (!user) { // Try by email @@ -52,7 +53,7 @@ passport.use(new GoogleStrategy({ return done(null, user); } catch (error) { - return done(err, null); + return done(error, null); } })); @@ -65,7 +66,7 @@ passport.use(new GoogleStrategy({ router.get("/auth/google/callback", passport.authenticate("google", {session: false}), async (req,res) => { try { - const user = req.user; + let user = req.user; // Generate JWT token const token = jwt.sign( From b652f8ee6c00d1c9fed452b9148b529efe6343ef Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 22:09:30 -0400 Subject: [PATCH 046/148] feat-votingRank model --- database/models/votingRank.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 database/models/votingRank.js diff --git a/database/models/votingRank.js b/database/models/votingRank.js new file mode 100644 index 0000000..e69de29 From ba747124d702571b4051e3cb6979c75f65f30489 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Mon, 14 Jul 2025 23:23:59 -0400 Subject: [PATCH 047/148] defined VotingRank model --- database/index.js | 3 ++- database/models/votingRank.js | 13 +++++++++++++ database/seed.js | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/database/index.js b/database/index.js index 10624b5..9c1b570 100644 --- a/database/index.js +++ b/database/index.js @@ -2,7 +2,7 @@ const db = require("./db"); const User = require("./models/user"); const Poll = require("./models/poll"); const PollOption = require("./models/pollOption"); - +const VotingRank = require("./models/votingRank"); //One to many - user has many polls User.hasMany(Poll, { foreignKey: 'userId', @@ -29,4 +29,5 @@ module.exports = { User, Poll, PollOption, + VotingRank, }; diff --git a/database/models/votingRank.js b/database/models/votingRank.js index e69de29..05aa1b2 100644 --- a/database/models/votingRank.js +++ b/database/models/votingRank.js @@ -0,0 +1,13 @@ +const { DataTypes } = require("sequelize"); +const db = require("../db"); + +// define the votingRank model + +const VotingRank = db.define("votingRank", { + rank: { + type: DataTypes.INTEGER, + allowNull: false, + } +}) + +module.exports = VotingRank; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index 2110406..c789900 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,6 +1,6 @@ const { Pool } = require("pg"); const db = require("./db"); -const { User, Poll, PollOption } = require("./index"); +const { User, Poll, PollOption, VotingRank } = require("./index"); const seed = async () => { try { From f25f397cd394a207b29115b0b12df34627f88b37 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 00:56:46 -0400 Subject: [PATCH 048/148] feat- association --- database/index.js | 54 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/database/index.js b/database/index.js index 9c1b570..c91f4ef 100644 --- a/database/index.js +++ b/database/index.js @@ -3,31 +3,81 @@ const User = require("./models/user"); const Poll = require("./models/poll"); const PollOption = require("./models/pollOption"); const VotingRank = require("./models/votingRank"); +const Vote = require("./models/vote"); + //One to many - user has many polls User.hasMany(Poll, { foreignKey: 'userId', // onDelete: 'CASCADE' deletes poll is user is deleted }); -// One to one - Each Poll belongs to one user + +// many to one - Each Poll belongs to one user Poll.belongsTo(User, { foreignKey: 'userId', }); + // One to many - one Poll has many options Poll.hasMany(PollOption, { foreignKey: 'pollId', onDelete: "CASCADE", // delete poll_options if poll is deleted }); -// one to one - Each pollOption belongs to one Poll + +// many to one - Each pollOption belongs to one Poll PollOption.belongsTo(Poll, { foreignKey: "pollId" +}); + + +// one to many- each user can submit many votes +User.hasMany(Vote, { + foreignKey: "userId", +}) + + +// one to one- Each vote(ballot) belongs to a user +Vote.belongsTo(User, { + foreignKey: "userId", +}) + + +// many to one - Each vote(ballot) belongs to a poll +Vote.belongsTo(Poll, { + foreignKey: "pollId" }) + + +// one to many- each vote(ballot) can have many ranked options +Vote.hasMany(VotingRank, { + foreignKey: "voteRankId", +}) + + +//one to many- A vote(ballot) can have many ranked options +VotingRank.belongsTo(Vote, { + foreignKey: 'voteId', +}); + + +// many to one - each rank entry belongs to one vote(ballot) +VotingRank.belongsTo(Vote, { + foreignKey: "voteId", +}) + + +// many to one- Each voteRank belongs to one Polloption +VotingRank.belongsTo(PollOption, { + foreignKey: 'pollOptionId', +}) + + module.exports = { db, User, Poll, PollOption, + Vote, VotingRank, }; From 3dd6e849acc0369b6f82784032420f6b5dd373d0 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 11:09:46 -0400 Subject: [PATCH 049/148] feat- created Vote data --- database/seed.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/database/seed.js b/database/seed.js index c789900..1517bd3 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,6 +1,7 @@ const { Pool } = require("pg"); const db = require("./db"); -const { User, Poll, PollOption, VotingRank } = require("./index"); +const { User, Poll, PollOption, Vote, VotingRank } = require("./index"); +const pollOption = require("./models/pollOption"); const seed = async () => { try { @@ -190,11 +191,38 @@ const seed = async () => { }, + ]); + + + + const votes = await Vote.bulkCreate([ + { + userId: users[1].id, + pollId: createdPolls.anime.id + }, + { + userId: users[2].id, + pollId: createdPolls.movie.id + }, + { + userId: users[1].id, + pollId: createdPolls.bbq.id + }, + { + userId: users[2].id, + pollId: createdPolls.authRequired.id + }, + { + userId: users[1].id, + pollId: createdPolls.restricited.id + }, ]) + + console.log(`๐Ÿ‘ค Created ${users.length} users`); console.log(`Created ${Object.keys(createdPolls).length} polls`); - + console.log(`Created ${pollOption.length} poll options`); // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! From a94702bb6f84ec00ad29bdaacf968de923d4dc79 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 11:38:55 -0400 Subject: [PATCH 050/148] feat- created voteRank data --- database/seed.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index 1517bd3..e8a2c06 100644 --- a/database/seed.js +++ b/database/seed.js @@ -194,7 +194,7 @@ const seed = async () => { ]); - + // vote ---> envelope const votes = await Vote.bulkCreate([ { userId: users[1].id, @@ -218,6 +218,44 @@ const seed = async () => { }, ]) + const optionMap = {}; + PollOptions.forEach((option) => { + optionMap[option.optionText] = option; + }); + + const ranks = await VotingRank.bulkCreate([ + { + voteId: votes[0].id, + pollOptionId: optionMap["Demon Slayer"].id, + rank: 1, + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["OnePiece"].id, + rank: 3, + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["AOT"].id, + rank: 4, + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["Devil May Cry"].id, + rank: 6 + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["Castlevania"].id, + rank: 5 + }, + { + voteId: votes[0].id, + pollOptionId: optionMap["Naruto"].id, + rank: 2 + }, + ]) + console.log(`๐Ÿ‘ค Created ${users.length} users`); From 0e7cf4a31da08bb2422044febbfca4c14c91272a Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 11:52:22 -0400 Subject: [PATCH 051/148] fixed- bug in rank data --- database/seed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seed.js b/database/seed.js index e8a2c06..6a5a272 100644 --- a/database/seed.js +++ b/database/seed.js @@ -231,7 +231,7 @@ const seed = async () => { }, { voteId: votes[0].id, - pollOptionId: optionMap["OnePiece"].id, + pollOptionId: optionMap["One Piece"].id, rank: 3, }, { From ea5cab20359f7e762f5b19e52cd74e18eb9ad7c6 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 12:28:09 -0400 Subject: [PATCH 052/148] feat- added attributes to VotingRank --- database/models/votingRank.js | 8 ++++++++ database/seed.js | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/database/models/votingRank.js b/database/models/votingRank.js index 05aa1b2..d5a03c2 100644 --- a/database/models/votingRank.js +++ b/database/models/votingRank.js @@ -4,6 +4,14 @@ const db = require("../db"); // define the votingRank model const VotingRank = db.define("votingRank", { + voteId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + pollOptionId: { + type: DataTypes.INTEGER, + allowNull: false, + }, rank: { type: DataTypes.INTEGER, allowNull: false, diff --git a/database/seed.js b/database/seed.js index 6a5a272..160746a 100644 --- a/database/seed.js +++ b/database/seed.js @@ -117,7 +117,7 @@ const seed = async () => { { optionText: "Devil May Cry", position: 5, - poll_Id: createdPolls.anime.id, + pollId: createdPolls.anime.id, }, { optionText: "Castlevania", @@ -218,6 +218,7 @@ const seed = async () => { }, ]) + const optionMap = {}; PollOptions.forEach((option) => { optionMap[option.optionText] = option; @@ -254,13 +255,13 @@ const seed = async () => { pollOptionId: optionMap["Naruto"].id, rank: 2 }, - ]) + ]); console.log(`๐Ÿ‘ค Created ${users.length} users`); console.log(`Created ${Object.keys(createdPolls).length} polls`); - console.log(`Created ${pollOption.length} poll options`); + console.log(`๐Ÿงพ Created ${PollOptions.length} poll options`); // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! From 78c546aa21617f5b85b122c969fa6796ca17c7f2 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 12:34:57 -0400 Subject: [PATCH 053/148] feat- added attr to Vote model --- database/models/vote.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/database/models/vote.js b/database/models/vote.js index a0f1695..e078ef9 100644 --- a/database/models/vote.js +++ b/database/models/vote.js @@ -6,20 +6,26 @@ const db = require('../db'); const Vote = db.define("vote", { submitted: { type: DataTypes.BOOLEAN, - defaultValue: false // if false; vote has not yet been submited; ballot can still be edited + defaultValue: false, }, voterToken: { type: DataTypes.STRING, - allowNull: true, // this allows us to uniquely identify a guest . we can track if they voted. + allowNull: true, }, ipAddress: { type: DataTypes.STRING, - allowNull: true, // same as voter token we can use either + allowNull: true, }, -}, - { - timestamps: true, - } -); + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + pollId: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}, { + timestamps: true, +}); module.exports = Vote; \ No newline at end of file From 53c26fcc455c2aebf3f3470e7f9e8911df7e2d5e Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 13:15:57 -0400 Subject: [PATCH 054/148] fixed- association bugs --- database/index.js | 8 +------- database/seed.js | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/database/index.js b/database/index.js index c91f4ef..20775f3 100644 --- a/database/index.js +++ b/database/index.js @@ -51,16 +51,10 @@ Vote.belongsTo(Poll, { // one to many- each vote(ballot) can have many ranked options Vote.hasMany(VotingRank, { - foreignKey: "voteRankId", + foreignKey: "voteId", }) -//one to many- A vote(ballot) can have many ranked options -VotingRank.belongsTo(Vote, { - foreignKey: 'voteId', -}); - - // many to one - each rank entry belongs to one vote(ballot) VotingRank.belongsTo(Vote, { foreignKey: "voteId", diff --git a/database/seed.js b/database/seed.js index 160746a..661f859 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,7 +1,6 @@ const { Pool } = require("pg"); const db = require("./db"); const { User, Poll, PollOption, Vote, VotingRank } = require("./index"); -const pollOption = require("./models/pollOption"); const seed = async () => { try { From eb49d4ad34515be90f98936629021b2acab7b06f Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 13:43:50 -0400 Subject: [PATCH 055/148] create- restricted_poll_access model --- database/restrictedPollAccess.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 database/restrictedPollAccess.js diff --git a/database/restrictedPollAccess.js b/database/restrictedPollAccess.js new file mode 100644 index 0000000..e69de29 From 76f7c1316f0ee14daa5f387a631b775ad09496c9 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 14:08:06 -0400 Subject: [PATCH 056/148] feat- defined restricted_poll_access model --- database/index.js | 2 ++ database/models/restrictedPollAccess.js | 25 +++++++++++++++++++++++++ database/restrictedPollAccess.js | 0 3 files changed, 27 insertions(+) create mode 100644 database/models/restrictedPollAccess.js delete mode 100644 database/restrictedPollAccess.js diff --git a/database/index.js b/database/index.js index 20775f3..aa9416d 100644 --- a/database/index.js +++ b/database/index.js @@ -4,6 +4,7 @@ const Poll = require("./models/poll"); const PollOption = require("./models/pollOption"); const VotingRank = require("./models/votingRank"); const Vote = require("./models/vote"); +const RestrictedPollAccess = require('./models/restrictedPollAccess') //One to many - user has many polls User.hasMany(Poll, { @@ -74,4 +75,5 @@ module.exports = { PollOption, Vote, VotingRank, + RestrictedPollAccess, }; diff --git a/database/models/restrictedPollAccess.js b/database/models/restrictedPollAccess.js new file mode 100644 index 0000000..3c70587 --- /dev/null +++ b/database/models/restrictedPollAccess.js @@ -0,0 +1,25 @@ +const { DataTypes } = require("sequelize"); +const db = require("../db"); + +// Table Restricted_Poll_Access { +// id PK +// poll_id FK [ref: > Poll.id] +// user_id FK [ref: > User.id] +// created_at timestamp +// } +const RestrictedPollAccess = db.define("restrictedPollAccess", { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + pollId: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}, + { + timestamps: true, + } +) + +module.exports = RestrictedPollAccess; \ No newline at end of file diff --git a/database/restrictedPollAccess.js b/database/restrictedPollAccess.js deleted file mode 100644 index e69de29..0000000 From 76a106c7cff007902ac04cacaab7f376d5deede9 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 14:35:30 -0400 Subject: [PATCH 057/148] feat- defined associations for restricted --- database/index.js | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/database/index.js b/database/index.js index aa9416d..f046fab 100644 --- a/database/index.js +++ b/database/index.js @@ -6,6 +6,8 @@ const VotingRank = require("./models/votingRank"); const Vote = require("./models/vote"); const RestrictedPollAccess = require('./models/restrictedPollAccess') +//----- User Model-------- + //One to many - user has many polls User.hasMany(Poll, { foreignKey: 'userId', @@ -13,43 +15,52 @@ User.hasMany(Poll, { }); + +//----- Poll Model-------- + // many to one - Each Poll belongs to one user Poll.belongsTo(User, { foreignKey: 'userId', }); + +//----- PollOption Model-------- + // One to many - one Poll has many options Poll.hasMany(PollOption, { foreignKey: 'pollId', onDelete: "CASCADE", // delete poll_options if poll is deleted }); - // many to one - Each pollOption belongs to one Poll PollOption.belongsTo(Poll, { foreignKey: "pollId" }); + +//----- Vote Model-------- + // one to many- each user can submit many votes User.hasMany(Vote, { foreignKey: "userId", }) - // one to one- Each vote(ballot) belongs to a user Vote.belongsTo(User, { foreignKey: "userId", }) - // many to one - Each vote(ballot) belongs to a poll Vote.belongsTo(Poll, { foreignKey: "pollId" }) + +//----- Voting Rank Model-------- + // one to many- each vote(ballot) can have many ranked options Vote.hasMany(VotingRank, { foreignKey: "voteId", @@ -68,6 +79,27 @@ VotingRank.belongsTo(PollOption, { }) + +//----- Restricted Access Model-------- + +//A user can be restricted to many polls +User.belongsToMany(Poll, { + through: RestrictedPollAccess, + as: "restrictedPolls", + foreignKey: "userId" +}) + +// A poll can belong to many users through Restrictred access +Poll.belongsToMany(User, { + through: RestrictedPollAccess, + as: "restictedUsers", + foreignKey: "pollId", +}) + +// + + + module.exports = { db, User, From 798b846e7cdeee404f9a3bff4d397014172554ea Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Tue, 15 Jul 2025 15:54:11 -0400 Subject: [PATCH 058/148] feat(api)-poll router --- api/index.js | 2 ++ api/poll.js | 0 2 files changed, 2 insertions(+) create mode 100644 api/poll.js diff --git a/api/index.js b/api/index.js index f08162e..7e96c0d 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,9 @@ const express = require("express"); const router = express.Router(); +const pollRouter = express.Router(); const testDbRouter = require("./test-db"); router.use("/test-db", testDbRouter); +router.use("/poll", pollRouter) module.exports = router; diff --git a/api/poll.js b/api/poll.js new file mode 100644 index 0000000..e69de29 From 44fc16f27f91f09a9e6f9d4889b3889ed2bed16c Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 14:20:59 -0400 Subject: [PATCH 059/148] chore: credentials and callback url added to .env --- package-lock.json | 41 +++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 42 insertions(+) diff --git a/package-lock.json b/package-lock.json index 1b30289..2bf87b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@react-oauth/google": "^0.12.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -26,6 +27,16 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@react-oauth/google": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz", + "integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1336,6 +1347,29 @@ "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/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1397,6 +1431,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", diff --git a/package.json b/package.json index 7e0a0af..e346ee0 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "license": "ISC", "description": "", "dependencies": { + "@react-oauth/google": "^0.12.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", From 5427bc087c9a2f4a704c7550d3c922d9db32c632 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 20:24:13 -0400 Subject: [PATCH 060/148] chore: routes/auth created --- auth/index.js | 1 + auth/routes.js | 0 2 files changed, 1 insertion(+) create mode 100644 auth/routes.js diff --git a/auth/index.js b/auth/index.js index 47dfd1a..35bd97b 100644 --- a/auth/index.js +++ b/auth/index.js @@ -409,4 +409,5 @@ router.get("/me", (req, res) => { }); }); + module.exports = { router, authenticateJWT }; diff --git a/auth/routes.js b/auth/routes.js new file mode 100644 index 0000000..e69de29 From 7f02cfba3dc4fc30466f68b6b11d9ca04741c4f2 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 21:57:33 -0400 Subject: [PATCH 061/148] feat: defined google strategy --- auth/routes.js | 55 ++++++++++++++++ package-lock.json | 157 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + 3 files changed, 215 insertions(+) diff --git a/auth/routes.js b/auth/routes.js index e69de29..58fd864 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -0,0 +1,55 @@ +const express = require('express'); +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const { User } = require("../database"); +require('../passport'); + +const router = express.Router(); + +passport.use(new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL + }, async (accessToken, refreshToken, profile, done) => { + try { + const user = User.findOne({where: {googleId: profile.id}}); + + if (!user) { + // Try by email + const email = profile.emails?.[0]?.value; + if (email) { + user = await User.findOne({ where: { email } }); + + if (user) { + user.googleId = profile.id; + await user.save(); + } + } + } + + if (!user) { + // Create new user + const email = profile.emails?.[0]?.value || null; + const username = email ? email.split('@')[0] : `user_${Date.now()}`; + + // Ensure username is unique + const finalUsername = username; + const counter = 1; + while (await User.findOne({ where: { username: finalUsername } })) { + finalUsername = `${username}_${counter}`; + counter++; + } + + user = await User.create({ + googleId: profile.id, + email, + username: finalUsername, + passwordHash: null, + }); + } + + return done(null, profile); + } catch (error) { + return done(err, null); + } + })); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2bf87b8..a8d821b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,11 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-session": "^1.18.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "pg": "^8.16.2", "sequelize": "^6.37.7" }, @@ -101,6 +104,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -506,6 +518,46 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1078,6 +1130,12 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1138,6 +1196,64 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -1147,6 +1263,11 @@ "node": ">=16" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg": { "version": "8.16.2", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz", @@ -1323,6 +1444,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1732,6 +1862,24 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1754,6 +1902,15 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/package.json b/package.json index e346ee0..cc6c7b5 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,11 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-session": "^1.18.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", "pg": "^8.16.2", "sequelize": "^6.37.7" }, From 09602d75cb73586dbc766ff9ff43186950e8c076 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 22:09:21 -0400 Subject: [PATCH 062/148] feat: GET /auth/google route added --- auth/routes.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/auth/routes.js b/auth/routes.js index 58fd864..a0fcec2 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -52,4 +52,9 @@ passport.use(new GoogleStrategy({ } catch (error) { return done(err, null); } + })); + + //OAuth flow + router.get("/auth/google", passport.authenticate("google", { + scope: ["profile", "email"] })); \ No newline at end of file From 51dffcbb9345942b644806c14b30c7ad47575ff6 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 22:35:38 -0400 Subject: [PATCH 063/148] feat: handle callback route + Issue JWT token and redirect to frontend --- auth/routes.js | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/auth/routes.js b/auth/routes.js index a0fcec2..727c2e1 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -6,13 +6,15 @@ require('../passport'); const router = express.Router(); +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + passport.use(new GoogleStrategy({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, callbackURL: process.env.GOOGLE_CALLBACK_URL }, async (accessToken, refreshToken, profile, done) => { try { - const user = User.findOne({where: {googleId: profile.id}}); + const user = await User.findOne({where: {googleId: profile.id}}); if (!user) { // Try by email @@ -33,8 +35,8 @@ passport.use(new GoogleStrategy({ const username = email ? email.split('@')[0] : `user_${Date.now()}`; // Ensure username is unique - const finalUsername = username; - const counter = 1; + let finalUsername = username; + let counter = 1; while (await User.findOne({ where: { username: finalUsername } })) { finalUsername = `${username}_${counter}`; counter++; @@ -48,7 +50,7 @@ passport.use(new GoogleStrategy({ }); } - return done(null, profile); + return done(null, user); } catch (error) { return done(err, null); } @@ -57,4 +59,39 @@ passport.use(new GoogleStrategy({ //OAuth flow router.get("/auth/google", passport.authenticate("google", { scope: ["profile", "email"] - })); \ No newline at end of file + })); + + //Handle callback + router.get("/auth/google/callback", passport.authenticate("google", {session: false}), + async (req,res) => { + try { + const user = req.user; + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + username: user.username, + auth0Id: user.auth0Id, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.redirect(`${process.env.FRONTEND_URL}/dashboard`); + } catch (error) { + res.redirect(`${process.env.FRONTEND_URL}/login?error=google&message=Google%20login%20failed`); + + } + + }); + + module.exports = router; \ No newline at end of file From b891697061706f6b21ccce8d3a4239605b1083aa Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Tue, 15 Jul 2025 22:46:28 -0400 Subject: [PATCH 064/148] chore: google router mounted --- app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.js b/app.js index 5857036..c2504ce 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,7 @@ const apiRouter = require("./api"); const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); +const oAuthRouter = require("./auth/routes") const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -30,6 +31,7 @@ app.use(morgan("dev")); // logging middleware app.use(express.static(path.join(__dirname, "public"))); // serve static files from public folder app.use("/api", apiRouter); // mount api router app.use("/auth", authRouter); // mount auth router +app.use("/auth", oAuthRouter); //mount oAuth router // error handling middleware app.use((err, req, res, next) => { From 7a7fda207d602be106787130c7404dd0a8bb5dc6 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Wed, 16 Jul 2025 00:54:53 -0400 Subject: [PATCH 065/148] refactor: added require jwt + changed const user to let + err->error/Tran's feedback --- auth/routes.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/auth/routes.js b/auth/routes.js index 727c2e1..f720e65 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -1,8 +1,9 @@ const express = require('express'); +const jwt = require('jsonwebtoken'); const passport = require('passport'); const GoogleStrategy = require('passport-google-oauth20').Strategy; const { User } = require("../database"); -require('../passport'); + const router = express.Router(); @@ -14,7 +15,7 @@ passport.use(new GoogleStrategy({ callbackURL: process.env.GOOGLE_CALLBACK_URL }, async (accessToken, refreshToken, profile, done) => { try { - const user = await User.findOne({where: {googleId: profile.id}}); + let user = await User.findOne({where: {googleId: profile.id}}); if (!user) { // Try by email @@ -52,7 +53,7 @@ passport.use(new GoogleStrategy({ return done(null, user); } catch (error) { - return done(err, null); + return done(error, null); } })); @@ -65,7 +66,7 @@ passport.use(new GoogleStrategy({ router.get("/auth/google/callback", passport.authenticate("google", {session: false}), async (req,res) => { try { - const user = req.user; + let user = req.user; // Generate JWT token const token = jwt.sign( From 64707accf26eb3da1515da6886b3b93c14331f0f Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 16 Jul 2025 10:53:45 -0400 Subject: [PATCH 066/148] draft poll creation --- api/poll.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/poll.js b/api/poll.js index e69de29..785d48f 100644 --- a/api/poll.js +++ b/api/poll.js @@ -0,0 +1,22 @@ +const express = require("express"); +const router = express.Router(); +const { Poll } = require("../database"); + +// Create poll +router.post("/", async (req, res) => { + try { + const poll = req.body; + + if (!poll) { + return res.status(400).json({ error: "Make sure to meet all constraints" }); + } + + const newPoll = await Poll.create(poll); + res.status(201).json(newPoll) + } catch (error) { + res.status(500).json({ + error: "Failed to create poll", + message: "Check that api end points match" + }) + } +}) \ No newline at end of file From 57c8f655e23c982ee0a6e5f225dfc0615f620613 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 16 Jul 2025 10:59:47 -0400 Subject: [PATCH 067/148] fixed- poll post request --- api/index.js | 4 ++-- api/{poll.js => polls.js} | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) rename api/{poll.js => polls.js} (95%) diff --git a/api/index.js b/api/index.js index 7e96c0d..eb7fad7 100644 --- a/api/index.js +++ b/api/index.js @@ -1,9 +1,9 @@ const express = require("express"); const router = express.Router(); -const pollRouter = express.Router(); +const pollRouter = require("./polls"); const testDbRouter = require("./test-db"); router.use("/test-db", testDbRouter); -router.use("/poll", pollRouter) +router.use("/polls", pollRouter) module.exports = router; diff --git a/api/poll.js b/api/polls.js similarity index 95% rename from api/poll.js rename to api/polls.js index 785d48f..dc47142 100644 --- a/api/poll.js +++ b/api/polls.js @@ -19,4 +19,6 @@ router.post("/", async (req, res) => { message: "Check that api end points match" }) } -}) \ No newline at end of file +}) + +module.exports = router; \ No newline at end of file From 6742da477589b4985ff6a11db1200dac6d50a2d2 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 16 Jul 2025 12:55:44 -0400 Subject: [PATCH 068/148] feat- added JWT to poll post request --- api/polls.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api/polls.js b/api/polls.js index dc47142..23299a5 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,17 +1,21 @@ const express = require("express"); const router = express.Router(); const { Poll } = require("../database"); - +const { authenticateJWT } = require("../auth") // Create poll -router.post("/", async (req, res) => { +router.post("/", authenticateJWT, async (req, res) => { try { - const poll = req.body; + const userId = req.user.id + const pollData = req.body; if (!poll) { return res.status(400).json({ error: "Make sure to meet all constraints" }); } - const newPoll = await Poll.create(poll); + const newPoll = await Poll.create({ + ...pollData, + userId + }); res.status(201).json(newPoll) } catch (error) { res.status(500).json({ From 798f41e98106042e1ef028cbf529390f7a172830 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 14:09:01 -0400 Subject: [PATCH 069/148] fix: resolve Google OAuth startup errors and field name inconsistencies --- app.js | 13 +++++ auth/routes.js | 112 ++++++++++++++++++++++++++++++++++++++++ database/models/user.js | 5 ++ 3 files changed, 130 insertions(+) create mode 100644 auth/routes.js diff --git a/app.js b/app.js index 5857036..ec7587d 100644 --- a/app.js +++ b/app.js @@ -4,6 +4,8 @@ const morgan = require("morgan"); const path = require("path"); const jwt = require("jsonwebtoken"); const cookieParser = require("cookie-parser"); +const passport = require("passport"); +const session = require("express-session"); const app = express(); const apiRouter = require("./api"); const { router: authRouter } = require("./auth"); @@ -26,6 +28,17 @@ app.use( // cookie parser middleware app.use(cookieParser()); +// Session middleware for passport +app.use(session({ + secret: process.env.JWT_SECRET || 'your-secret-key', + resave: false, + saveUninitialized: false +})); + +// Initialize passport +app.use(passport.initialize()); +app.use(passport.session()); + app.use(morgan("dev")); // logging middleware app.use(express.static(path.join(__dirname, "public"))); // serve static files from public folder app.use("/api", apiRouter); // mount api router diff --git a/auth/routes.js b/auth/routes.js new file mode 100644 index 0000000..261adce --- /dev/null +++ b/auth/routes.js @@ -0,0 +1,112 @@ +const express = require('express'); +const jwt = require('jsonwebtoken'); +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const { User } = require("../database"); + + +const router = express.Router(); + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +// Passport serialization +passport.serializeUser((user, done) => { + done(null, user.id); +}); + +passport.deserializeUser(async (id, done) => { + try { + const user = await User.findByPk(id); + done(null, user); + } catch (error) { + done(error, null); + } +}); + +passport.use(new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL + }, async (accessToken, refreshToken, profile, done) => { + try { + let user = await User.findOne({where: {googleId: profile.id}}); + + if (!user) { + // Try by email + const email = profile.emails?.[0]?.value; + if (email) { + user = await User.findOne({ where: { email } }); + + if (user) { + user.googleId = profile.id; + await user.save(); + } + } + } + + if (!user) { + // Create new user + const email = profile.emails?.[0]?.value || null; + const username = email ? email.split('@')[0] : `user_${Date.now()}`; + + // Ensure username is unique + let finalUsername = username; + let counter = 1; + while (await User.findOne({ where: { userName: finalUsername } })) { + finalUsername = `${username}_${counter}`; + counter++; + } + + user = await User.create({ + googleId: profile.id, + email, + userName: finalUsername, + passwordHash: null, + }); + } + + return done(null, user); + } catch (error) { + return done(error, null); + } + })); + + //OAuth flow + router.get("/google", passport.authenticate("google", { + scope: ["profile", "email"] + })); + + //Handle callback + router.get("/google/callback", passport.authenticate("google", {session: false}), + async (req,res) => { + try { + let user = req.user; + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + userName: user.userName, + auth0Id: user.auth0Id, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.redirect(`${process.env.FRONTEND_URL}/dashboard`); + } catch (error) { + res.redirect(`${process.env.FRONTEND_URL}/login?error=google&message=Google%20login%20failed`); + + } + + }); + + module.exports = router; \ No newline at end of file diff --git a/database/models/user.js b/database/models/user.js index 6e82a40..e83fb26 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -48,6 +48,11 @@ const User = db.define("user", { allowNull: true, unique: true, }, + googleId: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, img: { type: DataTypes.STRING, allowNull: true, From 44ca5073134fc4e4eebc991dbfd35a3088fb8f92 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 14:53:34 -0400 Subject: [PATCH 070/148] : fix: trying to reosolve conflict --- auth/routes.js | 98 -------------------------------------------------- 1 file changed, 98 deletions(-) diff --git a/auth/routes.js b/auth/routes.js index ede50d0..261adce 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -109,102 +109,4 @@ passport.use(new GoogleStrategy({ }); - module.exports = router; -const express = require('express'); -const jwt = require('jsonwebtoken'); -const passport = require('passport'); -const GoogleStrategy = require('passport-google-oauth20').Strategy; -const { User } = require("../database"); - - -const router = express.Router(); - -const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; - -passport.use(new GoogleStrategy({ - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: process.env.GOOGLE_CALLBACK_URL - }, async (accessToken, refreshToken, profile, done) => { - try { - let user = await User.findOne({where: {googleId: profile.id}}); - - if (!user) { - // Try by email - const email = profile.emails?.[0]?.value; - if (email) { - user = await User.findOne({ where: { email } }); - - if (user) { - user.googleId = profile.id; - await user.save(); - } - } - } - - if (!user) { - // Create new user - const email = profile.emails?.[0]?.value || null; - const username = email ? email.split('@')[0] : `user_${Date.now()}`; - - // Ensure username is unique - let finalUsername = username; - let counter = 1; - while (await User.findOne({ where: { username: finalUsername } })) { - finalUsername = `${username}_${counter}`; - counter++; - } - - user = await User.create({ - googleId: profile.id, - email, - username: finalUsername, - passwordHash: null, - }); - } - - return done(null, user); - } catch (error) { - return done(error, null); - } - })); - - //OAuth flow - router.get("/auth/google", passport.authenticate("google", { - scope: ["profile", "email"] - })); - - //Handle callback - router.get("/auth/google/callback", passport.authenticate("google", {session: false}), - async (req,res) => { - try { - let user = req.user; - - // Generate JWT token - const token = jwt.sign( - { - id: user.id, - username: user.username, - auth0Id: user.auth0Id, - email: user.email, - }, - JWT_SECRET, - { expiresIn: "24h" } - ); - - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 24 * 60 * 60 * 1000, // 24 hours - }); - - res.redirect(`${process.env.FRONTEND_URL}/dashboard`); - } catch (error) { - res.redirect(`${process.env.FRONTEND_URL}/login?error=google&message=Google%20login%20failed`); - - } - - }); - module.exports = router; \ No newline at end of file From 0697da3b96de0ed77f48a8d393d721a09918ddd2 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 16 Jul 2025 15:01:26 -0400 Subject: [PATCH 071/148] fixed- user attribute bugs --- api/polls.js | 4 +++- app.js | 1 - database/models/user.js | 2 +- database/seed.js | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/polls.js b/api/polls.js index 23299a5..8958fc0 100644 --- a/api/polls.js +++ b/api/polls.js @@ -2,13 +2,15 @@ const express = require("express"); const router = express.Router(); const { Poll } = require("../database"); const { authenticateJWT } = require("../auth") + + // Create poll router.post("/", authenticateJWT, async (req, res) => { try { const userId = req.user.id const pollData = req.body; - if (!poll) { + if (!pollData) { return res.status(400).json({ error: "Make sure to meet all constraints" }); } diff --git a/app.js b/app.js index c2504ce..6bba530 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,3 @@ -require("dotenv").config(); const express = require("express"); const morgan = require("morgan"); const path = require("path"); diff --git a/database/models/user.js b/database/models/user.js index 6e82a40..c84b05d 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -3,7 +3,7 @@ const db = require("../db"); const bcrypt = require("bcrypt"); const User = db.define("user", { - userName: { + username: { type: DataTypes.STRING, allowNull: true, unique: true, diff --git a/database/seed.js b/database/seed.js index 661f859..3aed02f 100644 --- a/database/seed.js +++ b/database/seed.js @@ -9,15 +9,15 @@ const seed = async () => { const users = await User.bulkCreate([ { - userName: "admin", + username: "admin", passwordHash: User.hashPassword("admin123"), }, { - userName: "user1", + username: "user1", passwordHash: User.hashPassword("user111") }, { - userName: "user2", + username: "user2", passwordHash: User.hashPassword("user222") }, ]); From 5f93073eee1aa47eca16d682287c78d8574679d6 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 15:22:54 -0400 Subject: [PATCH 072/148] fix: standardize auth field names to use 'username' according to new update to model --- auth/index.js | 1 - auth/routes.js | 6 +++--- database/models/user.js | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/auth/index.js b/auth/index.js index da92618..f804146 100644 --- a/auth/index.js +++ b/auth/index.js @@ -169,7 +169,6 @@ 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" }); } diff --git a/auth/routes.js b/auth/routes.js index 261adce..e4095a3 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -52,7 +52,7 @@ passport.use(new GoogleStrategy({ // Ensure username is unique let finalUsername = username; let counter = 1; - while (await User.findOne({ where: { userName: finalUsername } })) { + while (await User.findOne({ where: { username: finalUsername } })) { finalUsername = `${username}_${counter}`; counter++; } @@ -60,7 +60,7 @@ passport.use(new GoogleStrategy({ user = await User.create({ googleId: profile.id, email, - userName: finalUsername, + username: finalUsername, passwordHash: null, }); } @@ -86,7 +86,7 @@ passport.use(new GoogleStrategy({ const token = jwt.sign( { id: user.id, - userName: user.userName, + username: user.username, auth0Id: user.auth0Id, email: user.email, }, diff --git a/database/models/user.js b/database/models/user.js index e83fb26..4e3876b 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -3,7 +3,7 @@ const db = require("../db"); const bcrypt = require("bcrypt"); const User = db.define("user", { - userName: { + username: { type: DataTypes.STRING, allowNull: true, unique: true, From f3504a670d673fbc327cf13821a1782840048bbd Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 15:25:56 -0400 Subject: [PATCH 073/148] fix: update user model to username from userName --- database/models/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/models/user.js b/database/models/user.js index 6e82a40..c84b05d 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -3,7 +3,7 @@ const db = require("../db"); const bcrypt = require("bcrypt"); const User = db.define("user", { - userName: { + username: { type: DataTypes.STRING, allowNull: true, unique: true, From 91f0f3d51b62285c6498abc3cfb4781e3905534a Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 15:39:54 -0400 Subject: [PATCH 074/148] : fix: log-in/sign-up routes to match username field --- auth/index.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/auth/index.js b/auth/index.js index 35bd97b..ccaef1f 100644 --- a/auth/index.js +++ b/auth/index.js @@ -120,20 +120,20 @@ router.post("/signup", async (req, res) => { } // Check if user already exists - const existingUser = await User.findOne({ where: { userName: username } }); + const existingUser = await User.findOne({ where: { username } }); if (existingUser) { return res.status(409).send({ error: "Username already exists" }); } // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ userName: username, passwordHash }); + const user = await User.create({ username, passwordHash }); // Generate JWT token const token = jwt.sign( { id: user.id, - userName: user.userName, + username: user.username, email: user.email, }, JWT_SECRET, @@ -149,7 +149,7 @@ router.post("/signup", async (req, res) => { res.send({ message: "User created successfully", - user: { id: user.id, userName: user.userName }, + user: { id: user.id, username: user.username }, }); } catch (error) { console.error("Signup error:", error); @@ -188,10 +188,10 @@ router.post("/signup/username", async (req, res) => { }); } - // Check if username already exists (check both userName and email fields to prevent duplicates) + // Check if username already exists (check both username and email fields to prevent duplicates) const existingUser = await User.findOne({ where: { - [Op.or]: [{ userName: username }, { email: username }], + [Op.or]: [{ username }, { email: username }], }, }); @@ -202,7 +202,7 @@ router.post("/signup/username", async (req, res) => { // Create new user const passwordHash = User.hashPassword(password); const userData = { - userName: username, + username, passwordHash, firstName: firstName || null, lastName: lastName || null, @@ -214,7 +214,7 @@ router.post("/signup/username", async (req, res) => { const token = jwt.sign( { id: user.id, - userName: user.userName, + username: user.username, email: user.email, }, JWT_SECRET, @@ -232,7 +232,7 @@ router.post("/signup/username", async (req, res) => { message: "User created successfully with username", user: { id: user.id, - userName: user.userName, + username: user.username, firstName: user.firstName, lastName: user.lastName, }, @@ -267,12 +267,12 @@ router.post("/signup/email", async (req, res) => { return res.status(400).send({ error: "Please provide a valid email address" }); } - // Check if email already exists (check both email and userName fields to prevennt ani duplicates) + // Check if email already exists (check both email and username fields to prevent duplicates) const existingUser = await User.findOne({ where: { [Op.or]: [ { email: email }, - { userName: email } + { username: email } ] } }); @@ -296,7 +296,7 @@ router.post("/signup/email", async (req, res) => { const token = jwt.sign( { id: user.id, - userName: user.userName, + username: user.username, email: user.email, }, JWT_SECRET, @@ -338,7 +338,7 @@ router.post("/login", async (req, res) => { const user = await User.findOne({ where: { [Op.or]: [ - { userName: username }, + { username }, { email: username } ] } @@ -357,7 +357,7 @@ router.post("/login", async (req, res) => { const token = jwt.sign( { id: user.id, - userName: user.userName, + username: user.username, email: user.email, }, JWT_SECRET, @@ -375,7 +375,7 @@ router.post("/login", async (req, res) => { message: "Login successful", user: { id: user.id, - userName: user.userName, + username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName From 4c234e34decd65d5a25581903c5f2a45b647dcc7 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 15:46:37 -0400 Subject: [PATCH 075/148] fix: resoleved merge con flict --- auth/routes.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/auth/routes.js b/auth/routes.js index e4095a3..1a8fd9f 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -52,7 +52,11 @@ passport.use(new GoogleStrategy({ // Ensure username is unique let finalUsername = username; let counter = 1; +<<<<<<< HEAD while (await User.findOne({ where: { username: finalUsername } })) { +======= + while (await User.findOne({ where: { userName: finalUsername } })) { +>>>>>>> 44ca507 (: fix: trying to reosolve conflict) finalUsername = `${username}_${counter}`; counter++; } @@ -60,7 +64,11 @@ passport.use(new GoogleStrategy({ user = await User.create({ googleId: profile.id, email, +<<<<<<< HEAD username: finalUsername, +======= + userName: finalUsername, +>>>>>>> 44ca507 (: fix: trying to reosolve conflict) passwordHash: null, }); } @@ -86,7 +94,11 @@ passport.use(new GoogleStrategy({ const token = jwt.sign( { id: user.id, +<<<<<<< HEAD username: user.username, +======= + userName: user.userName, +>>>>>>> 44ca507 (: fix: trying to reosolve conflict) auth0Id: user.auth0Id, email: user.email, }, From 4f7c95d349407ca66f6415ea4aa9c0ad96671d0d Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 15:49:50 -0400 Subject: [PATCH 076/148] : fix: resoleved merge conflicts and added .env config --- app.js | 1 + auth/routes.js | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/app.js b/app.js index 2bf0c75..cefe0a1 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,4 @@ +require("dotenv").config(); const express = require("express"); const morgan = require("morgan"); const path = require("path"); diff --git a/auth/routes.js b/auth/routes.js index 1a8fd9f..e4095a3 100644 --- a/auth/routes.js +++ b/auth/routes.js @@ -52,11 +52,7 @@ passport.use(new GoogleStrategy({ // Ensure username is unique let finalUsername = username; let counter = 1; -<<<<<<< HEAD while (await User.findOne({ where: { username: finalUsername } })) { -======= - while (await User.findOne({ where: { userName: finalUsername } })) { ->>>>>>> 44ca507 (: fix: trying to reosolve conflict) finalUsername = `${username}_${counter}`; counter++; } @@ -64,11 +60,7 @@ passport.use(new GoogleStrategy({ user = await User.create({ googleId: profile.id, email, -<<<<<<< HEAD username: finalUsername, -======= - userName: finalUsername, ->>>>>>> 44ca507 (: fix: trying to reosolve conflict) passwordHash: null, }); } @@ -94,11 +86,7 @@ passport.use(new GoogleStrategy({ const token = jwt.sign( { id: user.id, -<<<<<<< HEAD username: user.username, -======= - userName: user.userName, ->>>>>>> 44ca507 (: fix: trying to reosolve conflict) auth0Id: user.auth0Id, email: user.email, }, From 323f7beec447b7cfaa813baa793965065824da4b Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 16 Jul 2025 15:54:08 -0400 Subject: [PATCH 077/148] refactor- update to deadline attr --- database/models/poll.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/models/poll.js b/database/models/poll.js index 5031a1f..41969a6 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -22,7 +22,7 @@ const Poll = db.define("poll", { }, deadline: { type: DataTypes.DATE, - allowNull: false, + // allowNull: false, }, authRequired: { type: DataTypes.BOOLEAN, // allow only user votes if true From d622b5669905a2114a0ad9571f26baa2354a16ea Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 15:56:06 -0400 Subject: [PATCH 078/148] fix: added back sign up routes that was lost during merge conflict resolve, and adde d back imports --- auth/index.js | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/auth/index.js b/auth/index.js index 181d235..b5a102c 100644 --- a/auth/index.js +++ b/auth/index.js @@ -1,6 +1,7 @@ const express = require("express"); const jwt = require("jsonwebtoken"); const { User } = require("../database"); +const { Op } = require("sequelize"); const router = express.Router(); @@ -229,4 +230,173 @@ router.get("/me", (req, res) => { }); }); +// Helper function to check if input looks like email +const looksLikeEmail = (input) => { + return input.includes("@"); +}; + +// Signup with username and password +router.post("/signup/username", async (req, res) => { + try { + const { username, password, firstName, lastName } = req.body; + + if (!username || !password) { + return res + .status(400) + .send({ error: "Username and password are required" }); + } + + if (password.length < 6) { + return res + .status(400) + .send({ error: "Password must be at least 6 characters long" }); + } + + // Check if input looks like email + if (looksLikeEmail(username)) { + return res + .status(400) + .send({ + error: + "Username cannot be an email address. Please use a regular username.", + }); + } + + // Check if username already exists (check both username and email fields to prevent duplicates) + const existingUser = await User.findOne({ + where: { + [Op.or]: [{ username }, { email: username }], + }, + }); + + if (existingUser) { + return res.status(409).send({ error: "Username already exists" }); + } + + // Create new user + const passwordHash = User.hashPassword(password); + const userData = { + username, + passwordHash, + firstName: firstName || null, + lastName: lastName || null, + }; + + const user = await User.create(userData); + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + username: user.username, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.send({ + message: "User created successfully with username", + user: { + id: user.id, + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + }, + }); + } catch (error) { + console.error("Username signup error:", error); + res.status(500).send({ error: "Internal server error" }); + } +}); + +// Helper function to validate email format +const isValidEmail = (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +// Signup with email and password +router.post("/signup/email", async (req, res) => { + try { + const { email, password, firstName, lastName } = req.body; + + if (!email || !password) { + return res.status(400).send({ error: "Email and password are required" }); + } + + if (password.length < 6) { + return res.status(400).send({ error: "Password must be at least 6 characters long" }); + } + + // Validate email format + if (!isValidEmail(email)) { + return res.status(400).send({ error: "Please provide a valid email address" }); + } + + // Check if email already exists (check both email and username fields to prevent duplicates) + const existingUser = await User.findOne({ + where: { + [Op.or]: [ + { email: email }, + { username: email } + ] + } + }); + + if (existingUser) { + return res.status(409).send({ error: "Email already exists" }); + } + + // Create new user + const passwordHash = User.hashPassword(password); + const userData = { + email: email, + passwordHash, + firstName: firstName || null, + lastName: lastName || null + }; + + const user = await User.create(userData); + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + username: user.username, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.send({ + message: "User created successfully with email", + user: { + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }, + }); + } catch (error) { + console.error("Email signup error:", error); + res.status(500).send({ error: "Internal server error" }); + } +}); + module.exports = { router, authenticateJWT }; From bff247c3c88cc79db7cd42d32bad1b16a2df1fdf Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 16 Jul 2025 16:02:38 -0400 Subject: [PATCH 079/148] fix: fix route to check both username and emails --- auth/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/auth/index.js b/auth/index.js index b5a102c..5faf45b 100644 --- a/auth/index.js +++ b/auth/index.js @@ -168,8 +168,15 @@ router.post("/login", async (req, res) => { return; } - // Find user - const user = await User.findOne({ where: { username } }); + // Find user by username or email + const user = await User.findOne({ + where: { + [Op.or]: [ + { username: username }, + { email: username } + ] + } + }); if (!user) { return res.status(401).send({ error: "Invalid credentials" }); } From ef4ec875c638c5a59653e26c0d4ac6044c47ae37 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Wed, 16 Jul 2025 19:47:45 -0400 Subject: [PATCH 080/148] feat: delete draft poll route made --- api/polls.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api/polls.js b/api/polls.js index 8958fc0..8b9208d 100644 --- a/api/polls.js +++ b/api/polls.js @@ -27,4 +27,23 @@ router.post("/", authenticateJWT, async (req, res) => { } }) +//delete draft poll + +router.delete("/:id",authenticateJWT, async (req, res) => { + try{ + const pollId = req.params.id; + const userId = req.user.id; + + const poll = await Poll.findByPk(pollId); + + if (!poll) {res.status(404).json({error: "Poll not found"})}; + + + + } + catch (error) { + + } +}); + module.exports = router; \ No newline at end of file From c737a5f25b06af8f9214ca1e1c3dd2b86dc2da25 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Wed, 16 Jul 2025 19:52:46 -0400 Subject: [PATCH 081/148] feat: delete is enabled only for poll owner --- api/polls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index 8b9208d..24f133b 100644 --- a/api/polls.js +++ b/api/polls.js @@ -38,7 +38,7 @@ router.delete("/:id",authenticateJWT, async (req, res) => { if (!poll) {res.status(404).json({error: "Poll not found"})}; - + if (poll.userId !== userId) {res.status(401).json({error: "Unauthorized action"})}; } catch (error) { From 64db6ade1b5849efb763bc064928bddc1e2a4269 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Wed, 16 Jul 2025 19:56:34 -0400 Subject: [PATCH 082/148] feat: delete is enabled only for drafts --- api/polls.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index 24f133b..34f7f6a 100644 --- a/api/polls.js +++ b/api/polls.js @@ -38,7 +38,11 @@ router.delete("/:id",authenticateJWT, async (req, res) => { if (!poll) {res.status(404).json({error: "Poll not found"})}; - if (poll.userId !== userId) {res.status(401).json({error: "Unauthorized action"})}; + if (poll.userId !== userId) {res.status(401).json({error: "Unauthorized action: You do not own this poll"})}; + + if (poll.status !== "draft") {res.status(401).json({error: "Unauthorized action: Only draft polls can be deleted"})}; + + } catch (error) { From 4b96e104d6fc0aae8d2a6b80e74df736a9e3b395 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Wed, 16 Jul 2025 20:04:49 -0400 Subject: [PATCH 083/148] feat: deletion confirmation --- api/polls.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/polls.js b/api/polls.js index 34f7f6a..f30505e 100644 --- a/api/polls.js +++ b/api/polls.js @@ -42,11 +42,13 @@ router.delete("/:id",authenticateJWT, async (req, res) => { if (poll.status !== "draft") {res.status(401).json({error: "Unauthorized action: Only draft polls can be deleted"})}; - + await poll.destroy(); + + res.json({message: "Draft poll deleted successfully"}); } catch (error) { - + res.status(500).json({error: "Failed to delete draft poll"}); } }); From 837516281ee2c35aa40e6e0a82c51468251a2ec1 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Thu, 17 Jul 2025 02:07:32 -0400 Subject: [PATCH 084/148] feat- create poll with options --- api/index.js | 4 ++-- api/polls.js | 37 +++++++++++++++++++++++------------ database/models/pollOption.js | 4 ++++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/api/index.js b/api/index.js index 7e96c0d..eb7fad7 100644 --- a/api/index.js +++ b/api/index.js @@ -1,9 +1,9 @@ const express = require("express"); const router = express.Router(); -const pollRouter = express.Router(); +const pollRouter = require("./polls"); const testDbRouter = require("./test-db"); router.use("/test-db", testDbRouter); -router.use("/poll", pollRouter) +router.use("/polls", pollRouter) module.exports = router; diff --git a/api/polls.js b/api/polls.js index 8958fc0..e21501b 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,30 +1,43 @@ const express = require("express"); const router = express.Router(); -const { Poll } = require("../database"); +const { Poll, PollOption } = require("../database"); const { authenticateJWT } = require("../auth") -// Create poll +// Create polls router.post("/", authenticateJWT, async (req, res) => { + const userId = req.user.id + const { title, description, deadline, status, options = [] } = req.body; try { - const userId = req.user.id - const pollData = req.body; - - if (!pollData) { - return res.status(400).json({ error: "Make sure to meet all constraints" }); - } const newPoll = await Poll.create({ - ...pollData, - userId + title, + description, + deadline, + status, + userId, }); - res.status(201).json(newPoll) + + //[opttion1, option2, option3] + if (options.length > 0) { + const formattedOptions = options.map(text => ({ + optionText: text, + pollId: newPoll.id + })); + + await PollOption.bulkCreate(formattedOptions) + res.status(201).json({ + message: "Poll and options created", + poll: newPoll + }); + } } catch (error) { res.status(500).json({ error: "Failed to create poll", - message: "Check that api end points match" + message: "Check that API fields and data are correct" }) } }) + module.exports = router; \ No newline at end of file diff --git a/database/models/pollOption.js b/database/models/pollOption.js index 0e56250..e6f3612 100644 --- a/database/models/pollOption.js +++ b/database/models/pollOption.js @@ -17,6 +17,10 @@ const pollOption = db.define("pollOption", { type: DataTypes.INTEGER, allowNull: true, }, + pollId: { + type: DataTypes.INTEGER, + allowNull: false, + } }, { timestamps: true, From de2e1acd29c1624cd5c0c46cbe4a14884b5c5963 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Thu, 17 Jul 2025 02:29:22 -0400 Subject: [PATCH 085/148] feat- added validation --- api/polls.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/polls.js b/api/polls.js index e21501b..cda48a2 100644 --- a/api/polls.js +++ b/api/polls.js @@ -8,6 +8,14 @@ const { authenticateJWT } = require("../auth") router.post("/", authenticateJWT, async (req, res) => { const userId = req.user.id const { title, description, deadline, status, options = [] } = req.body; + + if (status === "published" && options.length < 2) { + res.status(400).json({ + error: " 2 options are requires to publish a poll" + }) + }; + + try { const newPoll = await Poll.create({ From 154cff7c890a611246610505968fbef4485c3977 Mon Sep 17 00:00:00 2001 From: Heavenwilson Date: Thu, 17 Jul 2025 10:44:52 -0400 Subject: [PATCH 086/148] feat(pollform): add end date/time toggle with datetime-local input --- .env.example | 3 --- api/index.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 848ad61..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DATABASE_URL=postgres://hostname:5432/database-name -JWT_SECRET=super-secret-jwt-key -FRONTEND_URL=http://localhost:3000 diff --git a/api/index.js b/api/index.js index 7e96c0d..6961dcd 100644 --- a/api/index.js +++ b/api/index.js @@ -1,9 +1,9 @@ const express = require("express"); const router = express.Router(); -const pollRouter = express.Router(); +const pollsRouter = require("./polls");; const testDbRouter = require("./test-db"); router.use("/test-db", testDbRouter); -router.use("/poll", pollRouter) +router.use("/polls", pollsRouter) module.exports = router; From 7abe57dda90b2624d48f421415f52e476cc62273 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Thu, 17 Jul 2025 11:04:56 -0400 Subject: [PATCH 087/148] refactor- cleaned up code --- api/polls.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/api/polls.js b/api/polls.js index 7eb3561..f9d2c92 100644 --- a/api/polls.js +++ b/api/polls.js @@ -15,9 +15,7 @@ router.post("/", authenticateJWT, async (req, res) => { }) }; - try { - const newPoll = await Poll.create({ title, description, @@ -25,7 +23,6 @@ router.post("/", authenticateJWT, async (req, res) => { status, userId, }); - //[opttion1, option2, option3] if (options.length > 0) { const formattedOptions = options.map(text => ({ @@ -45,10 +42,11 @@ router.post("/", authenticateJWT, async (req, res) => { message: "Check that API fields and data are correct" }) } -}) +}); + -//delete draft poll +//delete draft poll router.delete("/:id", authenticateJWT, async (req, res) => { try { const pollId = req.params.id; From db692d4bd5bb23e1545100f354a2fb2594167f7f Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Thu, 17 Jul 2025 13:02:09 -0400 Subject: [PATCH 088/148] feat- get all polls --- api/polls.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/polls.js b/api/polls.js index f9d2c92..d781f79 100644 --- a/api/polls.js +++ b/api/polls.js @@ -3,6 +3,17 @@ const router = express.Router(); const { Poll, PollOption } = require("../database"); const { authenticateJWT } = require("../auth") +// Get all Polls +router.get("/", authenticateJWT, async (req, res) => { + const userId = req.user.id; + + try { + const userPolls = await Poll.findAll({ where: { userId } }); + res.json(userPolls); + } catch (error) { + res.status(500).json({ error: "Failed to get all polls" }); + } +}); // Create polls router.post("/", authenticateJWT, async (req, res) => { From 587d6d8e851ce7130dd326558ed88021d934c51f Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Thu, 17 Jul 2025 16:59:29 -0400 Subject: [PATCH 089/148] feat: slug added to poll model --- app.js | 2 +- database/models/poll.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index cefe0a1..0c4a32a 100644 --- a/app.js +++ b/app.js @@ -54,7 +54,7 @@ app.use((err, req, res, next) => { const runApp = async () => { try { - await db.sync(); + await db.sync({ alter: true }); //update tables to match model console.log("โœ… Connected to the database"); app.listen(PORT, () => { console.log(`๐Ÿš€ Server is running on port ${PORT}`); diff --git a/database/models/poll.js b/database/models/poll.js index 41969a6..9006b62 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -36,6 +36,12 @@ const Poll = db.define("poll", { type: DataTypes.BOOLEAN, // only specic users can parcipate if true default: false, }, + slug: { + type: DataTypes.STRING, + unique: true, + allowNull: false, + }, + }, { timestamps: true, From 3f67a2e278d6e88e6fdd56dfe0311d1958cc30d7 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Thu, 17 Jul 2025 17:38:02 -0400 Subject: [PATCH 090/148] feat: slug auto-generated upon creation --- database/models/poll.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/database/models/poll.js b/database/models/poll.js index 9006b62..c0d10fd 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -48,4 +48,32 @@ const Poll = db.define("poll", { } ); +//slug creation + +function slugify(text) { + return text + .toString() + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^\w\-]+/g, "") + .replace(/\-\-+/g, "-"); + } + + //auto-generate slug + Poll.beforeCreate(async (poll) => { + if (!poll.slug) { + const baseSlug = slugify(poll.title); + let uniqueSlug = baseSlug; + let counter = 1; + + while (await Poll.findOne({where: {slug: uniqueSlug}})){ + uniqueSlug = `${baseSlug}-${counter++}`; + } + poll.slug = uniqueSlug; + } + }) + + + module.exports = Poll; \ No newline at end of file From f24e6218f57532237f831eeb38a205e30099e1e4 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Fri, 18 Jul 2025 00:23:13 -0400 Subject: [PATCH 091/148] feat- Patch complete --- api/polls.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++-- database/seed.js | 2 +- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/api/polls.js b/api/polls.js index d781f79..a8738fc 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,7 +1,9 @@ const express = require("express"); const router = express.Router(); const { Poll, PollOption } = require("../database"); -const { authenticateJWT } = require("../auth") +const { authenticateJWT } = require("../auth"); +const pollOption = require("../database/models/pollOption"); +const { where } = require("sequelize"); // Get all Polls router.get("/", authenticateJWT, async (req, res) => { @@ -15,6 +17,7 @@ router.get("/", authenticateJWT, async (req, res) => { } }); + // Create polls router.post("/", authenticateJWT, async (req, res) => { const userId = req.user.id @@ -42,11 +45,12 @@ router.post("/", authenticateJWT, async (req, res) => { })); await PollOption.bulkCreate(formattedOptions) - res.status(201).json({ + return res.status(201).json({ message: "Poll and options created", poll: newPoll }); } + return res.json(newPoll) } catch (error) { res.status(500).json({ error: "Failed to create poll", @@ -56,6 +60,72 @@ router.post("/", authenticateJWT, async (req, res) => { }); +router.patch("/:pollId", authenticateJWT, async (req, res) => { + const userId = req.user.id + const poll = req.body; + const { title, description, deadline, status, options = [] } = req.body; + const newBody = { + title, + description, + deadline, + status, + } + const { pollId } = req.params; + + try { + const updatePoll = await Poll.findByPk(pollId); + + if (!updatePoll) { + return res.status(404).json({ error: "poll not found" }) + } else if (updatePoll.userId !== userId) { + return res.status(403).json({ error: "poll does not belong to this user" }) + }; + + + if (updatePoll.status === "draft") { + const updatedPoll = await updatePoll.update(newBody); + const optionsToDestroy = await PollOption.destroy({ where: { pollId } }); + + // [option1, option2, option3] + // formattedOptions = [ + // { + // optionText: 'option1', + // pollId: pollId, + // }, + // { + // optionText: 'option2', + // pollId: pollId, + // }, + // { + // optionText: 'option3', + // pollId: pollId, + // } + // ]; + + const formattedOptions = await options.map((text) => ({ + optionText: text, + pollId: pollId, + })); + + const newPollOptions = await PollOption.bulkCreate(formattedOptions); + + return res.json(newBody) + }; + + if (updatePoll.status === "published") { + const updateDeadline = await updatePoll.update({ deadline }); + return res.json(updateDeadline); + } + return res.status(400).json({ error: "Invalid poll status string or update not allowed" }) + } catch (error) { + console.error("Update error:", error); + res.status(500).json({ + error: "Failed to update poll", + message: "Only deadline can be edited when poll is published", + }) + } + +}); //delete draft poll router.delete("/:id", authenticateJWT, async (req, res) => { diff --git a/database/seed.js b/database/seed.js index 3aed02f..176f73a 100644 --- a/database/seed.js +++ b/database/seed.js @@ -30,7 +30,7 @@ const seed = async () => { title: "Best Anime?", description: "Rank your favorite animes!", participants: 0, - status: "published", + status: "draft", userKey: "user1", }, From 8a4c79d96a42c705d8bbb879c8f437cb3b9dd4bb Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 18 Jul 2025 11:24:50 -0400 Subject: [PATCH 092/148] feat: random string generator added for uniqueness/Tran's feedback --- database/models/poll.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/database/models/poll.js b/database/models/poll.js index c0d10fd..82b8eb4 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -59,16 +59,25 @@ function slugify(text) { .replace(/[^\w\-]+/g, "") .replace(/\-\-+/g, "-"); } + + // generate a random string + function generateRandomString(length = 6) { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; + } //auto-generate slug Poll.beforeCreate(async (poll) => { if (!poll.slug) { const baseSlug = slugify(poll.title); let uniqueSlug = baseSlug; - let counter = 1; while (await Poll.findOne({where: {slug: uniqueSlug}})){ - uniqueSlug = `${baseSlug}-${counter++}`; + uniqueSlug = `${baseSlug}-${generateRandomString()}`; } poll.slug = uniqueSlug; } From 392dae70f611d5f7f5c31b21847ca20b756a7fdd Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 18 Jul 2025 12:05:05 -0400 Subject: [PATCH 093/148] fix: slugifying switch from beforeCreate to beforeValidate + status msg moved outside option creation conditional --- api/polls.js | 12 +++++++----- database/models/poll.js | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/polls.js b/api/polls.js index f9d2c92..b827b32 100644 --- a/api/polls.js +++ b/api/polls.js @@ -10,7 +10,7 @@ router.post("/", authenticateJWT, async (req, res) => { const { title, description, deadline, status, options = [] } = req.body; if (status === "published" && options.length < 2) { - res.status(400).json({ + return res.status(400).json({ error: " 2 options are requires to publish a poll" }) }; @@ -31,12 +31,14 @@ router.post("/", authenticateJWT, async (req, res) => { })); await PollOption.bulkCreate(formattedOptions) - res.status(201).json({ - message: "Poll and options created", - poll: newPoll - }); + } + res.status(201).json({ + message: "Poll and options created", + poll: newPoll + }); } catch (error) { + console.error("Poll creation failed:", error); res.status(500).json({ error: "Failed to create poll", message: "Check that API fields and data are correct" diff --git a/database/models/poll.js b/database/models/poll.js index 82b8eb4..bae6812 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -71,7 +71,7 @@ function slugify(text) { } //auto-generate slug - Poll.beforeCreate(async (poll) => { + Poll.beforeValidate(async (poll) => { if (!poll.slug) { const baseSlug = slugify(poll.title); let uniqueSlug = baseSlug; From bb74919f2ca9825a6c522f9615b0d7355c3ce789 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Fri, 18 Jul 2025 12:40:36 -0400 Subject: [PATCH 094/148] feat-Get draft polls complete --- api/polls.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/api/polls.js b/api/polls.js index a8738fc..26708cc 100644 --- a/api/polls.js +++ b/api/polls.js @@ -2,10 +2,8 @@ const express = require("express"); const router = express.Router(); const { Poll, PollOption } = require("../database"); const { authenticateJWT } = require("../auth"); -const pollOption = require("../database/models/pollOption"); -const { where } = require("sequelize"); -// Get all Polls +// Get all users Polls---------------------------- router.get("/", authenticateJWT, async (req, res) => { const userId = req.user.id; @@ -18,7 +16,23 @@ router.get("/", authenticateJWT, async (req, res) => { }); -// Create polls +//Get all users draft polls +router.get("/draft", authenticateJWT, async (req, res) => { + const userId = req.user.id; + try { + const draftPolls = await Poll.findAll({ + where: { + userId, + status: "draft", + } + }) + res.json(draftPolls) + } catch (error) { + res.status(500).json({ error: "Failed to get drafted polls" }) + } +}) + +// Create polls--------------------------- router.post("/", authenticateJWT, async (req, res) => { const userId = req.user.id const { title, description, deadline, status, options = [] } = req.body; @@ -60,6 +74,8 @@ router.post("/", authenticateJWT, async (req, res) => { }); + +//Edit polls-------------------- router.patch("/:pollId", authenticateJWT, async (req, res) => { const userId = req.user.id const poll = req.body; From 7278e27ce5d816e0dc47b083a89032e7c8a6afda Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 18 Jul 2025 14:03:18 -0400 Subject: [PATCH 095/148] feat: get poll by slug add (includes options) --- api/polls.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api/polls.js b/api/polls.js index 64d1f68..a440c90 100644 --- a/api/polls.js +++ b/api/polls.js @@ -15,6 +15,25 @@ router.get("/", authenticateJWT, async (req, res) => { } }); +// Get polls by slug +router.get("/:slug", authenticateJWT, async (req, res) => { + try { + const pollSlug = req.params.slug; + const poll = await Poll.findOne({ + where: { slug: pollSlug }, + include: [{ + model: PollOption, + }] + }); + + if (!poll) { return res.status(404).json({ error: "Poll not found" }) }; + res.json(poll); + + } + catch (error) { + res.status(500).json({ error: "Failed to get poll" }); + } +}); //Get all users draft polls router.get("/draft", authenticateJWT, async (req, res) => { From 1d7c7ce1ddbefc30652e342fe42a84bf47a367c8 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Fri, 18 Jul 2025 14:11:26 -0400 Subject: [PATCH 096/148] refactor-optimized response --- api/polls.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/polls.js b/api/polls.js index 26708cc..e569e44 100644 --- a/api/polls.js +++ b/api/polls.js @@ -16,17 +16,25 @@ router.get("/", authenticateJWT, async (req, res) => { }); -//Get all users draft polls +//Get all draft polls by user router.get("/draft", authenticateJWT, async (req, res) => { const userId = req.user.id; try { + const draftPolls = await Poll.findAll({ where: { userId, status: "draft", } }) - res.json(draftPolls) + const specialDelivery = { + message: draftPolls.length === 0 + ? "There no polls to display" + : "Polls successfully retrived", + polls: draftPolls, + } + + res.status(200).json(specialDelivery); } catch (error) { res.status(500).json({ error: "Failed to get drafted polls" }) } From d649ad227d1528b64672f5a3b58adb3c393eb543 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 18 Jul 2025 14:43:05 -0400 Subject: [PATCH 097/148] refactor: /draft moved on top of /:slug /Tran's feedback --- api/polls.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/api/polls.js b/api/polls.js index a440c90..b372d74 100644 --- a/api/polls.js +++ b/api/polls.js @@ -15,6 +15,22 @@ router.get("/", authenticateJWT, async (req, res) => { } }); +//Get all users draft polls +router.get("/draft", authenticateJWT, async (req, res) => { + const userId = req.user.id; + try { + const draftPolls = await Poll.findAll({ + where: { + userId, + status: "draft", + } + }) + res.json(draftPolls) + } catch (error) { + res.status(500).json({ error: "Failed to get drafted polls" }) + } +}); + // Get polls by slug router.get("/:slug", authenticateJWT, async (req, res) => { try { @@ -35,21 +51,6 @@ router.get("/:slug", authenticateJWT, async (req, res) => { } }); -//Get all users draft polls -router.get("/draft", authenticateJWT, async (req, res) => { - const userId = req.user.id; - try { - const draftPolls = await Poll.findAll({ - where: { - userId, - status: "draft", - } - }) - res.json(draftPolls) - } catch (error) { - res.status(500).json({ error: "Failed to get drafted polls" }) - } -}) // Create polls--------------------------- router.post("/", authenticateJWT, async (req, res) => { From 51f08ea0aa319e9eb05bdd329ee40985c8dddde8 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Fri, 18 Jul 2025 15:20:00 -0400 Subject: [PATCH 098/148] refactor- send sorted response by createdAT --- api/polls.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index e569e44..947dfcc 100644 --- a/api/polls.js +++ b/api/polls.js @@ -31,9 +31,14 @@ router.get("/draft", authenticateJWT, async (req, res) => { message: draftPolls.length === 0 ? "There no polls to display" : "Polls successfully retrived", - polls: draftPolls, + polls: draftPolls, // polls is an array of objects } + specialDelivery.polls.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + specialDelivery.polls.map((poll) => { + console.log(poll.createdAt) + }) + res.status(200).json(specialDelivery); } catch (error) { res.status(500).json({ error: "Failed to get drafted polls" }) From 05ea1619bd7d3ce4d2480b8d6aabc8b148f15623 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Fri, 18 Jul 2025 20:44:40 -0400 Subject: [PATCH 099/148] feat- get poll by id complete --- api/polls.js | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/api/polls.js b/api/polls.js index 947dfcc..471a557 100644 --- a/api/polls.js +++ b/api/polls.js @@ -16,7 +16,7 @@ router.get("/", authenticateJWT, async (req, res) => { }); -//Get all draft polls by user +//Get all draft polls by user-------------------- router.get("/draft", authenticateJWT, async (req, res) => { const userId = req.user.id; try { @@ -45,6 +45,37 @@ router.get("/draft", authenticateJWT, async (req, res) => { } }) + +//Get a users poll by id with options----------------- +router.get("/:pollId", authenticateJWT, async (req, res) => { + const userId = req.user.id; + const { pollId } = req.params; + console.log(pollId); + console.log(userId); + + try { + // fetch a spcific poll with options that belong to this user + const poll = await Poll.findOne({ + where: { + id: pollId, + userId: userId + }, + include: { model: PollOption } + }) + + if (!poll) { + return res.status(404).json({ error: "No polls found" }) + } + res.json(poll) + + } catch (error) { + console.error("Error fetching poll:", error); + res.status(500).json({ error: "Failed to get poll by ID" }); + } +}); + + + // Create polls--------------------------- router.post("/", authenticateJWT, async (req, res) => { const userId = req.user.id @@ -55,7 +86,6 @@ router.post("/", authenticateJWT, async (req, res) => { error: " 2 options are requires to publish a poll" }) }; - try { const newPoll = await Poll.create({ title, @@ -156,7 +186,7 @@ router.patch("/:pollId", authenticateJWT, async (req, res) => { }); -//delete draft poll +//delete draft poll------------------------- router.delete("/:id", authenticateJWT, async (req, res) => { try { const pollId = req.params.id; From eab9f6018e75c02adbb60c7afc8b4effed0e225a Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sat, 19 Jul 2025 14:29:00 -0400 Subject: [PATCH 100/148] fixed- change slug endpoint for a more explicit path --- api/polls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index d5cabab..81263da 100644 --- a/api/polls.js +++ b/api/polls.js @@ -48,7 +48,7 @@ router.get("/draft", authenticateJWT, async (req, res) => { }); // Get polls by slug -router.get("/:slug", authenticateJWT, async (req, res) => { +router.get("/slug/:slug", authenticateJWT, async (req, res) => { try { const pollSlug = req.params.slug; const poll = await Poll.findOne({ From bd371880028cef3fa95f5b10d07cb73654cb3d43 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:10:41 -0400 Subject: [PATCH 101/148] chore: middleware folder created + checkDuplicateVote file added --- middleware/checkDuplicateVote.js | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 middleware/checkDuplicateVote.js diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js new file mode 100644 index 0000000..23534e2 --- /dev/null +++ b/middleware/checkDuplicateVote.js @@ -0,0 +1,5 @@ +const {Vote} = require("../database"); + +const checkDuplicateVote = async (req, res, next) => {}; + +module.exports = checkDuplicateVote; \ No newline at end of file From 687f816cc8780e95674d6e309d7838f310026313 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:22:00 -0400 Subject: [PATCH 102/148] feat: check if user already voted --- middleware/checkDuplicateVote.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index 23534e2..bb8fc60 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -1,5 +1,27 @@ -const {Vote} = require("../database"); +const { Vote } = require("../database"); -const checkDuplicateVote = async (req, res, next) => {}; +const checkDuplicateVote = async (req, res, next) => { + const userId = req.user.id; + const pollId = req.body.pollId; -module.exports = checkDuplicateVote; \ No newline at end of file + try { + if (userId) { + //check if user already voted + const existingVote = await Vote.findOne({ + where: { + userId: userId, + pollId: pollId, + }, + }); + + if (existingVote) { + return res + .status(409) + .json({ error: "You have already voted on this poll." }); + } + } + + } catch (error) {} +}; + +module.exports = checkDuplicateVote; From 9ad7c03f3488b934748b3100f42ffc048825310f Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:22:58 -0400 Subject: [PATCH 103/148] feat: check if guest already voted through voterToken --- middleware/checkDuplicateVote.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index bb8fc60..bb46272 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -20,7 +20,21 @@ const checkDuplicateVote = async (req, res, next) => { .json({ error: "You have already voted on this poll." }); } } - + else { + const guestUser = req.body.voterToken; + //check if guest user already voted + const existingGuestVote = await Vote.findOne({ + where: { + voterToken: guestUser, + pollId: pollId, + }, + }); + if (existingGuestVote) { + return res + .status(409) + .json({ error: "You have already voted on this poll." }); + } + } } catch (error) {} }; From 5835ea590abf31e19e4de31cce6b17649fb42494 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:26:21 -0400 Subject: [PATCH 104/148] feat: IP checking added for guests --- middleware/checkDuplicateVote.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index bb46272..c72af9f 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -22,10 +22,12 @@ const checkDuplicateVote = async (req, res, next) => { } else { const guestUser = req.body.voterToken; + const ipAddress = req.ipAddress //check if guest user already voted const existingGuestVote = await Vote.findOne({ where: { voterToken: guestUser, + ipAddress: ipAddress, pollId: pollId, }, }); From 842ee6beeeb781a7bc4c2dbb126c6545a96b1377 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:27:53 -0400 Subject: [PATCH 105/148] feat: error handling added to catch block --- middleware/checkDuplicateVote.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index c72af9f..75d973c 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -37,7 +37,10 @@ const checkDuplicateVote = async (req, res, next) => { .json({ error: "You have already voted on this poll." }); } } - } catch (error) {} + } catch (error) { + console.error("Duplicate vote check failed:", error); + res.status(500).json({ error: "Server error checking vote." }); + } }; module.exports = checkDuplicateVote; From a64e91fcdae47aaf6051cc26d55b39fe61fc56f1 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:36:05 -0400 Subject: [PATCH 106/148] feat: sequelize Op.or operator added to make guest users be checked through ip or voterToken --- middleware/checkDuplicateVote.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index 75d973c..032d51a 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -1,4 +1,5 @@ const { Vote } = require("../database"); +const {Op} = require("sequelize"); //operator to specify multiple conditions const checkDuplicateVote = async (req, res, next) => { const userId = req.user.id; @@ -26,9 +27,11 @@ const checkDuplicateVote = async (req, res, next) => { //check if guest user already voted const existingGuestVote = await Vote.findOne({ where: { - voterToken: guestUser, - ipAddress: ipAddress, pollId: pollId, + [Op.or]: [ + { voterToken: guestUser }, + { ipAddress: ipAddress } + ], }, }); if (existingGuestVote) { @@ -37,6 +40,7 @@ const checkDuplicateVote = async (req, res, next) => { .json({ error: "You have already voted on this poll." }); } } + return next(); // proceed to the next middleware or route handler } catch (error) { console.error("Duplicate vote check failed:", error); res.status(500).json({ error: "Server error checking vote." }); From fb4be07a4bf213c8c1f1609f5426082578e1aba3 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:40:12 -0400 Subject: [PATCH 107/148] fix: ip retrieval fixed ( .ipAddress --> .ip) --- middleware/checkDuplicateVote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index 032d51a..1c2abbc 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -23,7 +23,7 @@ const checkDuplicateVote = async (req, res, next) => { } else { const guestUser = req.body.voterToken; - const ipAddress = req.ipAddress + const ipAddress = req.ip; //check if guest user already voted const existingGuestVote = await Vote.findOne({ where: { From 89afbef57768624bb5475a07e6cdf824de3cbdfb Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 19 Jul 2025 17:44:30 -0400 Subject: [PATCH 108/148] fix: safety check added if no userId available --- middleware/checkDuplicateVote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index 1c2abbc..acfd3a9 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -2,7 +2,7 @@ const { Vote } = require("../database"); const {Op} = require("sequelize"); //operator to specify multiple conditions const checkDuplicateVote = async (req, res, next) => { - const userId = req.user.id; + const userId = req.user ? req.user.id : null; const pollId = req.body.pollId; try { From c5f9655d3f164940230dba772d8e191e0f3cd908 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Mon, 21 Jul 2025 12:56:49 -0400 Subject: [PATCH 109/148] able to create a vote --- api/polls.js | 473 ++++++++++++++++++++++++++++------------------- database/seed.js | 116 ++++++------ 2 files changed, 341 insertions(+), 248 deletions(-) diff --git a/api/polls.js b/api/polls.js index 81263da..9abefab 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,237 +1,330 @@ const express = require("express"); const router = express.Router(); -const { Poll, PollOption } = require("../database"); +const { Poll, PollOption, Vote, VotingRank } = require("../database"); const { authenticateJWT } = require("../auth"); // Get all users Polls---------------------------- router.get("/", authenticateJWT, async (req, res) => { - const userId = req.user.id; - - try { - const userPolls = await Poll.findAll({ where: { userId } }); - res.json(userPolls); - } catch (error) { - res.status(500).json({ error: "Failed to get all polls" }); - } + const userId = req.user.id; + + try { + const userPolls = await Poll.findAll({ where: { userId } }); + res.json(userPolls); + } catch (error) { + res.status(500).json({ error: "Failed to get all polls" }); + } }); - - //Get all draft polls by user-------------------- router.get("/draft", authenticateJWT, async (req, res) => { - const userId = req.user.id; - try { - - const draftPolls = await Poll.findAll({ - where: { - userId, - status: "draft", - } - }) - const specialDelivery = { - message: draftPolls.length === 0 - ? "There no polls to display" - : "Polls successfully retrived", - polls: draftPolls, // polls is an array of objects - } - - specialDelivery.polls.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) - specialDelivery.polls.map((poll) => { - console.log(poll.createdAt) - }) - - res.status(200).json(specialDelivery); - } catch (error) { - res.status(500).json({ error: "Failed to get drafted polls" }) - } + const userId = req.user.id; + try { + const draftPolls = await Poll.findAll({ + where: { + userId, + status: "draft", + }, + }); + const specialDelivery = { + message: + draftPolls.length === 0 + ? "There no polls to display" + : "Polls successfully retrived", + polls: draftPolls, // polls is an array of objects + }; + + specialDelivery.polls.sort( + (a, b) => new Date(b.createdAt) - new Date(a.createdAt) + ); + specialDelivery.polls.map((poll) => { + console.log(poll.createdAt); + }); + + res.status(200).json(specialDelivery); + } catch (error) { + res.status(500).json({ error: "Failed to get drafted polls" }); + } }); // Get polls by slug router.get("/slug/:slug", authenticateJWT, async (req, res) => { - try { - const pollSlug = req.params.slug; - const poll = await Poll.findOne({ - where: { slug: pollSlug }, - include: [{ - model: PollOption, - }] - }); - - if (!poll) { return res.status(404).json({ error: "Poll not found" }) }; - res.json(poll); - - } - catch (error) { - res.status(500).json({ error: "Failed to get poll" }); + try { + const pollSlug = req.params.slug; + const poll = await Poll.findOne({ + where: { slug: pollSlug }, + include: [ + { + model: PollOption, + }, + ], + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); } + res.json(poll); + } catch (error) { + res.status(500).json({ error: "Failed to get poll" }); + } }); - - -//Get a users poll by id with options----------------- +//Get a users poll by id with options----------------- router.get("/:pollId", authenticateJWT, async (req, res) => { - const userId = req.user.id; - const { pollId } = req.params; - console.log(pollId); - console.log(userId); - - try { - // fetch a spcific poll with options that belong to this user - const poll = await Poll.findOne({ - where: { - id: pollId, - userId: userId - }, - include: { model: PollOption } - }) - - if (!poll) { - return res.status(404).json({ error: "No polls found" }) - } - res.json(poll) - - } catch (error) { - console.error("Error fetching poll:", error); - res.status(500).json({ error: "Failed to get poll by ID" }); + const userId = req.user.id; + const { pollId } = req.params; + console.log(pollId); + console.log(userId); + + try { + // fetch a spcific poll with options that belong to this user + const poll = await Poll.findOne({ + where: { + id: pollId, + userId: userId, + }, + include: { model: PollOption }, + }); + + if (!poll) { + return res.status(404).json({ error: "No polls found" }); } + res.json(poll); + } catch (error) { + console.error("Error fetching poll:", error); + res.status(500).json({ error: "Failed to get poll by ID" }); + } }); - - // Create polls--------------------------- router.post("/", authenticateJWT, async (req, res) => { - const userId = req.user.id - const { title, description, deadline, status, options = [] } = req.body; - - if (status === "published" && options.length < 2) { - return res.status(400).json({ - error: " 2 options are requires to publish a poll" - }) - }; - try { - const newPoll = await Poll.create({ - title, - description, - deadline, - status, - userId, - }); - //[opttion1, option2, option3] - if (options.length > 0) { - const formattedOptions = options.map(text => ({ - optionText: text, - pollId: newPoll.id - })); - - await PollOption.bulkCreate(formattedOptions) - return res.status(201).json({ - message: "Poll and options created", - poll: newPoll - }); - } - return res.json(newPoll) - } catch (error) { - console.error("Poll creation failed:", error); - res.status(500).json({ - error: "Failed to create poll", - message: "Check that API fields and data are correct" - }) + const userId = req.user.id; + const { title, description, deadline, status, options = [] } = req.body; + + if (status === "published" && options.length < 2) { + return res.status(400).json({ + error: " 2 options are requires to publish a poll", + }); + } + try { + const newPoll = await Poll.create({ + title, + description, + deadline, + status, + userId, + }); + //[opttion1, option2, option3] + if (options.length > 0) { + const formattedOptions = options.map((text) => ({ + optionText: text, + pollId: newPoll.id, + })); + + await PollOption.bulkCreate(formattedOptions); + return res.status(201).json({ + message: "Poll and options created", + poll: newPoll, + }); } + return res.json(newPoll); + } catch (error) { + console.error("Poll creation failed:", error); + res.status(500).json({ + error: "Failed to create poll", + message: "Check that API fields and data are correct", + }); + } }); - - //Edit polls-------------------- router.patch("/:pollId", authenticateJWT, async (req, res) => { - const userId = req.user.id - const poll = req.body; - const { title, description, deadline, status, options = [] } = req.body; - const newBody = { - title, - description, - deadline, - status, + const userId = req.user.id; + const poll = req.body; + const { title, description, deadline, status, options = [] } = req.body; + const newBody = { + title, + description, + deadline, + status, + }; + const { pollId } = req.params; + + try { + const updatePoll = await Poll.findByPk(pollId); + + if (!updatePoll) { + return res.status(404).json({ error: "poll not found" }); + } else if (updatePoll.userId !== userId) { + return res + .status(403) + .json({ error: "poll does not belong to this user" }); } - const { pollId } = req.params; - - try { - const updatePoll = await Poll.findByPk(pollId); - - if (!updatePoll) { - return res.status(404).json({ error: "poll not found" }) - } else if (updatePoll.userId !== userId) { - return res.status(403).json({ error: "poll does not belong to this user" }) - }; - - - if (updatePoll.status === "draft") { - const updatedPoll = await updatePoll.update(newBody); - const optionsToDestroy = await PollOption.destroy({ where: { pollId } }); - - // [option1, option2, option3] - // formattedOptions = [ - // { - // optionText: 'option1', - // pollId: pollId, - // }, - // { - // optionText: 'option2', - // pollId: pollId, - // }, - // { - // optionText: 'option3', - // pollId: pollId, - // } - // ]; - - const formattedOptions = await options.map((text) => ({ - optionText: text, - pollId: pollId, - })); - - const newPollOptions = await PollOption.bulkCreate(formattedOptions); - - return res.json(newBody) - }; - - if (updatePoll.status === "published") { - const updateDeadline = await updatePoll.update({ deadline }); - return res.json(updateDeadline); - } - return res.status(400).json({ error: "Invalid poll status string or update not allowed" }) - } catch (error) { - console.error("Update error:", error); - res.status(500).json({ - error: "Failed to update poll", - message: "Only deadline can be edited when poll is published", - }) + + if (updatePoll.status === "draft") { + const updatedPoll = await updatePoll.update(newBody); + const optionsToDestroy = await PollOption.destroy({ where: { pollId } }); + + // [option1, option2, option3] + // formattedOptions = [ + // { + // optionText: 'option1', + // pollId: pollId, + // }, + // { + // optionText: 'option2', + // pollId: pollId, + // }, + // { + // optionText: 'option3', + // pollId: pollId, + // } + // ]; + + const formattedOptions = await options.map((text) => ({ + optionText: text, + pollId: pollId, + })); + + const newPollOptions = await PollOption.bulkCreate(formattedOptions); + + return res.json(newBody); } + if (updatePoll.status === "published") { + const updateDeadline = await updatePoll.update({ deadline }); + return res.json(updateDeadline); + } + return res + .status(400) + .json({ error: "Invalid poll status string or update not allowed" }); + } catch (error) { + console.error("Update error:", error); + res.status(500).json({ + error: "Failed to update poll", + message: "Only deadline can be edited when poll is published", + }); + } }); //delete draft poll------------------------- router.delete("/:id", authenticateJWT, async (req, res) => { - try { - const pollId = req.params.id; - const userId = req.user.id; + try { + const pollId = req.params.id; + const userId = req.user.id; - const poll = await Poll.findByPk(pollId); + const poll = await Poll.findByPk(pollId); - if (!poll) { return res.status(404).json({ error: "Poll not found" }) }; + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } - if (poll.userId !== userId) { return res.status(401).json({ error: "Unauthorized action: You do not own this poll" }) }; + if (poll.userId !== userId) { + return res + .status(401) + .json({ error: "Unauthorized action: You do not own this poll" }); + } - if (poll.status !== "draft") { return res.status(401).json({ error: "Unauthorized action: Only draft polls can be deleted" }) }; + if (poll.status !== "draft") { + return res.status(401).json({ + error: "Unauthorized action: Only draft polls can be deleted", + }); + } - await poll.destroy(); + await poll.destroy(); - res.json({ message: "Draft poll deleted successfully" }); + res.json({ message: "Draft poll deleted successfully" }); + } catch (error) { + res.status(500).json({ error: "Failed to delete draft poll" }); + } +}); +//-------------------------------------------------------------- Create A vote ballot -------------------------------------------- +router.post("/:pollId/vote", authenticateJWT, async (req, res) => { + // rankings = [ + // { optionId: 1, rank: 1 }, + // { optionId: 2, rank: 2 }, + // ]; + console.log("Vote route hit"); + const userId = req.user.id; + console.log("User Id", userId) + + const { pollId } = req.params; + console.log("Poll Id", pollId) + const { rankings } = req.body; + if (!userId) { + return res.status(404).json({ error: "Unathorized action" }); + } + + try { + // I know I am going to need the options that belong to this poll so I should query this poll and include the options + + const poll = await Poll.findOne({ + where: { userId: userId, id: pollId }, + include: { model: PollOption }, + }); + // return res.send(poll) this is returns the poll that I want + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); } - catch (error) { - res.status(500).json({ error: "Failed to delete draft poll" }); + + // I want to create a new vote only if a vote does not already exist + + // Now that we have the poll we are workgin with I want to look at the vote that belongs to this poll + const vote = await Vote.findOne({ + where: { userId: userId, pollId: pollId }, + include: { model: VotingRank }, + }); + if (vote) { + return res + .status(401) + .json({ error: "A vote for this poll aready exist" }); } + + const newVote = await Vote.create({userId, pollId, submitted: true}) + // return res.send(newVote) + // I created the a new vote and linked it to the user and the poll now i need to create a new vote_ranking and link it to this vote + const formattedVotingRank = rankings.map((rank) => { + return {pollOptionId: rank.optionId, voteId: newVote.id, rank: rank.rank} + }) + + console.log(formattedVotingRank) + const newVoteRanking= await VotingRank.bulkCreate(formattedVotingRank) + // return res.send(newVoteRanking) + +const completedVote = await Vote.findOne({ + where: { userId: userId, pollId: pollId }, + include: { model: VotingRank, include: {model: PollOption, attributes: ['id', 'optionText']} }, + }); +// if (newVote.submitted === false) { +// vote.submitted = true; +// await newVotevote.save(); +// } + return res.send(completedVote); + + // Recap I have the target Poll then I featch the vote that belongs to the user and this poll .. after getting the vote + // i was able to fetech all rankings that belongs to this vote + + // I now know that i am able to fetch all the data the I need .. however I am fetchin a vote that already exist but has not been submitted + // if (vote.submitted === true) { + // return res.status(401).json({ + // error: "this user has already submitted a vote for this poll", + // }); + // } + + + // return res.send(vote); + + /// so from here i have succesfully submitted a vote now i need to go back to the top and create a new vote with new rankings since + // what I accomplish was to submit a vote predefined in the seed.js + } catch (error) { + console.log("Fatal error"); + return res.status(500).json({ error: "Failed to submit a vote" }); + } + + + }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/database/seed.js b/database/seed.js index 176f73a..b1f47ef 100644 --- a/database/seed.js +++ b/database/seed.js @@ -194,67 +194,67 @@ const seed = async () => { // vote ---> envelope - const votes = await Vote.bulkCreate([ - { - userId: users[1].id, - pollId: createdPolls.anime.id - }, - { - userId: users[2].id, - pollId: createdPolls.movie.id - }, - { - userId: users[1].id, - pollId: createdPolls.bbq.id - }, - { - userId: users[2].id, - pollId: createdPolls.authRequired.id - }, - { - userId: users[1].id, - pollId: createdPolls.restricited.id - }, - ]) + // const votes = await Vote.bulkCreate([ + // { + // userId: users[1].id, + // pollId: createdPolls.anime.id + // }, + // { + // userId: users[2].id, + // pollId: createdPolls.movie.id + // }, + // { + // userId: users[1].id, + // pollId: createdPolls.bbq.id + // }, + // { + // userId: users[2].id, + // pollId: createdPolls.authRequired.id + // }, + // { + // userId: users[1].id, + // pollId: createdPolls.restricited.id + // }, + // ]) - const optionMap = {}; - PollOptions.forEach((option) => { - optionMap[option.optionText] = option; - }); + // const optionMap = {}; + // PollOptions.forEach((option) => { + // optionMap[option.optionText] = option; + // }); - const ranks = await VotingRank.bulkCreate([ - { - voteId: votes[0].id, - pollOptionId: optionMap["Demon Slayer"].id, - rank: 1, - }, - { - voteId: votes[0].id, - pollOptionId: optionMap["One Piece"].id, - rank: 3, - }, - { - voteId: votes[0].id, - pollOptionId: optionMap["AOT"].id, - rank: 4, - }, - { - voteId: votes[0].id, - pollOptionId: optionMap["Devil May Cry"].id, - rank: 6 - }, - { - voteId: votes[0].id, - pollOptionId: optionMap["Castlevania"].id, - rank: 5 - }, - { - voteId: votes[0].id, - pollOptionId: optionMap["Naruto"].id, - rank: 2 - }, - ]); + // const ranks = await VotingRank.bulkCreate([ + // { + // voteId: votes[0].id, + // pollOptionId: optionMap["Demon Slayer"].id, + // rank: 1, + // }, + // { + // voteId: votes[0].id, + // pollOptionId: optionMap["One Piece"].id, + // rank: 3, + // }, + // { + // voteId: votes[0].id, + // pollOptionId: optionMap["AOT"].id, + // rank: 4, + // }, + // { + // voteId: votes[0].id, + // pollOptionId: optionMap["Devil May Cry"].id, + // rank: 6 + // }, + // { + // voteId: votes[0].id, + // pollOptionId: optionMap["Castlevania"].id, + // rank: 5 + // }, + // { + // voteId: votes[0].id, + // pollOptionId: optionMap["Naruto"].id, + // rank: 2 + // }, + // ]); From e0bda7828b0a2471f48c7ae56ed928d932453959 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Tue, 22 Jul 2025 17:48:36 -0400 Subject: [PATCH 110/148] fix: modify delete route/now logged in user can delete their own published polls + drafts --- api/polls.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/api/polls.js b/api/polls.js index 9abefab..cae2197 100644 --- a/api/polls.js +++ b/api/polls.js @@ -220,22 +220,14 @@ router.delete("/:id", authenticateJWT, async (req, res) => { } if (poll.userId !== userId) { - return res - .status(401) - .json({ error: "Unauthorized action: You do not own this poll" }); - } - - if (poll.status !== "draft") { - return res.status(401).json({ - error: "Unauthorized action: Only draft polls can be deleted", - }); + return res.status(401).json({ error: "Unauthorized action: You do not own this poll" }); } await poll.destroy(); - res.json({ message: "Draft poll deleted successfully" }); + res.json({ message: "Poll deleted successfully" }); } catch (error) { - res.status(500).json({ error: "Failed to delete draft poll" }); + res.status(500).json({ error: "Failed to delete poll" }); } }); From b0223d0cf01e4424d93f0dd882aa436e5f9280a2 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Tue, 22 Jul 2025 18:38:53 -0400 Subject: [PATCH 111/148] feat: duplicates routes now added --- api/polls.js | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/api/polls.js b/api/polls.js index cae2197..83de1c1 100644 --- a/api/polls.js +++ b/api/polls.js @@ -319,4 +319,102 @@ const completedVote = await Vote.findOne({ }); +// duplicate poll endpoint +router.post('/:id/duplicate', authenticateJWT, async (req, res) => { + try { + const pollId = req.params.id; + const userId = req.user.id; + + // fetch poll and options + const poll = await Poll.findByPk(pollId, { + include: { model: PollOption } + }); + if (!poll) { + return res.status(404).json({ error: 'Poll not found' }); + } + if (poll.userId !== userId) { + return res.status(403).json({ error: 'Unauthorized: not your poll' }); + } + + // create new poll with same fields + const newPoll = await Poll.create({ + title: poll.title + ' (copy)', + description: poll.description, + status: 'draft', + userId: userId, + deadline: poll.deadline, + authRequired: poll.authRequired, + restricted: poll.restricted + }); + + // generate unique slug + await newPoll.update({ slug: `${poll.slug}-copy-${newPoll.id}` }); + + // copy options + const newOptions = poll.pollOptions.map((opt) => ({ + optionText: opt.optionText, + pollId: newPoll.id, + position: opt.position + })); + await PollOption.bulkCreate(newOptions); + + // fetch new poll with options + const pollWithOptions = await Poll.findByPk(newPoll.id, { + include: { model: PollOption } + }); + + res.status(201).json({ + message: 'Poll duplicated successfully', + poll: pollWithOptions + }); + } catch (error) { + res.status(500).json({ error: 'Failed to duplicate poll' }); + } +}); + +// duplicate poll by id--------------------------- +router.post('/:pollId/duplicate', authenticateJWT, async (req, res) => { + const userId = req.user.id; + const { pollId } = req.params; + try { + // fetch poll and options + const poll = await Poll.findOne({ + where: { id: pollId, userId }, + include: { model: PollOption } + }); + if (!poll) { + return res.status(404).json({ error: 'Poll not found' }); + } + // create new poll + const newPoll = await Poll.create({ + title: poll.title + ' (copy)', + description: poll.description, + status: 'draft', + userId, + deadline: poll.deadline, + authRequired: poll.authRequired, + restricted: poll.restricted + }); + await newPoll.update({ slug: `${poll.slug}-copy-${newPoll.id}` }); + // copy options + const newOptions = poll.pollOptions.map((opt) => ({ + optionText: opt.optionText, + pollId: newPoll.id, + position: opt.position + })); + await PollOption.bulkCreate(newOptions); + // fetch new poll with options + const pollWithOptions = await Poll.findOne({ + where: { id: newPoll.id, userId }, + include: { model: PollOption } + }); + res.status(201).json({ + message: 'Poll duplicated successfully', + poll: pollWithOptions + }); + } catch (error) { + res.status(500).json({ error: 'Failed to duplicate poll' }); + } +}); + module.exports = router; From aab60c27505d2bedefc38b10650040fca6fe20a3 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 23 Jul 2025 00:26:51 -0400 Subject: [PATCH 112/148] algo complete --- api/polls.js | 193 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 177 insertions(+), 16 deletions(-) diff --git a/api/polls.js b/api/polls.js index 9abefab..65f00f4 100644 --- a/api/polls.js +++ b/api/polls.js @@ -2,6 +2,7 @@ const express = require("express"); const router = express.Router(); const { Poll, PollOption, Vote, VotingRank } = require("../database"); const { authenticateJWT } = require("../auth"); +const { where } = require("sequelize"); // Get all users Polls---------------------------- router.get("/", authenticateJWT, async (req, res) => { @@ -248,7 +249,7 @@ router.post("/:pollId/vote", authenticateJWT, async (req, res) => { console.log("Vote route hit"); const userId = req.user.id; console.log("User Id", userId) - + const { pollId } = req.params; console.log("Poll Id", pollId) const { rankings } = req.body; @@ -260,7 +261,7 @@ router.post("/:pollId/vote", authenticateJWT, async (req, res) => { // I know I am going to need the options that belong to this poll so I should query this poll and include the options const poll = await Poll.findOne({ - where: { userId: userId, id: pollId }, + where: { id: pollId }, include: { model: PollOption }, }); // return res.send(poll) this is returns the poll that I want @@ -282,26 +283,26 @@ router.post("/:pollId/vote", authenticateJWT, async (req, res) => { .json({ error: "A vote for this poll aready exist" }); } - const newVote = await Vote.create({userId, pollId, submitted: true}) + const newVote = await Vote.create({ userId, pollId, submitted: true }) // return res.send(newVote) // I created the a new vote and linked it to the user and the poll now i need to create a new vote_ranking and link it to this vote const formattedVotingRank = rankings.map((rank) => { - return {pollOptionId: rank.optionId, voteId: newVote.id, rank: rank.rank} + return { pollOptionId: rank.optionId, voteId: newVote.id, rank: rank.rank } }) console.log(formattedVotingRank) - const newVoteRanking= await VotingRank.bulkCreate(formattedVotingRank) + const newVoteRanking = await VotingRank.bulkCreate(formattedVotingRank) // return res.send(newVoteRanking) -const completedVote = await Vote.findOne({ - where: { userId: userId, pollId: pollId }, - include: { model: VotingRank, include: {model: PollOption, attributes: ['id', 'optionText']} }, - }); -// if (newVote.submitted === false) { -// vote.submitted = true; -// await newVotevote.save(); -// } - return res.send(completedVote); + const completedVote = await Vote.findOne({ + where: { userId: userId, pollId: pollId }, + include: { model: VotingRank, include: { model: PollOption, attributes: ['id', 'optionText'] } }, + }); + // if (newVote.submitted === false) { + // vote.submitted = true; + // await newVotevote.save(); + // } + return res.send(completedVote); // Recap I have the target Poll then I featch the vote that belongs to the user and this poll .. after getting the vote // i was able to fetech all rankings that belongs to this vote @@ -313,7 +314,7 @@ const completedVote = await Vote.findOne({ // }); // } - + // return res.send(vote); /// so from here i have succesfully submitted a vote now i need to go back to the top and create a new vote with new rankings since @@ -324,7 +325,167 @@ const completedVote = await Vote.findOne({ } - + }); +//------------------------------------ Calculate results -------------------------------------------------------- + +router.get("/:pollId/results", authenticateJWT, async (req, res) => { + + + const userId = req.user.id; + const { pollId } = req.params; + + const votes = await Vote.findAll({ + where: { pollId: pollId }, + include: { model: VotingRank }, + }); + + const allBallots = votes.map(vote => vote.votingRanks); + + const ballots = allBallots.map(ballot => { + return ballot + .sort((a, b) => a.rank - b.rank) + .map((element) => element.pollOptionId) + }) + + const options = await PollOption.findAll({ where: { pollId: pollId } }) + + const optionsMap = {}; + + for (const option of options) { + optionsMap[option.id] = + { + name: option.optionText, + count: 0, + eliminated: false, + }; + } + + + const totalVotes = ballots.length; + console.log(totalVotes) + const majorityThreshhold = Math.floor(totalVotes / 2) + 1; + console.log(majorityThreshhold) + + + let foundWinner = false; + + while (!foundWinner) { + + for (const option of Object.values(optionsMap)) { + option.count = 0; + } + + for (const ballot of ballots) { + for (const optionId of ballot) { + const option = optionsMap[optionId]; + if (!option.eliminated) { + option.count += 1; + break; + } + } + } + + + for (const [id, option] of Object.entries(optionsMap)) { + if (option.count > majorityThreshhold) { + foundWinner = true; + return res.json({ + status: "winner", + optionId: id, + name: option.name, + voteCount: option.count, + totalVotes, + }); + } + } + + + let minCount = Infinity; + let optionToEliminate = []; + + + for (const [optionId, option] of Object.entries(optionsMap)) { + if (!option.eliminated) { + if (option.count < minCount) { + minCount = option.count; + optionToEliminate = [optionId]; + } else if (option.count === minCount) { + optionToEliminate.push(optionId) + } + } + } + console.log(optionToEliminate) + console.log(minCount); + + const remaining = Object.values(optionsMap).filter((option) => !option.eliminated); + + if (remaining.length === optionToEliminate.length) { + foundWinner = true + return res.json({ + status: "tie", + tiedOptions: optionToEliminate.map((id) => ({ + optionId: id, + name: optionsMap[id].name, + voteCount: optionsMap[id].count, + })), + totalVotes, + }); + } + + + for (const optionId of optionToEliminate) { + optionsMap[optionId].eliminated = true + } + } + + + + + + + // return res.send(allBallots) + // return res.send(ballots) + // [ + // [ + // 7, + // 8, + // 9, + // 10 + // ], + // [ + // 10, + // 9, + // 8, + // 7 + // ] + // ] + // return res.send(options) + return res.send(optionsMap) + + // { + // "7": { + // "name": "Die Hard", + // "count": 0, + // "elimated": 0 + // }, + // "8": { + // "name": "Die Hard 2", + // "count": 0, + // "elimated": 0 + // }, + // "9": { + // "name": "Twilight", + // "count": 0, + // "elimated": 0 + // }, + // "10": { + // "name": "Spiderverse", + // "count": 0, + // "elimated": 0 + // } + // } +}) + module.exports = router; From b081d0b8eb67700253356b43cdceea38de127990 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 23 Jul 2025 06:32:30 -0400 Subject: [PATCH 113/148] feat: added a route to fetch votes --- api/polls.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/api/polls.js b/api/polls.js index e3d90d4..e6815de 100644 --- a/api/polls.js +++ b/api/polls.js @@ -320,6 +320,30 @@ router.post("/:pollId/vote", authenticateJWT, async (req, res) => { }); +// get the current user's submission for a poll +router.get("/:pollId/vote", authenticateJWT, async (req, res) => { + const userId = req.user.id; + const { pollId } = req.params; + try { + const vote = await Vote.findOne({ + where: { userId, pollId }, + include: { + model: VotingRank, + include: { + model: PollOption, + attributes: ["id", "optionText"], + }, + }, + }); + if (!vote) { + return res.status(404).json({ error: "No submission found for this poll" }); + } + res.json(vote); + } catch (error) { + console.error("Error fetching user submission:", error); + res.status(500).json({ error: "Failed to fetch user submission" }); + } +}); //------------------------------------ Calculate results -------------------------------------------------------- From 77b2066090c695a083269e0db197c4dfbe71d872 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 23 Jul 2025 11:53:47 -0400 Subject: [PATCH 114/148] fixed-position and send all votes --- api/polls.js | 18 +++++++++++------- database/index.js | 4 +++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/api/polls.js b/api/polls.js index e3d90d4..c59d1bb 100644 --- a/api/polls.js +++ b/api/polls.js @@ -2,7 +2,7 @@ const express = require("express"); const router = express.Router(); const { Poll, PollOption, Vote, VotingRank } = require("../database"); const { authenticateJWT } = require("../auth"); -const { where } = require("sequelize"); +const { where, Model } = require("sequelize"); // Get all users Polls---------------------------- router.get("/", authenticateJWT, async (req, res) => { @@ -84,7 +84,11 @@ router.get("/:pollId", authenticateJWT, async (req, res) => { id: pollId, userId: userId, }, - include: { model: PollOption }, + include: [ + { model: PollOption }, + { model: Vote }, + ], + }); if (!poll) { @@ -288,12 +292,12 @@ router.post("/:pollId/vote", authenticateJWT, async (req, res) => { const completedVote = await Vote.findOne({ where: { userId: userId, pollId: pollId }, - include: { model: VotingRank, include: { model: PollOption, attributes: ['id', 'optionText'] } }, + include: { model: VotingRank, include: { model: PollOption, attributes: ['id', 'optionText', 'position'] } }, }); - // if (newVote.submitted === false) { - // vote.submitted = true; - // await newVotevote.save(); - // } + // console.log(completedVote.votingRanks) + for (const votingRank of completedVote.votingRanks) { + votingRank.pollOption.position = votingRank.rank - 1; + } return res.send(completedVote); // Recap I have the target Poll then I featch the vote that belongs to the user and this poll .. after getting the vote diff --git a/database/index.js b/database/index.js index f046fab..cb15ff4 100644 --- a/database/index.js +++ b/database/index.js @@ -39,7 +39,9 @@ PollOption.belongsTo(Poll, { }); - +Poll.hasMany(Vote, { + foreignKey: "pollId" +}); //----- Vote Model-------- // one to many- each user can submit many votes From 739c938128a196202be8ca4e790ccea286fd088a Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Wed, 23 Jul 2025 12:25:43 -0400 Subject: [PATCH 115/148] fixed-paticipants --- api/polls.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/polls.js b/api/polls.js index c59d1bb..a68ab4f 100644 --- a/api/polls.js +++ b/api/polls.js @@ -286,6 +286,12 @@ router.post("/:pollId/vote", authenticateJWT, async (req, res) => { return { pollOptionId: rank.optionId, voteId: newVote.id, rank: rank.rank } }) + //increment participants + const allVotes = await Vote.findAll({ where: { pollId: pollId } }) + poll.participants = allVotes.length + await poll.save() + + console.log(formattedVotingRank) const newVoteRanking = await VotingRank.bulkCreate(formattedVotingRank) // return res.send(newVoteRanking) From bd9c7931ac748a30a5384e957bac2fdfc0805d74 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 23 Jul 2025 18:13:48 -0400 Subject: [PATCH 116/148] feat: fetch all user route --- api/users.js | 19 +++++++++++++++++++ app.js | 2 ++ auth/index.js | 24 ++++++++++++++++++------ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 api/users.js diff --git a/api/users.js b/api/users.js new file mode 100644 index 0000000..0fb62fb --- /dev/null +++ b/api/users.js @@ -0,0 +1,19 @@ +const express = require("express"); +const router = express.Router(); +const { User } = require("../database"); +const { authenticateJWT, isAdmin } = require("../auth"); + +// GET /api/users - admin only +router.get("/", authenticateJWT, isAdmin, async (req, res) => { + try { + const users = await User.findAll({ + attributes: ["id", "username", "email", "createdAt", "updatedAt", "status"] + }); + res.json(users); + } catch (error) { + console.error("Error fetching users:", error); + res.status(500).json({ error: "Failed to fetch users" }); + } +}); + +module.exports = router; diff --git a/app.js b/app.js index 0c4a32a..25f88fe 100644 --- a/app.js +++ b/app.js @@ -12,6 +12,7 @@ const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); const oAuthRouter = require("./auth/routes") +const usersRouter = require("./api/users") const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -45,6 +46,7 @@ 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("/auth", oAuthRouter); //mount oAuth router +app.use("/api/users", usersRouter); // mount users router // error handling middleware app.use((err, req, res, next) => { diff --git a/auth/index.js b/auth/index.js index 5faf45b..a90202b 100644 --- a/auth/index.js +++ b/auth/index.js @@ -24,6 +24,14 @@ const authenticateJWT = (req, res, next) => { }); }; +// Admin check middleware +function isAdmin(req, res, next) { + if (req.user && req.user.isAdmin) { + return next(); + } + return res.status(403).json({ error: "Admin access required" }); +} + // Auth0 authentication route router.post("/auth0", async (req, res) => { try { @@ -169,13 +177,13 @@ router.post("/login", async (req, res) => { } // Find user by username or email - const user = await User.findOne({ - where: { + const user = await User.findOne({ + where: { [Op.or]: [ { username: username }, - { email: username } - ] - } + { email: username }, + ], + }, }); if (!user) { return res.status(401).send({ error: "Invalid credentials" }); @@ -406,4 +414,8 @@ router.post("/signup/email", async (req, res) => { } }); -module.exports = { router, authenticateJWT }; +module.exports = { + router, + authenticateJWT, + isAdmin, +}; From 306a38c6ae2ffde663a14221f248423546d1891c Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 23 Jul 2025 18:50:04 -0400 Subject: [PATCH 117/148] feat: disable user route/prevents log in/any actions relates to polls beside viewing --- api/users.js | 19 ++++++++++++++++++- auth/index.js | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/api/users.js b/api/users.js index 0fb62fb..39c3ddf 100644 --- a/api/users.js +++ b/api/users.js @@ -3,7 +3,7 @@ const router = express.Router(); const { User } = require("../database"); const { authenticateJWT, isAdmin } = require("../auth"); -// GET /api/users - admin only +// GET /api/users , admin only router.get("/", authenticateJWT, isAdmin, async (req, res) => { try { const users = await User.findAll({ @@ -16,4 +16,21 @@ router.get("/", authenticateJWT, isAdmin, async (req, res) => { } }); +// PATCH /api/users/:userId/disable //// admin only +router.patch('/:userId/disable', authenticateJWT, isAdmin, async (req, res) => { + const { userId } = req.params; + try { + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + user.isDisable = true; + await user.save(); + res.json({ message: 'User account disabled', userId: user.id }); + } catch (error) { + console.error('Error disabling user:', error); + res.status(500).json({ error: 'Failed to disable user' }); + } +}); + module.exports = router; diff --git a/auth/index.js b/auth/index.js index a90202b..0431547 100644 --- a/auth/index.js +++ b/auth/index.js @@ -189,6 +189,11 @@ router.post("/login", async (req, res) => { return res.status(401).send({ error: "Invalid credentials" }); } + // check if the user is disabled + if (user.isDisable) { + return res.status(403).send({ error: "Account disabled. Please contact support." }); + } + // Check password if (!user.checkPassword(password)) { return res.status(401).send({ error: "Invalid credentials" }); From 7fad85db17f5cf20a1b7c097c80eef93478f1c34 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 23 Jul 2025 19:32:49 -0400 Subject: [PATCH 118/148] feat: admin route for all polls + disabl e polls --- api/polls.js | 40 ++++++++++++++++++++++++++++++++++++---- auth/index.js | 9 +++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/api/polls.js b/api/polls.js index ab3c1d8..d39b533 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); const { Poll, PollOption, Vote, VotingRank } = require("../database"); -const { authenticateJWT } = require("../auth"); +const { authenticateJWT, blockIfDisabled, isAdmin } = require("../auth"); const { where, Model } = require("sequelize"); // Get all users Polls---------------------------- @@ -102,7 +102,7 @@ router.get("/:pollId", authenticateJWT, async (req, res) => { }); // Create polls--------------------------- -router.post("/", authenticateJWT, async (req, res) => { +router.post("/", authenticateJWT, blockIfDisabled, async (req, res) => { const userId = req.user.id; const { title, description, deadline, status, options = [] } = req.body; @@ -164,6 +164,8 @@ router.patch("/:pollId", authenticateJWT, async (req, res) => { return res .status(403) .json({ error: "poll does not belong to this user" }); + } else if (updatePoll.isDisabled) { + return res.status(403).json({ error: "Poll is disabled and cannot be edited" }); } if (updatePoll.status === "draft") { @@ -237,7 +239,7 @@ router.delete("/:id", authenticateJWT, async (req, res) => { }); //-------------------------------------------------------------- Create A vote ballot -------------------------------------------- -router.post("/:pollId/vote", authenticateJWT, async (req, res) => { +router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) => { // rankings = [ // { optionId: 1, rank: 1 }, // { optionId: 2, rank: 2 }, @@ -357,7 +359,7 @@ router.get("/:pollId/vote", authenticateJWT, async (req, res) => { //------------------------------------ Calculate results -------------------------------------------------------- -router.get("/:pollId/results", authenticateJWT, async (req, res) => { +router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res) => { const userId = req.user.id; @@ -515,6 +517,20 @@ router.get("/:pollId/results", authenticateJWT, async (req, res) => { // } }) +// admin route to fetch all polls +router.get("/all", authenticateJWT, isAdmin, async (req, res) => { + try { + const polls = await Poll.findAll({ + include: [{ model: PollOption }], + attributes: { exclude: ["userId"] }, // exclude sensitive user info + }); + res.json(polls); + } catch (error) { + console.error("Error fetching all polls:", error); + res.status(500).json({ error: "Failed to fetch all polls" }); + } +}); + // duplicate poll endpoint router.post('/:id/duplicate', authenticateJWT, async (req, res) => { try { @@ -613,5 +629,21 @@ router.post('/:pollId/duplicate', authenticateJWT, async (req, res) => { } }); +// PATCH /api/polls/:pollId/disable - admin only +router.patch('/:pollId/disable', authenticateJWT, isAdmin, async (req, res) => { + const { pollId } = req.params; + try { + const poll = await Poll.findByPk(pollId); + if (!poll) { + return res.status(404).json({ error: 'Poll not found' }); + } + poll.isDisabled = true; + await poll.save(); + res.json({ message: 'Poll disabled', pollId: poll.id }); + } catch (error) { + console.error('Error disabling poll:', error); + res.status(500).json({ error: 'Failed to disable poll' }); + } +}); module.exports = router; diff --git a/auth/index.js b/auth/index.js index 0431547..d5b47ea 100644 --- a/auth/index.js +++ b/auth/index.js @@ -32,6 +32,14 @@ function isAdmin(req, res, next) { return res.status(403).json({ error: "Admin access required" }); } +// block disabled users from certain actions +function blockIfDisabled(req, res, next) { + if (req.user && req.user.isDisable) { + return res.status(403).json({ error: "Account disabled. Action not allowed." }); + } + next(); +} + // Auth0 authentication route router.post("/auth0", async (req, res) => { try { @@ -423,4 +431,5 @@ module.exports = { router, authenticateJWT, isAdmin, + blockIfDisabled, }; From a00969092bfc3154d02619079b6bdb65a05eb020 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 23 Jul 2025 19:43:33 -0400 Subject: [PATCH 119/148] fix: admin can toggle users disabled --- api/users.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/users.js b/api/users.js index 39c3ddf..479adbe 100644 --- a/api/users.js +++ b/api/users.js @@ -16,7 +16,7 @@ router.get("/", authenticateJWT, isAdmin, async (req, res) => { } }); -// PATCH /api/users/:userId/disable //// admin only +// PATCH /api/users/:userId/disable - admin only (toggle disable status) router.patch('/:userId/disable', authenticateJWT, isAdmin, async (req, res) => { const { userId } = req.params; try { @@ -24,12 +24,12 @@ router.patch('/:userId/disable', authenticateJWT, isAdmin, async (req, res) => { if (!user) { return res.status(404).json({ error: 'User not found' }); } - user.isDisable = true; + user.isDisable = !user.isDisable; await user.save(); - res.json({ message: 'User account disabled', userId: user.id }); + res.json({ message: `User account ${user.isDisable ? 'disabled' : 'enabled'}`, userId: user.id, isDisable: user.isDisable }); } catch (error) { - console.error('Error disabling user:', error); - res.status(500).json({ error: 'Failed to disable user' }); + console.error('Error toggling user disable:', error); + res.status(500).json({ error: 'Failed to toggle user disable' }); } }); From 9c03c69837327750310c7332951221e3c9c77048 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Wed, 23 Jul 2025 20:25:10 -0400 Subject: [PATCH 120/148] fix: CORS config to support both localhost and Vercel frontend origins --- app.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 25f88fe..8e92a6f 100644 --- a/app.js +++ b/app.js @@ -20,9 +20,20 @@ const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; // body parser middleware app.use(express.json()); +const allowedOrigins = [ + 'http://localhost:3000', + 'https://rankzilla-frontend.vercel.app' +]; + app.use( cors({ - origin: FRONTEND_URL, + origin: function (origin, callback) { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, credentials: true, }) ); From 43d01084ed8b5335640606466aaf7064e5714476 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 24 Jul 2025 00:49:26 -0400 Subject: [PATCH 121/148] feat:add route to fetch user profile info --- api/users.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/api/users.js b/api/users.js index 479adbe..9a32d28 100644 --- a/api/users.js +++ b/api/users.js @@ -33,4 +33,32 @@ router.patch('/:userId/disable', authenticateJWT, isAdmin, async (req, res) => { } }); +// GET /api/users/:userId - fetch user profile info +router.get('/:userId', authenticateJWT, async (req, res) => { + const { userId } = req.params; + try { + const user = await User.findByPk(userId, { + attributes: [ + 'id', + 'firstName', + 'lastName', + 'email', + 'username', + 'img', + 'isAdmin', + 'isDisable', + 'createdAt', + 'updatedAt' + ] + }); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(user); + } catch (error) { + console.error('Error fetching user profile:', error); + res.status(500).json({ error: 'Failed to fetch user profile' }); + } +}); + module.exports = router; From a41e868d6f4a5566f73a8109cfc3efe08223b32a Mon Sep 17 00:00:00 2001 From: rend1027 Date: Thu, 24 Jul 2025 06:00:07 -0400 Subject: [PATCH 122/148] refactor- attached results per round --- api/polls.js | 258 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 9 deletions(-) diff --git a/api/polls.js b/api/polls.js index d39b533..d3c8e4f 100644 --- a/api/polls.js +++ b/api/polls.js @@ -369,8 +369,122 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res where: { pollId: pollId }, include: { model: VotingRank }, }); + // [ + // { + // "id": 1, + // "submitted": true, + // "voterToken": null, + // "ipAddress": null, + // "userId": 2, + // "pollId": 1, + // "createdAt": "2025-07-24T07:04:10.459Z", + // "updatedAt": "2025-07-24T07:04:10.459Z", + // "votingRanks": [ + // { + // "id": 1, + // "voteId": 1, + // "pollOptionId": 1, + // "rank": 1, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 2, + // "voteId": 1, + // "pollOptionId": 2, + // "rank": 2, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 3, + // "voteId": 1, + // "pollOptionId": 3, + // "rank": 3, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 4, + // "voteId": 1, + // "pollOptionId": 4, + // "rank": 4, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 5, + // "voteId": 1, + // "pollOptionId": 5, + // "rank": 5, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 6, + // "voteId": 1, + // "pollOptionId": 5, + // "rank": 5, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // } + // ] + // } + // ] const allBallots = votes.map(vote => vote.votingRanks); + // [ + // [ + // { + // "id": 1, + // "voteId": 1, + // "pollOptionId": 1, + // "rank": 1, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 2, + // "voteId": 1, + // "pollOptionId": 2, + // "rank": 2, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 3, + // "voteId": 1, + // "pollOptionId": 3, + // "rank": 3, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 4, + // "voteId": 1, + // "pollOptionId": 4, + // "rank": 4, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 5, + // "voteId": 1, + // "pollOptionId": 5, + // "rank": 5, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // }, + // { + // "id": 6, + // "voteId": 1, + // "pollOptionId": 5, + // "rank": 5, + // "createdAt": "2025-07-24T07:04:10.475Z", + // "updatedAt": "2025-07-24T07:04:10.475Z" + // } + // ] + // ] const ballots = allBallots.map(ballot => { return ballot @@ -378,7 +492,71 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res .map((element) => element.pollOptionId) }) + // [ + // [ + // 7, + // 8, + // 9, + // 10 + // ], + // [ + // 10, + // 9, + // 8, + // 7 + // ] + // ] const options = await PollOption.findAll({ where: { pollId: pollId } }) + // [ + // { + // "id": 1, + // "optionText": "Demon Slayer", + // "position": 1, + // "pollId": 1, + // "createdAt": "2025-07-24T07:01:09.263Z", + // "updatedAt": "2025-07-24T07:01:09.263Z" + // }, + // { + // "id": 2, + // "optionText": "One Piece", + // "position": 2, + // "pollId": 1, + // "createdAt": "2025-07-24T07:01:09.263Z", + // "updatedAt": "2025-07-24T07:01:09.263Z" + // }, + // { + // "id": 3, + // "optionText": "AOT", + // "position": 3, + // "pollId": 1, + // "createdAt": "2025-07-24T07:01:09.263Z", + // "updatedAt": "2025-07-24T07:01:09.263Z" + // }, + // { + // "id": 4, + // "optionText": "Naruto", + // "position": 4, + // "pollId": 1, + // "createdAt": "2025-07-24T07:01:09.263Z", + // "updatedAt": "2025-07-24T07:01:09.263Z" + // }, + // { + // "id": 5, + // "optionText": "Devil May Cry", + // "position": 5, + // "pollId": 1, + // "createdAt": "2025-07-24T07:01:09.263Z", + // "updatedAt": "2025-07-24T07:01:09.263Z" + // }, + // { + // "id": 6, + // "optionText": "Castlevania", + // "position": 6, + // "pollId": 1, + // "createdAt": "2025-07-24T07:01:09.263Z", + // "updatedAt": "2025-07-24T07:01:09.263Z" + // } + // ] const optionsMap = {}; @@ -390,22 +568,60 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res eliminated: false, }; } - + // { + // "7": { + // "name": "Die Hard", + // "count": 0, + // "elimated": + // }, + // "8": { + // "name": "Die Hard 2", + // "count": 0, + // "elimated": + // }, + // "9": { + // "name": "Twilight", + // "count": 0, + // "elimated": + // }, + // "10": { + // "name": "Spiderverse", + // "count": 0, + // "elimated": + // } + // } const totalVotes = ballots.length; - console.log(totalVotes) + // console.log(totalVotes) const majorityThreshhold = Math.floor(totalVotes / 2) + 1; - console.log(majorityThreshhold) + // console.log(majorityThreshhold) let foundWinner = false; + // Front end needs the options that were eliminated for each round to include their count ------> + let roundNumber = 1; + let roundResults = []; + while (!foundWinner) { for (const option of Object.values(optionsMap)) { option.count = 0; } - + // [ + // [ + // 1, + // 2, + // 3, + // 4 + // ], + // [ + // 10, + // 9, + // 8, + // 7 + // ] + // ] for (const ballot of ballots) { for (const optionId of ballot) { const option = optionsMap[optionId]; @@ -415,8 +631,32 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res } } } + // { + // '1': { name: 'Demon Slayer', count: 1, eliminated: false }, + // '2': { name: 'One Piece', count: 0, eliminated: true }, + // '3': { name: 'AOT', count: 0, eliminated: true }, + // '4': { name: 'Naruto', count: 0, eliminated: true }, + // '5': { name: 'Devil May Cry', count: 0, eliminated: true }, + // '6': { name: 'Castlevania', count: 0, eliminated: true } + // } + + // Front end needs the options that were eliminated for each round to include their count ------> + const currentRound = { + round: roundNumber, + results: {}, + }; + for (const [id, option] of Object.entries(optionsMap)) { + currentRound.results[id] = { + name: option.name, + count: option.count, + eliminated: option.eliminated + }; + } + roundResults.push(currentRound); + roundNumber++; + for (const [id, option] of Object.entries(optionsMap)) { if (option.count > majorityThreshhold) { foundWinner = true; @@ -426,6 +666,7 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res name: option.name, voteCount: option.count, totalVotes, + rounds: roundResults }); } } @@ -445,8 +686,7 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res } } } - console.log(optionToEliminate) - console.log(minCount); + const remaining = Object.values(optionsMap).filter((option) => !option.eliminated); @@ -460,6 +700,7 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res voteCount: optionsMap[id].count, })), totalVotes, + rounds: roundResults }); } @@ -471,9 +712,8 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res - - - + // return res.send(options) + // return res.send(votes) // return res.send(allBallots) // return res.send(ballots) // [ From 7d4b81b991e54336444d469c56e7959c68ea27f6 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 24 Jul 2025 09:18:26 -0400 Subject: [PATCH 123/148] fix: now auth/me returns all user info instead just jwt --- auth/index.js | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/auth/index.js b/auth/index.js index d5b47ea..798cdb0 100644 --- a/auth/index.js +++ b/auth/index.js @@ -243,19 +243,30 @@ router.post("/logout", (req, res) => { }); // Get current user route (protected) -router.get("/me", (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" }); +router.get("/me", authenticateJWT, async (req, res) => { + try { + const user = await User.findByPk(req.user.id, { + attributes: [ + "id", + "firstName", + "lastName", + "email", + "username", + "img", + "isAdmin", + "isDisable", + "createdAt", + "updatedAt" + ] + }); + if (!user) { + return res.status(404).json({ error: "User not found" }); } - res.send({ user: user }); - }); + res.json(user); + } catch (error) { + console.error("error fetching user info:", error); + res.status(500).json({ error: "fail to fetch user info" }); + } }); // Helper function to check if input looks like email From 6c0361453cadaf8a0e988ee30d75017377fc3b12 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 24 Jul 2025 10:36:54 -0400 Subject: [PATCH 124/148] feat: updated seed for user infos --- database/seed.js | 55 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/database/seed.js b/database/seed.js index b1f47ef..15a0799 100644 --- a/database/seed.js +++ b/database/seed.js @@ -9,17 +9,49 @@ const seed = async () => { const users = await User.bulkCreate([ { - username: "admin", - passwordHash: User.hashPassword("admin123"), + username: "Tran", + passwordHash: User.hashPassword("tran123"), + email: "tran@example.com", + displayName: "Tran Vo", + firstName: "Tran", + lastName: "Vo", + img: "https://cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg", + isAdmin: true, + isDisable: false }, { - username: "user1", - passwordHash: User.hashPassword("user111") + username: "Flo", + passwordHash: User.hashPassword("flo123"), + email: "flo@example.com", + displayName: "Florencio Rendon", + firstName: "Florencio", + lastName: "Rendon", + img: "https://cdn2.thecatapi.com/images/MTY3ODIyMg.jpg", + isAdmin: false, + isDisable: false }, { - username: "user2", - passwordHash: User.hashPassword("user222") + username: "Olivia", + passwordHash: User.hashPassword("olivia123"), + email: "olivia@example.com", + displayName: "Olivia Wilson-Simmonds", + firstName: "Olivia", + lastName: "Wilson-Simmonds", + img: "https://cdn2.thecatapi.com/images/MTY3ODIyMw.jpg", + isAdmin: false, + isDisable: false }, + { + username: "Hai", + passwordHash: User.hashPassword("hai123"), + email: "hai@example.com", + displayName: "Hailia Sommerville", + firstName: "Hailia", + lastName: "Sommerville", + img: "https://cdn2.thecatapi.com/images/MTY3ODIyNA.jpg", + isAdmin: false, + isDisable: false + } ]); // deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), @@ -75,9 +107,10 @@ const seed = async () => { const deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); // 3days const userMap = { - admin: users[0], - user1: users[1], - user2: users[2], + Tran: users[0], + Flo: users[1], + Olivia: users[2], + Hai: users[3], }; for (const poll of pollData) { @@ -87,7 +120,6 @@ const seed = async () => { userId: userMap[poll.userKey].id, }) createdPolls[poll.key] = created; - // console.log(createdPolls.anime.id) }; @@ -259,6 +291,9 @@ const seed = async () => { console.log(`๐Ÿ‘ค Created ${users.length} users`); + users.forEach(u => { + console.log(`- ${u.username}: id=${u.id}, email=${u.email}, displayName=${u.displayName}, isAdmin=${u.isAdmin}, isDisable=${u.isDisable}`); + }); console.log(`Created ${Object.keys(createdPolls).length} polls`); console.log(`๐Ÿงพ Created ${PollOptions.length} poll options`); // Create more seed data here once you've created your models From 5aded2e8c828ddb0f113285b2049963c69c46aed Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 24 Jul 2025 10:43:02 -0400 Subject: [PATCH 125/148] fix: remove display name --- database/seed.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/database/seed.js b/database/seed.js index 15a0799..02af672 100644 --- a/database/seed.js +++ b/database/seed.js @@ -12,7 +12,6 @@ const seed = async () => { username: "Tran", passwordHash: User.hashPassword("tran123"), email: "tran@example.com", - displayName: "Tran Vo", firstName: "Tran", lastName: "Vo", img: "https://cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg", @@ -23,7 +22,6 @@ const seed = async () => { username: "Flo", passwordHash: User.hashPassword("flo123"), email: "flo@example.com", - displayName: "Florencio Rendon", firstName: "Florencio", lastName: "Rendon", img: "https://cdn2.thecatapi.com/images/MTY3ODIyMg.jpg", @@ -34,7 +32,6 @@ const seed = async () => { username: "Olivia", passwordHash: User.hashPassword("olivia123"), email: "olivia@example.com", - displayName: "Olivia Wilson-Simmonds", firstName: "Olivia", lastName: "Wilson-Simmonds", img: "https://cdn2.thecatapi.com/images/MTY3ODIyMw.jpg", @@ -45,7 +42,6 @@ const seed = async () => { username: "Hai", passwordHash: User.hashPassword("hai123"), email: "hai@example.com", - displayName: "Hailia Sommerville", firstName: "Hailia", lastName: "Sommerville", img: "https://cdn2.thecatapi.com/images/MTY3ODIyNA.jpg", @@ -63,7 +59,7 @@ const seed = async () => { description: "Rank your favorite animes!", participants: 0, status: "draft", - userKey: "user1", + userKey: "Tran", }, { @@ -72,14 +68,14 @@ const seed = async () => { description: "Rank your favorite movies!", participants: 0, status: "published", - userKey: "user2", + userKey: "Flo", }, { key: "bbq", title: "Best BBQ Item?", description: "Rank your favorite BBQ food!", status: "published", - userKey: "user1" + userKey: "Tran" }, { key: "authRequired", @@ -88,8 +84,7 @@ const seed = async () => { participants: 0, status: "published", authRequired: true, - userKey: "user2" - + userKey: "Flo" }, { key: "restricited", @@ -98,8 +93,7 @@ const seed = async () => { participants: 0, status: "published", restricted: true, - userKey: "user1" - + userKey: "Tran" }, ]; @@ -292,7 +286,7 @@ const seed = async () => { console.log(`๐Ÿ‘ค Created ${users.length} users`); users.forEach(u => { - console.log(`- ${u.username}: id=${u.id}, email=${u.email}, displayName=${u.displayName}, isAdmin=${u.isAdmin}, isDisable=${u.isDisable}`); + console.log(`- ${u.username}: id=${u.id}, email=${u.email}, firstName=${u.firstName}, lastName=${u.lastName}, displayName=${u.displayName || "(none)"}, img=${u.img}, isAdmin=${u.isAdmin}, isDisable=${u.isDisable}`); }); console.log(`Created ${Object.keys(createdPolls).length} polls`); console.log(`๐Ÿงพ Created ${PollOptions.length} poll options`); From de43d4e9504ff9f47736f815cadff6abb4a0f316 Mon Sep 17 00:00:00 2001 From: Heavenwilson Date: Thu, 24 Jul 2025 11:28:00 -0400 Subject: [PATCH 126/148] Merge branch 'main' of https://github.com/TPP-Team1/Rankzilla-Backend From 579647e86b2f86dd289667bfd68330a6093e97df Mon Sep 17 00:00:00 2001 From: Heavenwilson Date: Thu, 24 Jul 2025 12:56:35 -0400 Subject: [PATCH 127/148] since the beginning of time i had to fix all the routes cuz none of them were matching and all the attributes cuz none of them were working --- api/polls.js | 2 +- api/users.js | 2 +- auth/index.js | 5 +++++ database/models/user.js | 1 - 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/polls.js b/api/polls.js index d3c8e4f..20aeced 100644 --- a/api/polls.js +++ b/api/polls.js @@ -758,7 +758,7 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res }) // admin route to fetch all polls -router.get("/all", authenticateJWT, isAdmin, async (req, res) => { +router.get("/admin/all", authenticateJWT, isAdmin, async (req, res) => { try { const polls = await Poll.findAll({ include: [{ model: PollOption }], diff --git a/api/users.js b/api/users.js index 9a32d28..8d20d99 100644 --- a/api/users.js +++ b/api/users.js @@ -7,7 +7,7 @@ const { authenticateJWT, isAdmin } = require("../auth"); router.get("/", authenticateJWT, isAdmin, async (req, res) => { try { const users = await User.findAll({ - attributes: ["id", "username", "email", "createdAt", "updatedAt", "status"] + }); res.json(users); } catch (error) { diff --git a/auth/index.js b/auth/index.js index 798cdb0..fa16dc2 100644 --- a/auth/index.js +++ b/auth/index.js @@ -91,6 +91,7 @@ router.post("/auth0", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + isAdmin: user.isAdmin, }, JWT_SECRET, { expiresIn: "24h" } @@ -152,6 +153,7 @@ router.post("/signup", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + isAdmin: user.isAdmin, }, JWT_SECRET, { expiresIn: "24h" } @@ -214,6 +216,7 @@ router.post("/login", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + isAdmin: user.isAdmin, }, JWT_SECRET, { expiresIn: "24h" } @@ -329,6 +332,7 @@ router.post("/signup/username", async (req, res) => { id: user.id, username: user.username, email: user.email, + isAdmin: user.isAdmin, }, JWT_SECRET, { expiresIn: "24h" } @@ -411,6 +415,7 @@ router.post("/signup/email", async (req, res) => { id: user.id, username: user.username, email: user.email, + isAdmin: user.isAdmin, }, JWT_SECRET, { expiresIn: "24h" } diff --git a/database/models/user.js b/database/models/user.js index 4e3876b..156b772 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -73,7 +73,6 @@ const User = db.define("user", { }, { timestamps: true, - createdAt: 'created_at', } ); From 786abc5117dbc07086bb82e816b211277dd98cd9 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 24 Jul 2025 13:10:34 -0400 Subject: [PATCH 128/148] feat : route to update user info --- api/users.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/api/users.js b/api/users.js index 9a32d28..daa2c5f 100644 --- a/api/users.js +++ b/api/users.js @@ -60,5 +60,31 @@ router.get('/:userId', authenticateJWT, async (req, res) => { res.status(500).json({ error: 'Failed to fetch user profile' }); } }); +// PATCH /api/users/:userId - update user profile info +router.patch('/:userId', authenticateJWT, async (req, res) => { + const { userId } = req.params; + const { firstName, lastName, email, username, img } = req.body; + try { + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + if (firstName) user.firstName = firstName; + if (lastName) user.lastName = lastName; + if (email) user.email = email; + if (username) user.username = username; + if (img) user.img = img + await user.save(); + res.json({ message: 'User profile updated successfully', user }); + } catch (error) { + // handle unique constraint errors for email and username + if (error.name === 'SequelizeUniqueConstraintError') { + return res.status(400).json({ error: 'Email or username already in use' }); + } + console.error('Error updating user profile:', error); + res.status(500).json({ error: 'Failed to update user profile' }); + } +}); + module.exports = router; From 78b5fbe157dd3d24eb5ff8e49896b67bc9ca9474 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Thu, 24 Jul 2025 13:20:37 -0400 Subject: [PATCH 129/148] feat: add guard clause to ensure users can only edit their own profile via PATCH /api/users/:userId --- api/users.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/users.js b/api/users.js index daa2c5f..049d9d3 100644 --- a/api/users.js +++ b/api/users.js @@ -64,6 +64,10 @@ router.get('/:userId', authenticateJWT, async (req, res) => { router.patch('/:userId', authenticateJWT, async (req, res) => { const { userId } = req.params; const { firstName, lastName, email, username, img } = req.body; + // only allow user to edit their own account + if (parseInt(userId) !== req.user.id) { + return res.status(403).json({ error: 'you can only edit your own account' }); + } try { const user = await User.findByPk(userId); if (!user) { From ffc01551e35cc470b3f8118ff7b8aa28ce768037 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Thu, 24 Jul 2025 14:59:25 -0400 Subject: [PATCH 130/148] feat- created patch route to edit a drafted vote --- api/polls.js | 81 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/api/polls.js b/api/polls.js index d3c8e4f..49e0c08 100644 --- a/api/polls.js +++ b/api/polls.js @@ -244,17 +244,19 @@ router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) // { optionId: 1, rank: 1 }, // { optionId: 2, rank: 2 }, // ]; - console.log("Vote route hit"); + // console.log("Vote route hit"); const userId = req.user.id; - console.log("User Id", userId) + // console.log("User Id", userId) const { pollId } = req.params; console.log("Poll Id", pollId) - const { rankings } = req.body; + const { rankings, submitted } = req.body; if (!userId) { return res.status(404).json({ error: "Unathorized action" }); } + + try { // I know I am going to need the options that belong to this poll so I should query this poll and include the options @@ -281,7 +283,7 @@ router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) .json({ error: "A vote for this poll aready exist" }); } - const newVote = await Vote.create({ userId, pollId, submitted: true }) + const newVote = await Vote.create({ userId: userId, pollId: pollId, submitted: submitted }) // return res.send(newVote) // I created the a new vote and linked it to the user and the poll now i need to create a new vote_ranking and link it to this vote const formattedVotingRank = rankings.map((rank) => { @@ -289,9 +291,11 @@ router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) }) //increment participants - const allVotes = await Vote.findAll({ where: { pollId: pollId } }) - poll.participants = allVotes.length - await poll.save() + if (newVote.submitted === true) { + const allVotes = await Vote.findAll({ where: { pollId: pollId } }) + poll.participants = allVotes.length + await poll.save(); + } console.log(formattedVotingRank) @@ -327,11 +331,72 @@ router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) console.log("Fatal error"); return res.status(500).json({ error: "Failed to submit a vote" }); } +}); + +//-------------------------------------------------------------------------------- +router.patch("/poll/:pollId/vote/:voteId", authenticateJWT, async (req, res) => { + const userId = req.user.id; + const { voteId, pollId } = req.params; + const { submitted, rankings } = req.body; + + + + try { + const vote = await Vote.findOne({ + where: { id: voteId, pollId, userId }, + include: VotingRank, + }); + + if (!vote) { + return res.status(404).json({ error: "Vote not found" }); + } + if (vote.submitted) { + return res.status(403).json({ error: "Cannot update a submitted vote" }); + } + + // Update the vote (e.g., mark as submitted) + await vote.update({ submitted }); + // Replace rankings + if (Array.isArray(rankings)) { + // ๐Ÿงน Delete old rankings + await VotingRank.destroy({ where: { voteId } }); + //Create new rankings + const newRanks = rankings.map(rank => ({ + voteId, + pollOptionId: rank.optionId, + rank: rank.rank, + })); + await VotingRank.bulkCreate(newRanks); + } + // Re-fetch updated vote and send back + const updatedVote = await Vote.findOne({ + where: { id: voteId }, + include: { + model: VotingRank, + include: { + model: PollOption, + attributes: ['id', 'optionText', 'position'], + }, + }, + }); + + // positions for frontend display + for (const votingRank of updatedVote.votingRanks) { + votingRank.pollOption.position = votingRank.rank - 1; + } + + return res.json(updatedVote); + + } catch (error) { + console.error("Failed to update vote:", error); + return res.status(500).json({ error: "Failed to update vote" }); + } }); + // get the current user's submission for a poll router.get("/:pollId/vote", authenticateJWT, async (req, res) => { const userId = req.user.id; @@ -758,7 +823,7 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res }) // admin route to fetch all polls -router.get("/all", authenticateJWT, isAdmin, async (req, res) => { +router.get("/all/all", authenticateJWT, isAdmin, async (req, res) => { try { const polls = await Poll.findAll({ include: [{ model: PollOption }], From 0d4146c5b7a6c6075a440445955dc7965249d7aa Mon Sep 17 00:00:00 2001 From: rend1027 Date: Thu, 24 Jul 2025 23:58:48 -0400 Subject: [PATCH 131/148] changed cookies config for production --- auth/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/index.js b/auth/index.js index fa16dc2..b65babf 100644 --- a/auth/index.js +++ b/auth/index.js @@ -100,7 +100,7 @@ router.post("/auth0", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", + sameSite: "None", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); From c0977b9013112e701d47861df2782a24c293c2fe Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 00:49:48 -0400 Subject: [PATCH 132/148] fix: corrected patch endpoint --- api/polls.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api/polls.js b/api/polls.js index 84a0bbd..94c3848 100644 --- a/api/polls.js +++ b/api/polls.js @@ -334,7 +334,7 @@ router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) }); //-------------------------------------------------------------------------------- -router.patch("/poll/:pollId/vote/:voteId", authenticateJWT, async (req, res) => { +router.patch("/:pollId/vote/:voteId", authenticateJWT, async (req, res) => { const userId = req.user.id; const { voteId, pollId } = req.params; const { submitted, rankings } = req.body; @@ -823,9 +823,7 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res }) // admin route to fetch all polls - -router.get("/admin/all", authenticateJWT, isAdmin, async (req, res) => { - +router.get("/all/all", authenticateJWT, isAdmin, async (req, res) => { try { const polls = await Poll.findAll({ include: [{ model: PollOption }], From fee0813662bdd9371d7fe8631220c3b75735ef3d Mon Sep 17 00:00:00 2001 From: rend1027 Date: Fri, 25 Jul 2025 11:06:34 -0400 Subject: [PATCH 133/148] changed cookie config for prduction --- auth/index.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/index.js b/auth/index.js index b65babf..06443c0 100644 --- a/auth/index.js +++ b/auth/index.js @@ -100,7 +100,7 @@ router.post("/auth0", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "None", + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); diff --git a/package.json b/package.json index cc6c7b5..7065d79 100644 --- a/package.json +++ b/package.json @@ -33,4 +33,4 @@ "optionalDependencies": { "win-node-env": "^0.6.1" } -} +} \ No newline at end of file From cdab282a3959a02f4b79dc0be114cf63fa8e2de4 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Fri, 25 Jul 2025 11:15:51 -0400 Subject: [PATCH 134/148] change cookie setting again --- auth/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/index.js b/auth/index.js index 06443c0..37f23d6 100644 --- a/auth/index.js +++ b/auth/index.js @@ -424,7 +424,7 @@ router.post("/signup/email", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); From 364dd08b2b5356053eb487d4a12736f740d663ee Mon Sep 17 00:00:00 2001 From: rend1027 Date: Fri, 25 Jul 2025 11:21:45 -0400 Subject: [PATCH 135/148] changed cookie config one more time --- auth/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/index.js b/auth/index.js index 37f23d6..2f3b91c 100644 --- a/auth/index.js +++ b/auth/index.js @@ -225,7 +225,7 @@ router.post("/login", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); From 257754dcde7f42a549faecf9411330bce485597d Mon Sep 17 00:00:00 2001 From: rend1027 Date: Fri, 25 Jul 2025 14:02:45 -0400 Subject: [PATCH 136/148] changed cookie config --- auth/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/index.js b/auth/index.js index 2f3b91c..0d66a88 100644 --- a/auth/index.js +++ b/auth/index.js @@ -162,7 +162,7 @@ router.post("/signup", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: true, - sameSite: "strict", + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -341,7 +341,7 @@ router.post("/signup/username", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); From 79e6281bb580d21ff41df9f56d7ae5dbf498edd0 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Fri, 25 Jul 2025 18:58:59 -0400 Subject: [PATCH 137/148] fixed shared link --- api/polls.js | 99 ++++++++++++++++++++------------ auth/index.js | 30 +++++++--- database/models/vote.js | 6 +- middleware/checkDuplicateVote.js | 41 +++++++------ 4 files changed, 109 insertions(+), 67 deletions(-) diff --git a/api/polls.js b/api/polls.js index 84a0bbd..7d1e954 100644 --- a/api/polls.js +++ b/api/polls.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); const { Poll, PollOption, Vote, VotingRank } = require("../database"); -const { authenticateJWT, blockIfDisabled, isAdmin } = require("../auth"); +const { authenticateJWT, blockIfDisabled, isAdmin, optionalAuth } = require("../auth"); const { where, Model } = require("sequelize"); // Get all users Polls---------------------------- @@ -49,7 +49,7 @@ router.get("/draft", authenticateJWT, async (req, res) => { }); // Get polls by slug -router.get("/slug/:slug", authenticateJWT, async (req, res) => { +router.get("/slug/:slug", async (req, res) => { try { const pollSlug = req.params.slug; const poll = await Poll.findOne({ @@ -64,6 +64,12 @@ router.get("/slug/:slug", authenticateJWT, async (req, res) => { if (!poll) { return res.status(404).json({ error: "Poll not found" }); } + + if (poll.authRequired && !req.user) { + return res.status(401).json({ error: "Login required to view this poll" }); + } + + res.json(poll); } catch (error) { res.status(500).json({ error: "Failed to get poll" }); @@ -239,56 +245,66 @@ router.delete("/:id", authenticateJWT, async (req, res) => { }); //-------------------------------------------------------------- Create A vote ballot -------------------------------------------- -router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) => { +router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => { // rankings = [ // { optionId: 1, rank: 1 }, // { optionId: 2, rank: 2 }, // ]; // console.log("Vote route hit"); - const userId = req.user.id; - // console.log("User Id", userId) + const userId = req.user?.id || null; // null = guest + const { pollId } = req.params; - console.log("Poll Id", pollId) + // console.log("Poll Id", pollId) const { rankings, submitted } = req.body; - if (!userId) { - return res.status(404).json({ error: "Unathorized action" }); - } - try { - // I know I am going to need the options that belong to this poll so I should query this poll and include the options + // I know I am going to need the options that belong to this poll so I should query this poll and include the options const poll = await Poll.findOne({ where: { id: pollId }, include: { model: PollOption }, }); - // return res.send(poll) this is returns the poll that I want + + // check if this poll exist if (!poll) { return res.status(404).json({ error: "Poll not found" }); } + // I want to create a new vote only if a vote does not already exist + // Check if user already voted (only for logged-in users) + if (userId) { + const existingVote = await Vote.findOne({ + where: { userId, pollId } + }); + if (existingVote) { + return res.status(401).json({ error: "You already voted" }); + } + } - // Now that we have the poll we are workgin with I want to look at the vote that belongs to this poll - const vote = await Vote.findOne({ - where: { userId: userId, pollId: pollId }, - include: { model: VotingRank }, + + // Create a new vote + const newVote = await Vote.create({ + userId, // can be null + pollId, + submitted: submitted || false, }); - if (vote) { - return res - .status(401) - .json({ error: "A vote for this poll aready exist" }); - } - const newVote = await Vote.create({ userId: userId, pollId: pollId, submitted: submitted }) - // return res.send(newVote) + + // I created the a new vote and linked it to the user and the poll now i need to create a new vote_ranking and link it to this vote - const formattedVotingRank = rankings.map((rank) => { - return { pollOptionId: rank.optionId, voteId: newVote.id, rank: rank.rank } - }) + const formattedRanks = rankings.map(r => ({ + pollOptionId: r.optionId, + voteId: newVote.id, + rank: r.rank, + })); + // create the rankings + await VotingRank.bulkCreate(formattedRanks); + + //increment participants if (newVote.submitted === true) { @@ -298,13 +314,18 @@ router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) } - console.log(formattedVotingRank) - const newVoteRanking = await VotingRank.bulkCreate(formattedVotingRank) + // const newVoteRanking = await VotingRank.bulkCreate(formattedVotingRank) // return res.send(newVoteRanking) const completedVote = await Vote.findOne({ - where: { userId: userId, pollId: pollId }, - include: { model: VotingRank, include: { model: PollOption, attributes: ['id', 'optionText', 'position'] } }, + where: { id: newVote.id }, + include: { + model: VotingRank, + include: { + model: PollOption, + attributes: ['id', 'optionText', 'position'], + }, + }, }); // console.log(completedVote.votingRanks) for (const votingRank of completedVote.votingRanks) { @@ -328,12 +349,14 @@ router.post("/:pollId/vote", authenticateJWT, blockIfDisabled, async (req, res) /// so from here i have succesfully submitted a vote now i need to go back to the top and create a new vote with new rankings since // what I accomplish was to submit a vote predefined in the seed.js } catch (error) { - console.log("Fatal error"); - return res.status(500).json({ error: "Failed to submit a vote" }); + console.error("Guest vote error:", error); + res.status(500).json({ error: "Failed to submit vote" }); } }); -//-------------------------------------------------------------------------------- + + +//---------------------------------------Update drafted Vote----------------------------------------- router.patch("/poll/:pollId/vote/:voteId", authenticateJWT, async (req, res) => { const userId = req.user.id; const { voteId, pollId } = req.params; @@ -397,7 +420,7 @@ router.patch("/poll/:pollId/vote/:voteId", authenticateJWT, async (req, res) => }); -// get the current user's submission for a poll +// get the current user's submission for a poll----------------------------- router.get("/:pollId/vote", authenticateJWT, async (req, res) => { const userId = req.user.id; const { pollId } = req.params; @@ -822,7 +845,7 @@ router.get("/:pollId/results", authenticateJWT, blockIfDisabled, async (req, res // } }) -// admin route to fetch all polls +// admin route to fetch all polls--------------------------------------------------------------- router.get("/admin/all", authenticateJWT, isAdmin, async (req, res) => { @@ -838,7 +861,7 @@ router.get("/admin/all", authenticateJWT, isAdmin, async (req, res) => { } }); -// duplicate poll endpoint +// duplicate poll endpoint--------------------------------------------------------------- router.post('/:id/duplicate', authenticateJWT, async (req, res) => { try { const pollId = req.params.id; @@ -891,7 +914,7 @@ router.post('/:id/duplicate', authenticateJWT, async (req, res) => { } }); -// duplicate poll by id--------------------------- +// duplicate poll by id------------------------------------------------------------------------------------------ router.post('/:pollId/duplicate', authenticateJWT, async (req, res) => { const userId = req.user.id; const { pollId } = req.params; @@ -936,7 +959,7 @@ router.post('/:pollId/duplicate', authenticateJWT, async (req, res) => { } }); -// PATCH /api/polls/:pollId/disable - admin only +// PATCH /api/polls/:pollId/disable - admin only--------------------------------------------------------------- router.patch('/:pollId/disable', authenticateJWT, isAdmin, async (req, res) => { const { pollId } = req.params; try { diff --git a/auth/index.js b/auth/index.js index 0d66a88..dccbf43 100644 --- a/auth/index.js +++ b/auth/index.js @@ -7,6 +7,17 @@ const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; +const optionalAuth = (req, res, next) => { + const token = req.cookies.token; + if (!token) return next(); // No token = guest + + jwt.verify(token, process.env.JWT_SECRET, (err, user) => { + if (err) return next(); // Invalid token = still guest + req.user = user; + next(); + }); +}; + // Middleware to authenticate JWT tokens const authenticateJWT = (req, res, next) => { const token = req.cookies.token; @@ -119,7 +130,7 @@ router.post("/auth0", async (req, res) => { } }); -// Signup route +// Signup route-------------------------------------------- router.post("/signup", async (req, res) => { try { const { username, password } = req.body; @@ -176,7 +187,7 @@ router.post("/signup", async (req, res) => { } }); -// Login route +// Login route------------------------------------------------------- router.post("/login", async (req, res) => { try { const { username, password } = req.body; @@ -239,13 +250,13 @@ router.post("/login", async (req, res) => { } }); -// Logout route +// Logout route----------------------------------------------------- router.post("/logout", (req, res) => { res.clearCookie("token"); res.send({ message: "Logout successful" }); }); -// Get current user route (protected) +// Get current user route (protected)------------------------------ router.get("/me", authenticateJWT, async (req, res) => { try { const user = await User.findByPk(req.user.id, { @@ -277,7 +288,7 @@ const looksLikeEmail = (input) => { return input.includes("@"); }; -// Signup with username and password +// Signup with username and password------------------------------ router.post("/signup/username", async (req, res) => { try { const { username, password, firstName, lastName } = req.body; @@ -326,7 +337,7 @@ router.post("/signup/username", async (req, res) => { const user = await User.create(userData); - // Generate JWT token + // Generate JWT token--------------------------------------- const token = jwt.sign( { id: user.id, @@ -359,7 +370,7 @@ router.post("/signup/username", async (req, res) => { res.status(500).send({ error: "Internal server error" }); } }); - +//----------------------------------------------------------------------------------- // Helper function to validate email format const isValidEmail = (email) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -409,7 +420,7 @@ router.post("/signup/email", async (req, res) => { const user = await User.create(userData); - // Generate JWT token + // Generate JWT token--------------------------------------------------------- const token = jwt.sign( { id: user.id, @@ -442,10 +453,11 @@ router.post("/signup/email", async (req, res) => { res.status(500).send({ error: "Internal server error" }); } }); - +//--------------------------------------------------------------------------------- module.exports = { router, authenticateJWT, isAdmin, blockIfDisabled, + optionalAuth }; diff --git a/database/models/vote.js b/database/models/vote.js index e078ef9..2d49ef5 100644 --- a/database/models/vote.js +++ b/database/models/vote.js @@ -18,7 +18,11 @@ const Vote = db.define("vote", { }, userId: { type: DataTypes.INTEGER, - allowNull: false, + // allowNull: false, + }, + guestEmail: { + type: DataTypes.STRING, + unique: true, }, pollId: { type: DataTypes.INTEGER, diff --git a/middleware/checkDuplicateVote.js b/middleware/checkDuplicateVote.js index acfd3a9..b4381fa 100644 --- a/middleware/checkDuplicateVote.js +++ b/middleware/checkDuplicateVote.js @@ -1,8 +1,8 @@ const { Vote } = require("../database"); -const {Op} = require("sequelize"); //operator to specify multiple conditions +const { Op } = require("sequelize"); //operator to specify multiple conditions const checkDuplicateVote = async (req, res, next) => { - const userId = req.user ? req.user.id : null; + const userId = req.user ? req.user.id : null; const pollId = req.body.pollId; try { @@ -22,23 +22,23 @@ const checkDuplicateVote = async (req, res, next) => { } } else { - const guestUser = req.body.voterToken; - const ipAddress = req.ip; - //check if guest user already voted - const existingGuestVote = await Vote.findOne({ - where: { - pollId: pollId, - [Op.or]: [ - { voterToken: guestUser }, - { ipAddress: ipAddress } - ], - }, - }); - if (existingGuestVote) { - return res - .status(409) - .json({ error: "You have already voted on this poll." }); - } + const guestUser = req.body.voterToken; + const ipAddress = req.ip; + //check if guest user already voted + const existingGuestVote = await Vote.findOne({ + where: { + pollId: pollId, + [Op.or]: [ + { voterToken: guestUser }, + { ipAddress: ipAddress } + ], + }, + }); + if (existingGuestVote) { + return res + .status(409) + .json({ error: "You have already voted on this poll." }); + } } return next(); // proceed to the next middleware or route handler } catch (error) { @@ -47,4 +47,7 @@ const checkDuplicateVote = async (req, res, next) => { } }; + + + module.exports = checkDuplicateVote; From 918f75f18afe8b8cdc22f5fa1d70060412cf5825 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Fri, 25 Jul 2025 20:07:21 -0400 Subject: [PATCH 138/148] changed cookie config --- auth/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth/index.js b/auth/index.js index dccbf43..bea922e 100644 --- a/auth/index.js +++ b/auth/index.js @@ -111,7 +111,7 @@ router.post("/auth0", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "none", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -173,7 +173,7 @@ router.post("/signup", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: true, - sameSite: "none", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -236,7 +236,7 @@ router.post("/login", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "none", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -352,7 +352,7 @@ router.post("/signup/username", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "none", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); @@ -435,7 +435,7 @@ router.post("/signup/email", async (req, res) => { res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "none", + sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); From 6aa58884108dd56b3b29dc932ff109360718b621 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Fri, 25 Jul 2025 20:29:34 -0400 Subject: [PATCH 139/148] fix(cors): allow Vercel preview URLs for cross-site cookie support --- app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app.js b/app.js index 8e92a6f..b23aea9 100644 --- a/app.js +++ b/app.js @@ -25,6 +25,10 @@ const allowedOrigins = [ 'https://rankzilla-frontend.vercel.app' ]; + +const vercelPreviewPattern = /^https:\/\/rankzilla-frontend-[a-z0-9-]+\.vercel\.app$/; + + app.use( cors({ origin: function (origin, callback) { From 74bfd04deef23b5d2b8989b1fef830893843b8ef Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Fri, 25 Jul 2025 21:32:35 -0400 Subject: [PATCH 140/148] feat: added get route to fetch polls via filters --- api/polls.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index 7d1e954..ba1300e 100644 --- a/api/polls.js +++ b/api/polls.js @@ -419,7 +419,6 @@ router.patch("/poll/:pollId/vote/:voteId", authenticateJWT, async (req, res) => } }); - // get the current user's submission for a poll----------------------------- router.get("/:pollId/vote", authenticateJWT, async (req, res) => { const userId = req.user.id; @@ -976,4 +975,39 @@ router.patch('/:pollId/disable', authenticateJWT, isAdmin, async (req, res) => { } }); +// GET /api/polls/participated - Polls where current user has voted +router.get("/participated", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + // Find all votes by this user + const votes = await Vote.findAll({ where: { userId } }); + const pollIds = votes.map(v => v.pollId); + if (pollIds.length === 0) { + return res.json({ message: "No participated polls.", polls: [] }); + } + // find all polls where user has voted + const polls = await Poll.findAll({ where: { id: pollIds } }); + // label as participated + const labeled = polls.map(p => ({ ...p.toJSON(), participated: true })); + res.json({ message: "Participated polls retrieved.", polls: labeled }); + } catch (error) { + console.error("Error fetching participated polls:", error); + res.status(500).json({ error: "Failed to get participated polls" }); + } +}); + +// GET /api/polls/created - Polls created by current user +router.get("/created", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const polls = await Poll.findAll({ where: { userId } }); + // label as `created` + const labeled = polls.map(p => ({ ...p.toJSON(), created: true })); + res.json({ message: "Created polls retrieved.", polls: labeled }); + } catch (error) { + console.error("Error fetching created polls:", error); + res.status(500).json({ error: "Failed to get created polls" }); + } +}); + module.exports = router; From 71377e0b44c2baa9dfe9d132fd83bca97f1cee78 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Fri, 25 Jul 2025 22:57:31 -0400 Subject: [PATCH 141/148] fix: wrap user object in /auth/me response to match frontend expectations and persist login state --- api/testcookie.js | 1 + app.js | 6 +++++- auth/index.js | 3 ++- database/models/user.js | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 api/testcookie.js diff --git a/api/testcookie.js b/api/testcookie.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/testcookie.js @@ -0,0 +1 @@ + diff --git a/app.js b/app.js index b23aea9..0e2b693 100644 --- a/app.js +++ b/app.js @@ -32,7 +32,11 @@ const vercelPreviewPattern = /^https:\/\/rankzilla-frontend-[a-z0-9-]+\.vercel\. app.use( cors({ origin: function (origin, callback) { - if (!origin || allowedOrigins.includes(origin)) { + if ( + !origin || + allowedOrigins.includes(origin) || + vercelPreviewPattern.test(origin) + ) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); diff --git a/auth/index.js b/auth/index.js index bea922e..9652a5a 100644 --- a/auth/index.js +++ b/auth/index.js @@ -258,6 +258,7 @@ router.post("/logout", (req, res) => { // Get current user route (protected)------------------------------ router.get("/me", authenticateJWT, async (req, res) => { + res.set("Cache-Control", "no-store, no-cache, must-revalidate, private"); try { const user = await User.findByPk(req.user.id, { attributes: [ @@ -276,7 +277,7 @@ router.get("/me", authenticateJWT, async (req, res) => { if (!user) { return res.status(404).json({ error: "User not found" }); } - res.json(user); + res.json({ user }); } catch (error) { console.error("error fetching user info:", error); res.status(500).json({ error: "fail to fetch user info" }); diff --git a/database/models/user.js b/database/models/user.js index 156b772..cd3b967 100644 --- a/database/models/user.js +++ b/database/models/user.js @@ -73,6 +73,7 @@ const User = db.define("user", { }, { timestamps: true, + createdAt: "createdAt", } ); From 54ffcf7ebd0d21a89724b21d562a6489578fa55e Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Fri, 25 Jul 2025 23:09:42 -0400 Subject: [PATCH 142/148] feat: get participated polls --- api/polls.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/api/polls.js b/api/polls.js index a7f9271..a8455c5 100644 --- a/api/polls.js +++ b/api/polls.js @@ -10,7 +10,41 @@ router.get("/", authenticateJWT, async (req, res) => { try { const userPolls = await Poll.findAll({ where: { userId } }); - res.json(userPolls); + + //polls user voted on + const votedPolls = await Poll.findAll({ + include: { + model: Vote, + where: { userId }, + attributes: [], + }, + }); + + // Combine both results + const pollMap = new Map(); + + userPolls.forEach(poll => { + pollMap.set(poll.id, { + ...poll.toJSON(), + ownerId: userId, + participated: false, + }); + }); + + votedPolls.forEach(poll => { + if (!pollMap.has(poll.id)) { + pollMap.set(poll.id, { + ...poll.toJSON(), + ownerId: poll.userId, + participated: true, + }); + } + }); + + // Convert map back to array + const allPolls = Array.from(pollMap.values()); + + res.json(allPolls); } catch (error) { res.status(500).json({ error: "Failed to get all polls" }); } From 5a42076ffde2a325d758ac2233331c221c181d28 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sat, 26 Jul 2025 00:54:00 -0400 Subject: [PATCH 143/148] Votes from logged-in users now save userId correctly --- api/polls.js | 9 ++++++--- auth/index.js | 13 +++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/api/polls.js b/api/polls.js index 63c89a8..0a1720a 100644 --- a/api/polls.js +++ b/api/polls.js @@ -114,8 +114,7 @@ router.get("/slug/:slug", async (req, res) => { router.get("/:pollId", authenticateJWT, async (req, res) => { const userId = req.user.id; const { pollId } = req.params; - console.log(pollId); - console.log(userId); + console.log("Fetching poll with ID:", pollId); try { // fetch a spcific poll with options that belong to this user @@ -286,7 +285,7 @@ router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => // ]; // console.log("Vote route hit"); const userId = req.user?.id || null; // null = guest - + console.log("req.user:", req.user); const { pollId } = req.params; // console.log("Poll Id", pollId) @@ -307,6 +306,10 @@ router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => return res.status(404).json({ error: "Poll not found" }); } + //// validation for poll ended status--------------------------------------- + ///// here + + // I want to create a new vote only if a vote does not already exist // Check if user already voted (only for logged-in users) diff --git a/auth/index.js b/auth/index.js index 9652a5a..8193d68 100644 --- a/auth/index.js +++ b/auth/index.js @@ -9,11 +9,16 @@ const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; const optionalAuth = (req, res, next) => { const token = req.cookies.token; - if (!token) return next(); // No token = guest + console.log("Token from cookies:", token); + if (!token) return next(); + console.log("Cookies:", req.cookies); + console.log("Token:", req.cookies?.token); - jwt.verify(token, process.env.JWT_SECRET, (err, user) => { - if (err) return next(); // Invalid token = still guest - req.user = user; + jwt.verify(token, JWT_SECRET, (err, user) => { + if (!err) { + req.user = user; + console.log("Authenticated user:", user); + } next(); }); }; From 8fed6ac6e429121c9d758581b2f82c6944c071e1 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 26 Jul 2025 11:47:49 -0400 Subject: [PATCH 144/148] chore: nodemailer pkg installed, util folder + sendMail file created --- package-lock.json | 10 ++++++++++ package.json | 3 ++- utils/sendMail.js | 0 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 utils/sendMail.js diff --git a/package-lock.json b/package-lock.json index a8d821b..51559e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "express-session": "^1.18.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "nodemailer": "^7.0.5", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "pg": "^8.16.2", @@ -1091,6 +1092,15 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/nodemailer": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", diff --git a/package.json b/package.json index 7065d79..54e1923 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "express-session": "^1.18.1", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "nodemailer": "^7.0.5", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", "pg": "^8.16.2", @@ -33,4 +34,4 @@ "optionalDependencies": { "win-node-env": "^0.6.1" } -} \ No newline at end of file +} diff --git a/utils/sendMail.js b/utils/sendMail.js new file mode 100644 index 0000000..e69de29 From ed96f3d3c2022cd393c4d3f716b4119436742f55 Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 26 Jul 2025 11:54:29 -0400 Subject: [PATCH 145/148] feat: transporter added w gmail info --- utils/sendMail.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/utils/sendMail.js b/utils/sendMail.js index e69de29..b9d9ab1 100644 --- a/utils/sendMail.js +++ b/utils/sendMail.js @@ -0,0 +1,19 @@ +require(dotenv).config(); +const nodemailer = require("nodemailer"); + +const sendMail = async (to) => { + try{ + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, // email address + pass: process.env.EMAIL_PASS, // app password + }, + }); + } + catch (error) { + + } +}; + +export default sendMail; \ No newline at end of file From 6bd5587a6966e9337b154f5e298e86c592b1d16a Mon Sep 17 00:00:00 2001 From: hailia sommerville Date: Sat, 26 Jul 2025 12:36:07 -0400 Subject: [PATCH 146/148] feat: routing added, router mounted + test script --- api/email.js | 19 +++++++++++++++++++ app.js | 4 ++++ testMail.js | 12 ++++++++++++ utils/sendMail.js | 21 +++++++++++++++++---- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 api/email.js create mode 100644 testMail.js diff --git a/api/email.js b/api/email.js new file mode 100644 index 0000000..c9b0525 --- /dev/null +++ b/api/email.js @@ -0,0 +1,19 @@ +const express = require("express"); +const sendMail = require("../utils/sendMail"); +const router = express.Router(); + +router.post("/send-email", async (req, res) => { + const { to } = req.body; + + try{ + const result = await sendMail(to); + res.status(200).json({ message: "Email sent successfully", result }); + } + catch (error) { + console.error("Error sending email:", error); + res.status(500).json({ error: "Failed to send email" }); + } + +}); + +module.exports = router; \ No newline at end of file diff --git a/app.js b/app.js index b23aea9..0b7a2b4 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,7 @@ const { db } = require("./database"); const cors = require("cors"); const oAuthRouter = require("./auth/routes") const usersRouter = require("./api/users") +const emailRouter = require("./api/email"); const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -69,6 +70,9 @@ app.use((err, req, res, next) => { res.sendStatus(500); }); +// email router +app.use("/api/email", emailRouter); // mount email router + const runApp = async () => { try { await db.sync({ alter: true }); //update tables to match model diff --git a/testMail.js b/testMail.js new file mode 100644 index 0000000..b4db141 --- /dev/null +++ b/testMail.js @@ -0,0 +1,12 @@ +const sendMail = require("./utils/sendMail"); // Adjust path if needed + +async function test() { + try { + const info = await sendMail("rankzilla.results@gmail.com"); // Replace with your real email + console.log("Test email sent successfully:", info); + } catch (error) { + console.error("Failed to send test email:", error); + } +} + +test(); \ No newline at end of file diff --git a/utils/sendMail.js b/utils/sendMail.js index b9d9ab1..82ff12c 100644 --- a/utils/sendMail.js +++ b/utils/sendMail.js @@ -1,19 +1,32 @@ -require(dotenv).config(); +require("dotenv").config(); const nodemailer = require("nodemailer"); + const sendMail = async (to) => { try{ const transporter = nodemailer.createTransport({ service: "gmail", auth: { user: process.env.EMAIL_USER, // email address - pass: process.env.EMAIL_PASS, // app password + pass: process.env.EMAIL_PASSWORD, // app password }, }); + + const mailResults = { + from: `Rankzilla <${process.env.EMAIL_USER}>`, // sender address + to, + subject: "Poll Results Are Out!", + text: "Here are the results of the poll you voted on!", + }; + + const info = await transporter.sendMail(mailResults); + console.log("Email sent: ", info.messageId); + return info; } catch (error) { - + console.error("Error sending email:", error); + throw error; } }; -export default sendMail; \ No newline at end of file +module.exports = sendMail; \ No newline at end of file From 67ce7187bc5b344bddac338038cccb4255635193 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sun, 27 Jul 2025 02:34:13 -0400 Subject: [PATCH 147/148] fixed-unable to create a vote - participant count is noe being updated --- api/polls.js | 52 ++++++++++++++++++++++++++++++----------- app.js | 3 ++- database/models/vote.js | 7 ++++-- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/api/polls.js b/api/polls.js index 0a1720a..efa6e7c 100644 --- a/api/polls.js +++ b/api/polls.js @@ -32,13 +32,13 @@ router.get("/", authenticateJWT, async (req, res) => { }); votedPolls.forEach(poll => { - if (!pollMap.has(poll.id)) { - pollMap.set(poll.id, { - ...poll.toJSON(), - ownerId: poll.userId, - participated: true, - }); - } + if (!pollMap.has(poll.id)) { + pollMap.set(poll.id, { + ...poll.toJSON(), + ownerId: poll.userId, + participated: true, + }); + } }); // Convert map back to array @@ -143,7 +143,15 @@ router.get("/:pollId", authenticateJWT, async (req, res) => { // Create polls--------------------------- router.post("/", authenticateJWT, blockIfDisabled, async (req, res) => { const userId = req.user.id; - const { title, description, deadline, status, options = [] } = req.body; + const { + title, + description, + deadline, + status, + options = [], + authRequired, + allowSharedLinks, + } = req.body; if (status === "published" && options.length < 2) { return res.status(400).json({ @@ -156,6 +164,8 @@ router.post("/", authenticateJWT, blockIfDisabled, async (req, res) => { description, deadline, status, + authRequired, + allowSharedLinks, userId, }); //[opttion1, option2, option3] @@ -279,6 +289,7 @@ router.delete("/:id", authenticateJWT, async (req, res) => { //-------------------------------------------------------------- Create A vote ballot -------------------------------------------- router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => { + console.log("Received vote POST from guest/user:", req.user?.id || "guest"); // rankings = [ // { optionId: 1, rank: 1 }, // { optionId: 2, rank: 2 }, @@ -289,7 +300,7 @@ router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => const { pollId } = req.params; // console.log("Poll Id", pollId) - const { rankings, submitted } = req.body; + const { rankings, submitted, email } = req.body; try { @@ -300,12 +311,16 @@ router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => include: { model: PollOption }, }); - - // check if this poll exist if (!poll) { return res.status(404).json({ error: "Poll not found" }); } + // Prevent guest voting if auth is required + if (poll.authRequired && !userId) { + console.log("Blocked guest from voting on a restricted poll"); + return res.status(401).json({ error: "Authentication required to vote in this poll." }); + } + //// validation for poll ended status--------------------------------------- ///// here @@ -325,11 +340,13 @@ router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => // Create a new vote const newVote = await Vote.create({ - userId, // can be null + userId, // may be null pollId, submitted: submitted || false, + email, // store the voter's email }); + console.log("Creating vote:", { userId, email, pollId }); // I created the a new vote and linked it to the user and the poll now i need to create a new vote_ranking and link it to this vote @@ -341,7 +358,7 @@ router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => // create the rankings await VotingRank.bulkCreate(formattedRanks); - + console.log("Submitting rankings:", formattedRanks); //increment participants if (newVote.submitted === true) { @@ -391,7 +408,7 @@ router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => } }); -//-------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------Edit Draft--------------------------------------------------------------- router.patch("/:pollId/vote/:voteId", authenticateJWT, async (req, res) => { const userId = req.user.id; const { voteId, pollId } = req.params; @@ -415,6 +432,13 @@ router.patch("/:pollId/vote/:voteId", authenticateJWT, async (req, res) => { // Update the vote (e.g., mark as submitted) await vote.update({ submitted }); + if (!vote.submitted && submitted === true) { + const allVotes = await Vote.findAll({ where: { pollId, submitted: true } }) + const poll = await Poll.findByPk(pollId); + poll.participants = allVotes.length; + poll.save + } + // Replace rankings if (Array.isArray(rankings)) { // ๐Ÿงน Delete old rankings diff --git a/app.js b/app.js index 0e2b693..d4975ce 100644 --- a/app.js +++ b/app.js @@ -22,7 +22,8 @@ app.use(express.json()); const allowedOrigins = [ 'http://localhost:3000', - 'https://rankzilla-frontend.vercel.app' + 'http://[::1]:3000', + 'https://rankzilla-frontend.vercel.app', ]; diff --git a/database/models/vote.js b/database/models/vote.js index 2d49ef5..fc7244e 100644 --- a/database/models/vote.js +++ b/database/models/vote.js @@ -20,9 +20,12 @@ const Vote = db.define("vote", { type: DataTypes.INTEGER, // allowNull: false, }, - guestEmail: { + email: { type: DataTypes.STRING, - unique: true, + allowNull: true, // guests and users can choose to leave it blank + validate: { + isEmail: true, + }, }, pollId: { type: DataTypes.INTEGER, From 78be0c42da4f5d1e7dd1a469779e314c38f68cd9 Mon Sep 17 00:00:00 2001 From: rend1027 Date: Sun, 27 Jul 2025 04:54:38 -0400 Subject: [PATCH 148/148] Fix vote patch crash due to slugify on update --- api/polls.js | 275 +++++++++++++--------------------------- database/models/poll.js | 37 +++--- 2 files changed, 110 insertions(+), 202 deletions(-) diff --git a/api/polls.js b/api/polls.js index efa6e7c..10d0065 100644 --- a/api/polls.js +++ b/api/polls.js @@ -3,50 +3,61 @@ const router = express.Router(); const { Poll, PollOption, Vote, VotingRank } = require("../database"); const { authenticateJWT, blockIfDisabled, isAdmin, optionalAuth } = require("../auth"); const { where, Model } = require("sequelize"); +const { Op } = require("sequelize"); // Op = Sequelize's operator tool (like AND, OR, NOT, etc.) + // Get all users Polls---------------------------- router.get("/", authenticateJWT, async (req, res) => { const userId = req.user.id; try { - const userPolls = await Poll.findAll({ where: { userId } }); - - //polls user voted on - const votedPolls = await Poll.findAll({ - include: { - model: Vote, - where: { userId }, - attributes: [], - }, - }); + // Get all votes submitted by this user + const userVotes = await Vote.findAll({ where: { userId } }); - // Combine both results - const pollMap = new Map(); + // Extract poll IDs that this user has voted in + const votedPollIds = [...new Set(userVotes.map(vote => vote.pollId))]; + console.log("User voted in poll IDs:", votedPollIds); - userPolls.forEach(poll => { - pollMap.set(poll.id, { - ...poll.toJSON(), - ownerId: userId, - participated: false, - }); + // Get all polls created by this user + const createdPolls = await Poll.findAll({ + where: { userId }, + include: [{ model: PollOption }], // optional, fetches options if needed }); - votedPolls.forEach(poll => { - if (!pollMap.has(poll.id)) { - pollMap.set(poll.id, { - ...poll.toJSON(), - ownerId: poll.userId, - participated: true, - }); - } - }); + // Get all polls the user voted in, but DIDN'T create + const votedPolls = votedPollIds.length > 0 + ? await Poll.findAll({ + where: { + id: votedPollIds, // poll ID is in the user's vote list + userId: { [Op.ne]: userId }, // but NOT created by this user (Op.ne = "not equal") + }, + include: [{ model: PollOption }], + }) + : []; // fallback in case user has no votes + + console.log("Created polls:", createdPolls.length); + console.log("Voted (not owned) polls:", votedPolls.length); + + //Format results to mark `created` or `participated` for frontend filters + const formattedCreated = createdPolls.map(poll => ({ + ...poll.toJSON(), + created: true, + participated: false, + })); + + const formattedVoted = votedPolls.map(poll => ({ + ...poll.toJSON(), + created: false, + participated: true, + })); - // Convert map back to array - const allPolls = Array.from(pollMap.values()); + //Combine both into one clean array + const allPolls = [...formattedCreated, ...formattedVoted]; res.json(allPolls); } catch (error) { - res.status(500).json({ error: "Failed to get all polls" }); + console.error("Error in GET /api/polls:", error); + res.status(500).json({ error: "Failed to fetch polls" }); } }); @@ -287,193 +298,89 @@ router.delete("/:id", authenticateJWT, async (req, res) => { } }); -//-------------------------------------------------------------- Create A vote ballot -------------------------------------------- +// --------------------- Create Vote (Draft Only) --------------------- router.post("/:pollId/vote", optionalAuth, blockIfDisabled, async (req, res) => { - console.log("Received vote POST from guest/user:", req.user?.id || "guest"); - // rankings = [ - // { optionId: 1, rank: 1 }, - // { optionId: 2, rank: 2 }, - // ]; - // console.log("Vote route hit"); - const userId = req.user?.id || null; // null = guest - console.log("req.user:", req.user); - const { pollId } = req.params; - // console.log("Poll Id", pollId) - const { rankings, submitted, email } = req.body; - + const { email } = req.body; + const userId = req.user?.id || null; try { - - // I know I am going to need the options that belong to this poll so I should query this poll and include the options - const poll = await Poll.findOne({ - where: { id: pollId }, - include: { model: PollOption }, - }); - - if (!poll) { - return res.status(404).json({ error: "Poll not found" }); - } - - // Prevent guest voting if auth is required - if (poll.authRequired && !userId) { - console.log("Blocked guest from voting on a restricted poll"); - return res.status(401).json({ error: "Authentication required to vote in this poll." }); - } - - //// validation for poll ended status--------------------------------------- - ///// here - - - - // I want to create a new vote only if a vote does not already exist - // Check if user already voted (only for logged-in users) + // Block duplicate votes for authenticated users if (userId) { - const existingVote = await Vote.findOne({ - where: { userId, pollId } - }); + const existingVote = await Vote.findOne({ where: { pollId, userId } }); if (existingVote) { - return res.status(401).json({ error: "You already voted" }); + return res.status(409).json({ error: "Vote already exists" }); } } - - // Create a new vote - const newVote = await Vote.create({ - userId, // may be null - pollId, - submitted: submitted || false, - email, // store the voter's email - }); - - console.log("Creating vote:", { userId, email, pollId }); - - - // I created the a new vote and linked it to the user and the poll now i need to create a new vote_ranking and link it to this vote - const formattedRanks = rankings.map(r => ({ - pollOptionId: r.optionId, - voteId: newVote.id, - rank: r.rank, - })); - // create the rankings - await VotingRank.bulkCreate(formattedRanks); - - console.log("Submitting rankings:", formattedRanks); - - //increment participants - if (newVote.submitted === true) { - const allVotes = await Vote.findAll({ where: { pollId: pollId } }) - poll.participants = allVotes.length - await poll.save(); + // Validate email for guests + if (!userId && (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))) { + return res.status(400).json({ error: "Valid email is required for guests" }); } - - // const newVoteRanking = await VotingRank.bulkCreate(formattedVotingRank) - // return res.send(newVoteRanking) - - const completedVote = await Vote.findOne({ - where: { id: newVote.id }, - include: { - model: VotingRank, - include: { - model: PollOption, - attributes: ['id', 'optionText', 'position'], - }, - }, + // Create a draft vote + const vote = await Vote.create({ + userId, + pollId, + email: userId ? null : email, + submitted: false, }); - // console.log(completedVote.votingRanks) - for (const votingRank of completedVote.votingRanks) { - votingRank.pollOption.position = votingRank.rank - 1; - } - return res.send(completedVote); - // Recap I have the target Poll then I featch the vote that belongs to the user and this poll .. after getting the vote - // i was able to fetech all rankings that belongs to this vote - - // I now know that i am able to fetch all the data the I need .. however I am fetchin a vote that already exist but has not been submitted - // if (vote.submitted === true) { - // return res.status(401).json({ - // error: "this user has already submitted a vote for this poll", - // }); - // } - - - // return res.send(vote); - - /// so from here i have succesfully submitted a vote now i need to go back to the top and create a new vote with new rankings since - // what I accomplish was to submit a vote predefined in the seed.js - } catch (error) { - console.error("Guest vote error:", error); - res.status(500).json({ error: "Failed to submit vote" }); + return res.status(201).json(vote); + } catch (err) { + console.error("Vote creation error:", err); + return res.status(500).json({ error: "Failed to create vote" }); } }); -//--------------------------------------------------------------------------------Edit Draft--------------------------------------------------------------- -router.patch("/:pollId/vote/:voteId", authenticateJWT, async (req, res) => { - const userId = req.user.id; - const { voteId, pollId } = req.params; - const { submitted, rankings } = req.body; - - +// --------------------- Submit or Save Draft Vote --------------------- +router.patch("/:pollId/vote/:voteId", optionalAuth, async (req, res) => { + const { pollId, voteId } = req.params; + const { rankings, submitted } = req.body; + const userId = req.user?.id || null; try { + // Look up the vote โ€” must match pollId and either belong to logged-in user or be anonymous const vote = await Vote.findOne({ - where: { id: voteId, pollId, userId }, - include: VotingRank, + where: { + id: voteId, + pollId, + [Op.or]: [{ userId }, { userId: null }], + }, }); - if (!vote) { - return res.status(404).json({ error: "Vote not found" }); - } - if (vote.submitted) { - return res.status(403).json({ error: "Cannot update a submitted vote" }); - } - - // Update the vote (e.g., mark as submitted) - await vote.update({ submitted }); - - if (!vote.submitted && submitted === true) { - const allVotes = await Vote.findAll({ where: { pollId, submitted: true } }) - const poll = await Poll.findByPk(pollId); - poll.participants = allVotes.length; - poll.save - } + if (!vote) return res.status(404).json({ error: "Vote not found" }); + if (vote.submitted) return res.status(403).json({ error: "Vote already submitted" }); - // Replace rankings + // Update rankings if provided if (Array.isArray(rankings)) { - // ๐Ÿงน Delete old rankings await VotingRank.destroy({ where: { voteId } }); - //Create new rankings - const newRanks = rankings.map(rank => ({ + const newRanks = rankings.map((r) => ({ voteId, - pollOptionId: rank.optionId, - rank: rank.rank, + pollOptionId: r.optionId, + rank: r.rank, })); await VotingRank.bulkCreate(newRanks); } - // Re-fetch updated vote and send back - const updatedVote = await Vote.findOne({ - where: { id: voteId }, - include: { - model: VotingRank, - include: { - model: PollOption, - attributes: ['id', 'optionText', 'position'], - }, - }, - }); + // If submitted, finalize the vote + if (submitted === true) { + await vote.update({ submitted: true }); - // positions for frontend display - for (const votingRank of updatedVote.votingRanks) { - votingRank.pollOption.position = votingRank.rank - 1; - } + // Recount participants + const count = await Vote.count({ + where: { pollId, submitted: true }, + }); + await Poll.update({ participants: count }, { where: { id: pollId } }); - return res.json(updatedVote); + return res.status(200).json({ message: "Vote submitted" }); + } - } catch (error) { - console.error("Failed to update vote:", error); + // Otherwise, just saved as draft (only for logged-in users) + return res.status(200).json({ message: "Draft saved" }); + } catch (err) { + console.error("Vote update error:", err); return res.status(500).json({ error: "Failed to update vote" }); } }); diff --git a/database/models/poll.js b/database/models/poll.js index bae6812..e755233 100644 --- a/database/models/poll.js +++ b/database/models/poll.js @@ -41,7 +41,7 @@ const Poll = db.define("poll", { unique: true, allowNull: false, }, - + }, { timestamps: true, @@ -52,36 +52,37 @@ const Poll = db.define("poll", { function slugify(text) { return text - .toString() - .toLowerCase() - .trim() - .replace(/\s+/g, "-") - .replace(/[^\w\-]+/g, "") - .replace(/\-\-+/g, "-"); - } + .toString() + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^\w\-]+/g, "") + .replace(/\-\-+/g, "-"); +} - // generate a random string - function generateRandomString(length = 6) { +// generate a random string +function generateRandomString(length = 6) { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); + result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result; - } - - //auto-generate slug - Poll.beforeValidate(async (poll) => { - if (!poll.slug) { +} + +//auto-generate slug +Poll.beforeValidate(async (poll) => { + if (!poll.slug && poll.title) { // <- this prevents slugify(undefined) const baseSlug = slugify(poll.title); let uniqueSlug = baseSlug; - while (await Poll.findOne({where: {slug: uniqueSlug}})){ + while (await Poll.findOne({ where: { slug: uniqueSlug } })) { uniqueSlug = `${baseSlug}-${generateRandomString()}`; } + poll.slug = uniqueSlug; } - }) +});