diff --git a/api/index.js b/api/index.js index f08162e..eb7fad7 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,9 @@ const express = require("express"); const router = express.Router(); +const pollRouter = require("./polls"); const testDbRouter = require("./test-db"); router.use("/test-db", testDbRouter); +router.use("/polls", pollRouter) module.exports = router; diff --git a/api/poll.js b/api/poll.js new file mode 100644 index 0000000..e69de29 diff --git a/api/polls.js b/api/polls.js new file mode 100644 index 0000000..f9d2c92 --- /dev/null +++ b/api/polls.js @@ -0,0 +1,73 @@ +const express = require("express"); +const router = express.Router(); +const { Poll, PollOption } = require("../database"); +const { authenticateJWT } = require("../auth") + + +// 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) { + 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) + 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 fields and data are correct" + }) + } +}); + + + +//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" }) }; + + 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" }) }; + + await poll.destroy(); + + res.json({ message: "Draft poll deleted successfully" }); + + } + catch (error) { + res.status(500).json({ error: "Failed to delete draft poll" }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/app.js b/app.js index 5857036..cefe0a1 100644 --- a/app.js +++ b/app.js @@ -4,11 +4,14 @@ 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"); 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"; @@ -26,10 +29,22 @@ 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 app.use("/auth", authRouter); // mount auth router +app.use("/auth", oAuthRouter); //mount oAuth router // error handling middleware app.use((err, req, res, next) => { diff --git a/auth/index.js b/auth/index.js index 07968c5..5faf45b 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(); @@ -167,9 +168,15 @@ router.post("/login", async (req, res) => { return; } - // 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" }); } @@ -230,4 +237,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 }; diff --git a/auth/routes.js b/auth/routes.js new file mode 100644 index 0000000..e4095a3 --- /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/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}`, diff --git a/database/index.js b/database/index.js index e498df6..f046fab 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,111 @@ const db = require("./db"); -const User = require("./user"); +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"); +const RestrictedPollAccess = require('./models/restrictedPollAccess') + +//----- User Model-------- + +//One to many - user has many polls +User.hasMany(Poll, { + foreignKey: 'userId', + // onDelete: 'CASCADE' deletes poll is user is deleted +}); + + + +//----- 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", +}) + + +// 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', +}) + + + +//----- 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, + Poll, + PollOption, + Vote, + VotingRank, + RestrictedPollAccess, }; diff --git a/database/models/poll.js b/database/models/poll.js new file mode 100644 index 0000000..41969a6 --- /dev/null +++ b/database/models/poll.js @@ -0,0 +1,45 @@ +const { DataTypes } = require('sequelize'); +const db = require('../db'); + +// define the Poll model + +const Poll = db.define("poll", { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + // allowNull: true, + }, + participants: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + status: { + type: DataTypes.ENUM("draft", "published", "ended"), + allowNull: false, + }, + deadline: { + type: DataTypes.DATE, + // allowNull: false, + }, + 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, + }, +}, + { + timestamps: true, + } +); + +module.exports = Poll; \ No newline at end of file diff --git a/database/models/pollOption.js b/database/models/pollOption.js new file mode 100644 index 0000000..e6f3612 --- /dev/null +++ b/database/models/pollOption.js @@ -0,0 +1,30 @@ +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("pollOption", { + optionText: { + type: DataTypes.STRING, + allowNull: false, + }, + position: { + type: DataTypes.INTEGER, + allowNull: true, + }, + pollId: { + type: DataTypes.INTEGER, + allowNull: false, + } +}, + { + timestamps: true, + } +) + +module.exports = pollOption; \ No newline at end of file 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/user.js b/database/models/user.js similarity index 51% rename from database/user.js rename to database/models/user.js index 755c757..4e3876b 100644 --- a/database/user.js +++ b/database/models/user.js @@ -1,22 +1,46 @@ const { DataTypes } = require("sequelize"); -const db = require("./db"); +const db = require("../db"); const bcrypt = require("bcrypt"); const User = db.define("user", { username: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, unique: true, validate: { - len: [3, 20], + len: [1, 50] + } + }, + firstName: { + type: DataTypes.STRING, + // allowNull: false, + validate: { + notEmpty: true, + len: [1, 50], + }, + }, + lastName: { + type: DataTypes.STRING, + // allowNull: false, + validate: { + notEmpty: true, + len: [1, 50], }, }, email: { type: DataTypes.STRING, - allowNull: true, + // allowNull: false, unique: true, validate: { isEmail: true, + notEmpty: true, + }, + }, + passwordHash: { + type: DataTypes.STRING, + allowNull: true, + validate: { + notEmpty: true, }, }, auth0Id: { @@ -24,11 +48,34 @@ const User = db.define("user", { allowNull: true, unique: true, }, - passwordHash: { + googleId: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + 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', + } +); // Instance method to check password User.prototype.checkPassword = function (password) { diff --git a/database/models/vote.js b/database/models/vote.js new file mode 100644 index 0000000..e078ef9 --- /dev/null +++ b/database/models/vote.js @@ -0,0 +1,31 @@ +const { DataTypes } = require('sequelize'); +const db = require('../db'); + +// define the Vote model + +const Vote = db.define("vote", { + submitted: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + voterToken: { + type: DataTypes.STRING, + allowNull: true, + }, + ipAddress: { + type: DataTypes.STRING, + allowNull: true, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + pollId: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}, { + timestamps: true, +}); + +module.exports = Vote; \ No newline at end of file diff --git a/database/models/votingRank.js b/database/models/votingRank.js new file mode 100644 index 0000000..d5a03c2 --- /dev/null +++ b/database/models/votingRank.js @@ -0,0 +1,21 @@ +const { DataTypes } = require("sequelize"); +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, + } +}) + +module.exports = VotingRank; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index e58b595..3aed02f 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,6 @@ +const { Pool } = require("pg"); const db = require("./db"); -const { User } = require("./index"); +const { User, Poll, PollOption, Vote, VotingRank } = require("./index"); const seed = async () => { try { @@ -7,13 +8,259 @@ 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") + }, ]); - console.log(`๐Ÿ‘ค Created ${users.length} users`); + // deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + + const pollData = [ + { + key: "anime", + title: "Best Anime?", + description: "Rank your favorite animes!", + participants: 0, + status: "published", + userKey: "user1", + + }, + { + key: "movie", + title: "Best Movie?", + 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", + title: "authRequired true", + description: "?", + participants: 0, + status: "published", + authRequired: true, + userKey: "user2" + + }, + { + key: "restricited", + title: "restricted true", + description: "Rank your favorite anime of all time!", + participants: 0, + status: "published", + restricted: true, + userKey: "user1" + + }, + ]; + + const createdPolls = {}; + const deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000); // 3days + + const userMap = { + admin: users[0], + user1: users[1], + user2: users[2], + }; + + for (const poll of pollData) { + const created = await Poll.create({ + ...poll, + deadline, + userId: userMap[poll.userKey].id, + }) + createdPolls[poll.key] = created; + // console.log(createdPolls.anime.id) + }; + + + + const PollOptions = await PollOption.bulkCreate([ + { + optionText: "Demon Slayer", + position: 1, + pollId: createdPolls.anime.id, + }, + { + optionText: "One Piece", + position: 2, + pollId: createdPolls.anime.id, + }, + { + optionText: "AOT", + position: 3, + pollId: createdPolls.anime.id, + }, + { + optionText: "Naruto", + position: 4, + pollId: createdPolls.anime.id, + }, + { + optionText: "Devil May Cry", + position: 5, + pollId: createdPolls.anime.id, + }, + { + optionText: "Castlevania", + position: 6, + pollId: createdPolls.anime.id, + }, + { + optionText: "Die Hard", + pollId: createdPolls.movie.id + }, + { + optionText: "Die Hard 2", + pollId: createdPolls.movie.id, + }, + { + optionText: "Twilight", + pollId: createdPolls.movie.id, + }, + { + optionText: "Spiderverse", + pollId: createdPolls.movie.id, + }, + { + optionText: "Pork Ribs", + pollId: createdPolls.bbq.id, + }, + { + optionText: "Hot Dog", + pollId: createdPolls.bbq.id, + }, + { + optionText: "Cheeseburger", + pollId: createdPolls.bbq.id, + }, + { + optionText: "Suasage", + pollId: createdPolls.bbq.id, + }, + { + optionText: "a", + pollId: createdPolls.authRequired.id, + }, + { + optionText: "b", + pollId: createdPolls.authRequired.id, + }, + { + optionText: "c", + pollId: createdPolls.authRequired.id, + }, + { + optionText: "d", + pollId: createdPolls.authRequired.id, + }, + { + optionText: "1", + pollId: createdPolls.restricited.id, + }, + { + optionText: "2", + pollId: createdPolls.restricited.id, + }, + { + optionText: "3", + pollId: createdPolls.restricited.id, + + }, + { + optionText: "4", + pollId: createdPolls.restricited.id, + }, + + ]); + + + // 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 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 + }, + ]); + + + + console.log(`๐Ÿ‘ค Created ${users.length} users`); + console.log(`Created ${Object.keys(createdPolls).length} polls`); + console.log(`๐Ÿงพ Created ${PollOptions.length} poll options`); // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! diff --git a/package-lock.json b/package-lock.json index af0cf82..a8d821b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,25 @@ { - "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": { + "@react-oauth/google": "^0.12.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "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" }, @@ -26,6 +30,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", @@ -90,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", @@ -495,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", @@ -1067,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", @@ -1127,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", @@ -1136,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", @@ -1312,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", @@ -1336,6 +1477,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 +1561,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", @@ -1691,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", @@ -1713,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 7e0a0af..cc6c7b5 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,17 @@ "license": "ISC", "description": "", "dependencies": { + "@react-oauth/google": "^0.12.2", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "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" },