From b9a969aae094dcc8d4856a3f155f0ec9015b1316 Mon Sep 17 00:00:00 2001 From: Tran Vo Date: Sat, 12 Jul 2025 10:53:30 -0400 Subject: [PATCH 001/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] : 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] : 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/111] 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/111] 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/111] 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/111] : 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/111] 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/111] : 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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/111] 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 7abe57dda90b2624d48f421415f52e476cc62273 Mon Sep 17 00:00:00 2001 From: Florencio Rendon Date: Thu, 17 Jul 2025 11:04:56 -0400 Subject: [PATCH 086/111] 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 087/111] 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 088/111] 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 089/111] 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 090/111] 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 091/111] 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 092/111] 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 093/111] 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 094/111] 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 095/111] 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 096/111] 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 097/111] 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 098/111] 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 099/111] 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 100/111] 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 101/111] 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 102/111] 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 103/111] 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 104/111] 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 105/111] 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 106/111] 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 107/111] 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 108/111] 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 109/111] 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 110/111] 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 111/111] 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;