diff --git a/README.md b/README.md index b8ac36b..173d786 100644 --- a/README.md +++ b/README.md @@ -1,23 +1 @@ -# Capstone I Backend - -## Getting Started - -This project uses Express.js to serve up an API server, and Sequelize to connect to a PostgreSQL database. It uses JWTs for authentication with username and password. - -You will also need to create the database: by default it is called `capstone-1`, but you are welcome to rename it in `database/db.js` - -After that, you can get started with these commands - -``` -npm install # 📦 To install the packages -npm run seed # 🌱 To seed the database -npm run start-dev # 🚀 To start the server in development mode -``` - -This project runs in the Node.js runtime environment. We're not using Webpack here, but we are using a tool called nodemon, which re-runs our app whenever we save a file. You should see a message in the terminal telling you that the server is running on port 8080. - -When an error occurs on the backend (Express), you'll see a message in the terminal. When an error occurs on the frontend (React), you'll see that error in the browser. - -## Deployment - -This project has a vercel.json file, which will make it easier to deploy this project to Vercel. Check the video listed in the cohort repository for a walkthrough of how to connect your deployed Express server to Neon Postgres. +# FriendPollster Backend \ No newline at end of file diff --git a/api/admins.js b/api/admins.js new file mode 100644 index 0000000..c234718 --- /dev/null +++ b/api/admins.js @@ -0,0 +1,184 @@ +const express = require('express'); +const { authenticateJWT, requireAdmin } = require('../auth'); +const router = express.Router(); +const { User, Poll, Ballot, PollOption } = require("../database"); + +router.get( + '/users', + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const users = await User.findAll({ include: [Poll, Ballot] }); + res.status(200).send(users); + } catch (error) { + console.error("Error fetching users: ", error); + res.status(500).send("Error fetching users"); + } + } +); + +router.get( + '/polls', + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const polls = await Poll.findAll({ + include: [ + { + model: PollOption, + as: "PollOptions" + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"] + }, + { + model: Ballot, + required: false + } + ], + order: [["createdAt", "DESC"]] + }); + res.status(200).json(polls); + } catch (error) { + console.error("Error fetching polls for admin:", error); + res.status(500).json({ error: "Failed to fetch polls" }); + } + } +); + +router.patch( + "/polls/:id/disable", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id, { + include: [ + { + model: User, + as: "creator", + attributes: ["id", "username"] + } + ] + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + poll.isActive = !poll.isActive; + + if (!poll.isActive && !poll.endAt) { + poll.endAt = new Date(); + } + + await poll.save(); + + res.json({ + id: poll.id, + isActive: poll.isActive, + title: poll.title, + creator: poll.creator?.username, + message: `Poll ${poll.isActive ? 'enabled' : 'disabled'} successfully` + }); + } catch (err) { + console.error("Error toggling poll status:", err); + res.status(500).json({ error: "Failed to update poll status" }); + } + } +); + +router.patch( + "/polls/:id/close", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id, { + include: [PollOption, Ballot], + }); + if (!poll) return res.status(404).json({ error: "Poll not found" }); + if (poll.status === "closed") + return res.status(400).json({ error: "Poll already closed" }); + + poll.status = "closed"; + poll.isActive = false; + if (!poll.endAt) poll.endAt = new Date(); + await poll.save(); + + res.json(poll); + } catch (err) { + console.error("Error closing poll:", err); + res.status(500).json({ error: "Failed to close poll" }); + } + } +); + +router.delete( + "/polls/:id", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id, { + include: [ + { + model: User, + as: "creator", + attributes: ["id", "username"] + } + ] + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + const pollInfo = { + id: poll.id, + title: poll.title, + creator: poll.creator?.username + }; + + await poll.destroy(); + + res.json({ + message: "Poll deleted successfully", + deletedPoll: pollInfo + }); + } catch (err) { + console.error("Error deleting poll:", err); + res.status(500).json({ error: "Failed to delete poll" }); + } + } +); + +router.patch( + "/users/:id/disable", + authenticateJWT, + requireAdmin, + async (req, res) => { + try { + const target = await User.findByPk(req.params.id); + if (!target) return res.status(404).json({ error: "User not found" }); + + if (target.role === "admin") { + return res.status(403).json({ error: "Cannot disable an admin account" }); + } + + target.disabled = !target.disabled; + await target.save(); + + res.json({ id: target.id, disabled: target.disabled }); + } catch (err) { + console.error("Error disabling user:", err); + res.status(500).json({ error: "Failed to modify user" }); + } + } +); + +module.exports = router; \ No newline at end of file diff --git a/api/ballots.js b/api/ballots.js new file mode 100644 index 0000000..11d970e --- /dev/null +++ b/api/ballots.js @@ -0,0 +1,168 @@ +const express = require("express"); +const router = express.Router(); +const { + Poll, + User, + PollOption, + Ballot, + BallotRanking, + db, +} = require("../database"); +const { requireAuth } = require("../auth"); + + // _______ + // | | + // | ✔ | + // |_______| + // || + // \/ + // ______________________ + // | | + // | _____ | + // | | | | + // | | VOTE| | + // | |_____| | + // | | + // |______________________| + // \__________________/ + // || || + // || || + +router.get("/", async (req, res) => { + try { + const ballots = await Ballot.findAll({ + include: [{ model: BallotRanking }], + }); + console.log(`Found ${ballots.length} ballots`); + res.status(200).json(ballots); + } catch (error) { + console.error("Error fetching ballots:", error); + res.status(500).json({ + error: "Failed to fetch ballots", + message: "Check your database connection", + }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const ballot = await Ballot.findByPk(req.params.id, { + include: [{ model: BallotRanking }], + }); + if (!ballot) { + return res.status(404).json({ error: "Ballot not found" }); + } + res.status(200).json(ballot); + } catch (error) { + console.error(`Error fetching ballot`, error); + res.status(500).json({ + error: "Failed to fetch ballot", + message: "Check your database connection", + }); + } +}); + +router.post("/", async (req, res) => { + const { userId = null, pollId, rankings } = req.body; + + if (!pollId) return res.status(400).json({ error: "pollId is required" }); + + if (!Array.isArray(rankings) || rankings.length === 0) + return res + .status(400) + .json({ error: "rankings must be a non-empty array" }); + + for (const r of rankings) { + if (typeof r !== "object" || r.rank == null || r.pollOptionId == null) { + return res.status(400).json({ + error: "Each ranking must be an object { pollOptionId, rank }", + }); + } + } + + let tx; + try { + tx = await db.transaction(); + + const poll = await Poll.findByPk(pollId); + if (!poll) { + await tx.rollback(); + return res.status(404).json({ error: "Poll not found" }); + } + + if (!poll.isActive) { + await tx.rollback(); + return res.status(403).json({ + error: "This poll has been disabled by an administrator" + }); + } + + if (poll.endAt && new Date() > new Date(poll.endAt)) { + await tx.rollback(); + return res.status(400).json({ error: "Poll has ended" }); + } + + if (poll.status !== "published") { + await tx.rollback(); + return res.status(400).json({ error: "Cannot vote on unpublished polls" }); + } + + if (!poll.allowAnonymous && !userId) { + await tx.rollback(); + return res.status(401).json({ + error: "Please log in to vote on this poll" + }); + } + + if (userId && !poll.allowAnonymous) { + const existingBallot = await Ballot.findOne({ + where: { + user_id: userId, + poll_id: pollId + } + }); + + if (existingBallot) { + await tx.rollback(); + return res.status(400).json({ + error: "You have already voted on this poll" + }); + } + } + + const ballot = await Ballot.create( + { + user_id: userId, + poll_id: pollId, + }, + { transaction: tx } + ); + + const rankingRows = rankings.map(({ pollOptionId, rank }) => ({ + ballot_id: ballot.id, + option_id: pollOptionId, + rank, + })); + await BallotRanking.bulkCreate(rankingRows, { transaction: tx }); + + await tx.commit(); + + const result = await Ballot.findByPk(ballot.id, { + include: [{ model: BallotRanking, order: [["rank", "ASC"]] }], + }); + + return res.status(201).json({ + message: "Vote recorded successfully", + ballot: result + }); + } catch (err) { + if (tx) await tx.rollback(); + console.error("Failed to create ballot:", err); + return res.status(500).json({ + error: "Failed to submit ballot", + message: err.message, + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/follows.js b/api/follows.js new file mode 100644 index 0000000..42fe083 --- /dev/null +++ b/api/follows.js @@ -0,0 +1,172 @@ +const express = require("express"); +const router = express.Router(); +const { User, UserFollow } = require("../database"); +const { Op } = require("sequelize"); + +router.post("/", async (req, res) => { + try { + const { follower_id, following_id } = req.body; + + if (follower_id === following_id) { + return res.status(400).json({ error: "Cannot follow yourself" }); + } + + const existingFollow = await UserFollow.findOne({ + where: { follower_id, following_id } + }); + + if (existingFollow) { + return res.status(400).json({ error: "Already following this user" }); + } + + const follow = await UserFollow.create({ follower_id, following_id }); + + res.status(201).json({ + message: "Successfully followed user", + follow + }); + } catch (error) { + console.error("Error following user:", error); + res.status(500).json({ error: "Failed to follow user" }); + } +}); + +router.delete("/", async (req, res) => { + try { + const { follower_id, following_id } = req.body; + + const deletedCount = await UserFollow.destroy({ + where: { follower_id, following_id } + }); + + if (deletedCount === 0) { + return res.status(404).json({ error: "Follow relationship not found" }); + } + + res.json({ message: "Successfully unfollowed user" }); + } catch (error) { + console.error("Error unfollowing user:", error); + res.status(500).json({ error: "Failed to unfollow user" }); + } +}); + +router.get("/:userId/followers", async (req, res) => { + try { + const userId = req.params.userId; + + const user = await User.findByPk(userId, { + include: [{ + model: User, + as: "followers", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + }], + attributes: ["id", "username"] + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ + user: { id: user.id, username: user.username }, + followers: user.followers, + count: user.followers.length + }); + } catch (error) { + console.error("Error fetching followers:", error); + res.status(500).json({ error: "Failed to fetch followers" }); + } +}); + +router.get("/:userId/following", async (req, res) => { + try { + const userId = req.params.userId; + + const user = await User.findByPk(userId, { + include: [{ + model: User, + as: "following", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + }], + attributes: ["id", "username"] + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ + user: { id: user.id, username: user.username }, + following: user.following, + count: user.following.length + }); + } catch (error) { + console.error("Error fetching following:", error); + res.status(500).json({ error: "Failed to fetch following" }); + } +}); + +router.get("/:userId/friends", async (req, res) => { + try { + const userId = req.params.userId; + + const following = await UserFollow.findAll({ + where: { follower_id: userId }, + attributes: ["following_id"] + }); + + const followingIds = following.map(f => f.following_id); + + if (followingIds.length === 0) { + return res.json({ friends: [], count: 0 }); + } + + const mutualFollows = await UserFollow.findAll({ + where: { + follower_id: { [Op.in]: followingIds }, + following_id: userId + }, + include: [{ + model: User, + as: "follower", + attributes: ["id", "username", "imageUrl"] + }] + }); + + const friends = mutualFollows.map(mf => mf.follower); + + res.json({ + friends, + count: friends.length + }); + } catch (error) { + console.error("Error fetching friends:", error); + res.status(500).json({ error: "Failed to fetch friends" }); + } +}); + +router.get("/:userId/status/:targetUserId", async (req, res) => { + try { + const { userId, targetUserId } = req.params; + + const [isFollowing, isFollowed] = await Promise.all([ + UserFollow.findOne({ where: { follower_id: userId, following_id: targetUserId } }), + UserFollow.findOne({ where: { follower_id: targetUserId, following_id: userId } }) + ]); + + const areFriends = !!(isFollowing && isFollowed); + + res.json({ + isFollowing: !!isFollowing, + isFollowedBy: !!isFollowed, + areFriends + }); + } catch (error) { + console.error("Error checking follow status:", error); + res.status(500).json({ error: "Failed to check follow status" }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/index.js b/api/index.js index f08162e..199d3bb 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,16 @@ const express = require("express"); const router = express.Router(); -const testDbRouter = require("./test-db"); -router.use("/test-db", testDbRouter); +const usersRouter = require("./users"); +const pollsRouter = require("./polls"); +const pollOptionsRouter = require("./pollOptions"); +const ballotRouter = require("./ballots"); +const adminRouter = require("./admins") -module.exports = router; +router.use("/users", usersRouter); +router.use("/polls", pollsRouter); +router.use("/pollOptions", pollOptionsRouter); +router.use("/ballots", ballotRouter); +router.use("/admin", adminRouter) + +module.exports = router; \ No newline at end of file diff --git a/api/pollOptions.js b/api/pollOptions.js new file mode 100644 index 0000000..5059e64 --- /dev/null +++ b/api/pollOptions.js @@ -0,0 +1,56 @@ +const express = require("express"); +const router = express.Router(); +const { PollOption } = require("../database"); + +router.get("/", async (req, res) => { + try { + const options = await PollOption.findAll(); + console.log(`Found ${options.length} poll options`); + res.status(200).send(options); + } catch (error) { + console.error("Error fetching poll options:", error); + res.status(500).json({ + error: "Failed to fetch poll options", + message: "Check your database connection", + }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const option = await PollOption.findByPk(req.params.id); + if (!option) { + return res.status(404).json({ error: "Poll option not found" }); + } + res.status(200).send(option); + } catch (error) { + console.error("Error fetching poll option:", error); + res.status(500).json({ error: "Failed to fetch poll option by id" }); + } +}); + +router.post("/", async (req, res) => { + try { + const option = await PollOption.create(req.body); + res.status(201).send(option); + } catch (error) { + console.error("Error creating poll option:", error); + res.status(500).json({ error: "Failed to create poll option" }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const option = await PollOption.findByPk(req.params.id); + if (!option) { + return res.status(404).json({ error: "Poll option not found" }); + } + await option.destroy(); + res.status(200).json({ message: "Poll option deleted successfully" }); + } catch (error) { + console.error("Error deleting poll option:", error); + res.status(500).json({ error: "Failed to delete poll option" }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/polls.js b/api/polls.js new file mode 100644 index 0000000..546f2ef --- /dev/null +++ b/api/polls.js @@ -0,0 +1,596 @@ +const express = require("express"); +const router = express.Router(); +const { authenticateJWT, requireAuth, requireAdmin } = require("../auth"); +const { Poll, PollOption, Ballot, BallotRanking, User, PollAllowedUser, PollResult, PollResultValue, PollViewPermission, PollVotePermission, db } = require("../database"); +const { checkPollViewPermission, checkPollVotePermission } = require("../services/pollPermissions"); +const { Op } = require("sequelize"); + +router.use(authenticateJWT); + +router.get("/", async (req, res) => { + try { + const userId = req.user?.id; + + const polls = await Poll.findAll({ + where: { + [Op.or]: [ + { status: "published" }, // Published polls are visible to everyone (subject to view permissions) + { + status: "draft", + creator_id: userId || -1 // Draft polls only visible to their creator + } + ] + }, + include: [ + { + model: PollOption, + as: "PollOptions", + required: false + }, + { + model: Ballot, + required: false, + include: [ + { + model: BallotRanking, + required: false + } + ], + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"], + required: false + } + ], + }); + + const accessiblePolls = []; + + for (const poll of polls) { + if (poll.status === "draft") { + if (poll.creator_id === userId) { + accessiblePolls.push(poll); + } + } else { + const canView = await checkPollViewPermission(poll, userId); + if (canView) { + accessiblePolls.push(poll); + } + } + } + + res.status(200).send(accessiblePolls); + } catch (error) { + console.error("Error fetching polls:", error); + res.status(500).json({ + error: "Failed to fetch polls", + message: "Check your database connection", + }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const userId = req.user?.id; + + const poll = await Poll.findByPk(req.params.id, { + include: [ + { + model: PollOption, + as: "PollOptions", + required: false, + order: [['position', 'ASC']] + }, + { + model: Ballot, + required: false, + include: [ + { + model: BallotRanking, + required: false, + include: [ + { + model: PollOption, + required: false + } + ] + } + ], + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"], + required: false + } + ], + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + const canView = await checkPollViewPermission(poll, userId); + if (!canView) { + return res.status(403).json({ + error: "Access denied", + message: "You don't have permission to view this poll", + requiresLogin: !userId && poll.viewRestriction !== "public" + }); + } + + const canVote = await checkPollVotePermission(poll, userId); + const pollData = poll.toJSON(); + + if (!pollData.PollOptions && pollData.pollOptions) { + pollData.PollOptions = pollData.pollOptions; + delete pollData.pollOptions; + } + + pollData.permissions = { + canView: true, + canVote + }; + + res.status(200).json(pollData); + } catch (error) { + console.error("Error fetching poll by ID:", error); + res.status(500).json({ error: "Failed to fetch poll by id" }); + } +}); + +router.get("/search/users", async (req, res) => { + try { + const { q } = req.query; + + if (!q || q.length < 2) { + return res.json([]); + } + + const users = await User.findAll({ + where: { + username: { + [Op.iLike]: `%${q}%` + } + }, + attributes: ["id", "username", "imageUrl"], + limit: 10 + }); + + res.json(users); + } catch (error) { + console.error("Error searching users:", error); + res.status(500).json({ error: "Failed to search users" }); + } +}); + +router.post("/", requireAuth, async (req, res) => { + try { + const { + title, + description, + allowAnonymous, + status, + endAt, + viewRestriction, + voteRestriction, + customViewUsers, + customVoteUsers, + pollOptions, + } = req.body; + + const userId = req.user.id; + + console.log("Creating poll with data:", req.body); + + const result = await db.transaction(async (transaction) => { + const poll = await Poll.create({ + creator_id: userId, + title: title.trim(), + description: description ? description.trim() : null, + allowAnonymous: allowAnonymous || false, + status: status || "draft", + endAt: endAt ? new Date(endAt) : null, + viewRestriction: viewRestriction || "public", + voteRestriction: voteRestriction || "public", + isActive: true + }, { transaction }); + + if (pollOptions && Array.isArray(pollOptions) && pollOptions.length > 0) { + const optionsData = pollOptions.map(option => ({ + poll_id: poll.id, + text: option.text.trim(), + position: option.position || 1 + })); + + await PollOption.bulkCreate(optionsData, { transaction }); + console.log(`Created ${optionsData.length} options for poll ${poll.id}`); + } + + if (viewRestriction === "custom" && customViewUsers && customViewUsers.length > 0) { + const viewPermissions = customViewUsers.map(userId => ({ + poll_id: poll.id, + user_id: userId + })); + await PollViewPermission.bulkCreate(viewPermissions, { transaction }); + } + + if (voteRestriction === "custom" && customVoteUsers && customVoteUsers.length > 0) { + const votePermissions = customVoteUsers.map(userId => ({ + poll_id: poll.id, + user_id: userId + })); + await PollVotePermission.bulkCreate(votePermissions, { transaction }); + } + + return poll; + }); + + res.status(201).json({ + message: `Poll ${status === 'draft' ? 'saved as draft' : 'published'} successfully`, + poll: result, + id: result.id + }); + + } catch (error) { + console.error("Error creating poll:", error); + res.status(500).json({ + error: "Failed to create poll", + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +}); + +router.put("/:id", requireAuth, async (req, res) => { + try { + const pollId = req.params.id; + const userId = req.user.id; + + const poll = await Poll.findByPk(pollId); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + if (poll.creator_id !== userId) { + return res.status(403).json({ error: "You can only edit your own polls" }); + } + + const { + title, + description, + allowAnonymous, + status, + endAt, + viewRestriction, + voteRestriction, + customViewUsers, + customVoteUsers, + pollOptions + } = req.body; + + console.log("Updating poll with data:", req.body); + + const result = await db.transaction(async (transaction) => { + await poll.update({ + title: title ? title.trim() : poll.title, + description: description !== undefined ? (description ? description.trim() : null) : poll.description, + allowAnonymous: allowAnonymous !== undefined ? allowAnonymous : poll.allowAnonymous, + status: status || poll.status, + endAt: endAt !== undefined ? (endAt ? new Date(endAt) : null) : poll.endAt, + viewRestriction: viewRestriction || poll.viewRestriction, + voteRestriction: voteRestriction || poll.voteRestriction, + }, { transaction }); + + if (pollOptions && Array.isArray(pollOptions)) { + await PollOption.destroy({ + where: { poll_id: pollId }, + transaction + }); + + if (pollOptions.length > 0) { + const optionsData = pollOptions.map((option, index) => ({ + poll_id: pollId, + text: option.text.trim(), + position: option.position || index + 1, + })); + + await PollOption.bulkCreate(optionsData, { transaction }); + } + } + + if (viewRestriction === "custom") { + await PollViewPermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + + if (customViewUsers && Array.isArray(customViewUsers) && customViewUsers.length > 0) { + const viewPermissions = customViewUsers.map(userId => ({ + poll_id: pollId, + user_id: userId + })); + await PollViewPermission.bulkCreate(viewPermissions, { transaction }); + } + } else { + await PollViewPermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + } + + if (voteRestriction === "custom") { + await PollVotePermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + + if (customVoteUsers && Array.isArray(customVoteUsers) && customVoteUsers.length > 0) { + const votePermissions = customVoteUsers.map(userId => ({ + poll_id: pollId, + user_id: userId + })); + await PollVotePermission.bulkCreate(votePermissions, { transaction }); + } + } else { + await PollVotePermission.destroy({ + where: { poll_id: pollId }, + transaction + }); + } + + return poll; + }); + + const updatedPoll = await Poll.findByPk(pollId, { + include: [ + { + model: PollOption, + as: "PollOptions" + }, + { + model: User, + as: "creator", + attributes: ["id", "username", "imageUrl"] + } + ], + }); + + res.status(200).json({ + message: `Poll ${status === 'draft' ? 'saved as draft' : 'updated'} successfully`, + poll: updatedPoll + }); + + } catch (error) { + console.error("Error updating poll:", error); + res.status(500).json({ + error: "Failed to update poll", + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const poll = await Poll.findByPk(req.params.id); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + await poll.destroy(); + res.status(200).json({ message: "Poll deleted successfully" }); + } catch (error) { + console.error("Error deleting poll:", error); + res.status(500).json({ error: "Failed to delete poll" }); + } +}); + +router.get("/:pollId/results", async (req, res) => { + try { + const { pollId } = req.params; + + const poll = await Poll.findByPk(pollId, { + include: [ + { + model: PollOption, + include: [ + { + model: PollResultValue, + include: [ + { + model: PollResult, + where: { poll_id: pollId }, + required: false, + }, + ], + }, + ], + }, + ], + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + res.status(200).send(poll); + } catch (error) { + console.error("Error fetching poll results:", error); + res.status(500).json({ error: "Failed to fetch poll results" }); + } +}); + +router.post("/:pollId/vote", requireAuth, async (req, res) => { + try { + const { pollId } = req.params; + const { rankings } = req.body; + const userId = req.user.id; + + const poll = await Poll.findByPk(pollId, { + include: [{ model: PollOption, as: "PollOptions" }] + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + const canVote = await checkPollVotePermission(poll, userId); + if (!canVote) { + return res.status(403).json({ + error: "Access denied", + message: "You don't have permission to vote on this poll" + }); + } + + if (poll.endAt && new Date() > new Date(poll.endAt)) { + return res.status(400).json({ error: "Poll has ended" }); + } + + if (userId) { + const existingBallot = await Ballot.findOne({ + where: { poll_id: pollId, user_id: userId } + }); + + if (existingBallot) { + return res.status(400).json({ error: "You have already voted on this poll" }); + } + } + + const ballot = await Ballot.create({ + poll_id: pollId, + user_id: userId, + }); + + if (rankings && Array.isArray(rankings)) { + const ballotRankings = rankings.map(ranking => ({ + ballot_id: ballot.id, + option_id: ranking.option_id, + rank: ranking.rank, + })); + + await BallotRanking.bulkCreate(ballotRankings); + } + + res.status(201).json({ message: "Vote recorded successfully", ballot_id: ballot.id }); + } catch (error) { + console.error("Error recording vote:", error); + res.status(500).json({ error: "Failed to record vote" }); + } +}); + +router.get("/:pollId/my-ballot", requireAuth, async (req, res) => { + try { + const { pollId } = req.params; + const userId = req.user.id; + + const ballot = await Ballot.findOne({ + where: { poll_id: pollId, user_id: userId }, + include: [ + { + model: BallotRanking, + include: [{ model: PollOption }] + }, + ], + }); + + if (!ballot) { + return res.status(404).json({ error: "No ballot found for this poll" }); + } + + res.status(200).send(ballot); + } catch (error) { + console.error("Error fetching user ballot:", error); + res.status(500).json({ error: "Failed to fetch ballot" }); + } +}); + +router.post("/:pollId/allowed-users", async (req, res) => { + try { + const { pollId } = req.params; + const { userIds } = req.body; + + const poll = await Poll.findByPk(pollId); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + if (userIds && Array.isArray(userIds)) { + const allowedUsers = userIds.map(userId => ({ + poll_id: pollId, + user_id: userId, + })); + + await PollAllowedUser.bulkCreate(allowedUsers, { + ignoreDuplicates: true, + }); + } + + res.status(201).json({ message: "Users added to allowed list" }); + } catch (error) { + console.error("Error adding allowed users:", error); + res.status(500).json({ error: "Failed to add allowed users" }); + } +}); + +router.get("/:pollId/allowed-users", async (req, res) => { + try { + const { pollId } = req.params; + + const poll = await Poll.findByPk(pollId, { + include: [ + { + model: User, + as: "allowedUsers", + attributes: ["id", "username", "imageUrl"], + }, + ], + }); + + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + res.status(200).send(poll.allowedUsers); + } catch (error) { + console.error("Error fetching allowed users:", error); + res.status(500).json({ error: "Failed to fetch allowed users" }); + } +}); + +router.get("/:id/permissions", requireAuth, async (req, res) => { + try { + const pollId = req.params.id; + + const poll = await Poll.findByPk(pollId); + if (!poll) { + return res.status(404).json({ error: "Poll not found" }); + } + + const viewPermissions = await PollViewPermission.findAll({ + where: { poll_id: pollId }, + include: [{ + model: User, + attributes: ["id", "username", "imageUrl"] + }] + }); + + const votePermissions = await PollVotePermission.findAll({ + where: { poll_id: pollId }, + include: [{ + model: User, + attributes: ["id", "username", "imageUrl"] + }] + }); + + res.json({ + customViewUsers: viewPermissions.map(p => p.User), + customVoteUsers: votePermissions.map(p => p.User) + }); + + } catch (error) { + console.error("Error fetching poll permissions:", error); + res.status(500).json({ error: "Failed to fetch poll permissions" }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/test-db.js b/api/test-db.js deleted file mode 100644 index 4109c34..0000000 --- a/api/test-db.js +++ /dev/null @@ -1,25 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { User } = require("../database"); - -// You don't actually need this route, it's just a good way to confirm that your database connection is working. -// Feel free to delete this entire file. -router.get("/", async (req, res) => { - try { - const users = await User.findAll(); - console.log(`Found ${users.length} users`); - res.json({ - message: "You successfully connected to the database 🥳", - usersCount: users.length, - }); - } catch (error) { - console.error("Error fetching users:", error); - res.status(500).json({ - error: "Failed to fetch users", - message: - "Check your database connection, and consider running your seed file: npm run seed", - }); - } -}); - -module.exports = router; diff --git a/api/users.js b/api/users.js new file mode 100644 index 0000000..2eb21db --- /dev/null +++ b/api/users.js @@ -0,0 +1,322 @@ +const express = require("express"); +const router = express.Router(); +const { User, Poll, Ballot, UserFollow } = require("../database"); +const { authenticateJWT } = require("../auth"); + +router.post("/signup", async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required" }); + } + + if (password.length < 6) { + return res.status(400).json({ error: "Password must be at least 6 characters long" }); + } + + if (username.length < 3 || username.length > 20) { + return res.status(400).json({ error: "Username must be between 3 and 20 characters" }); + } + + // Check if user already exists + const existingUser = await User.findOne({ where: { username } }); + if (existingUser) { + return res.status(409).json({ error: "Username already exists" }); + } + + // Create new user + const passwordHash = await bcrypt.hash(password, 10); + const user = await User.create({ + username, + passwordHash, + role: 'user' // Set default role + }); + const token = jwt.sign( + { + id: user.id, + username: user.username, + role: user.role, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.status(201).json({ + message: "User created successfully", + token, + user: { + id: user.id, + username: user.username, + email: user.email, + bio: user.bio, + imageUrl: user.imageUrl, + role: user.role + } + }); + } catch (error) { + console.error("Signup error:", error); + res.status(500).json({ error: "Failed to create user" }); + } +}); + +router.post("/login", async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required" }); + } + + const user = await User.findOne({ where: { username } }); + if (!user) { + return res.status(401).json({ error: "Invalid credentials" }); + } + + // Check if user is disabled + if (user.disabled) { + return res.status(403).json({ error: "Account is disabled. Please contact support." }); + } + + const validPassword = await bcrypt.compare(password, user.passwordHash); + if (!validPassword) { + return res.status(401).json({ error: "Invalid credentials" }); + } + + const token = jwt.sign( + { id: user.id, username: user.username }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.json({ + token, + user: { + id: user.id, + username: user.username, + email: user.email, + bio: user.bio, + imageUrl: user.imageUrl, + role: user.role + } + }); + } catch (error) { + console.error("Login error:", error); + res.status(500).json({ error: "Login failed" }); + } +}); + +//get all users +router.get("/", async (req, res) => { + try { + const users = await User.findAll({ + include: [ + Poll, + Ballot, + { + model: User, + as: "followers", + attributes: ["id", "username", "imageUrl"], + through: { attributes: [] } + }, + { + model: User, + as: "following", + attributes: ["id", "username", "imageUrl"], + through: { attributes: [] } + } + ], + attributes: { exclude: ['passwordHash'] } + }); + res.status(200).send(users); + } catch (error) { + console.error("Error fetching users: ", error); + res.status(500).send("Error fetching users"); + } +}); + +//get a user by id +router.get("/:id", async (req, res) => { + try { + const user = await User.findByPk(req.params.id, { + include: [ + Poll, + Ballot, + { + model: User, + as: "followers", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + }, + { + model: User, + as: "following", + attributes: ["id", "username", "imageUrl"], + through: { attributes: ["created_at"] } + } + ], + attributes: { exclude: ['passwordHash'] } + }); + + if (!user) { + return res.status(404).send("User not found"); + } + + res.status(200).send(user); + } catch (error) { + console.error("Error fetching user by ID:", error); + res.status(500).send("Error fetching user"); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const user = await User.findByPk(req.params.id); + if (!user) { + return res.status(404).json({ error: "user not found" }); + } + await user.destroy(); + res.status(200).json({ message: "User deleted successfully" }); + } catch (error) { + res.status(500).json({ error: "Failed to delete a user" }); + } +}); + +//create a user +router.post("/", async (req, res) => { + try { + const { username, email, password } = req.body; + + const passwordHash = User.hashPassword(password); + + const newUser = { username, email, passwordHash }; + + const savedUser = await User.create(newUser); + + res.status(201).send(savedUser); + } catch (error) { + console.error("Error creating user:", error); + res.status(500).send("Error creating user"); + } +}); + +router.patch("/:id", authenticateJWT, async (req, res) => { + try { + const userId = req.params.id; + const { username, email, bio, imageUrl } = req.body; + + console.log("Updating user:", userId, req.body); + + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + if (req.user.id !== parseInt(userId) && req.user.role !== 'admin') { + return res.status(403).json({ error: "Unauthorized to update this profile" }); + } + + const updatedData = {}; + + if (username !== undefined) { + const trimmedUsername = username.trim(); + if (trimmedUsername.length < 3) { + return res.status(400).json({ error: "Username must be at least 3 characters long" }); + } + + const existingUser = await User.findOne({ + where: { username: trimmedUsername }, + attributes: ['id'] + }); + + if (existingUser && existingUser.id !== parseInt(userId)) { + return res.status(400).json({ error: "Username already taken" }); + } + + updatedData.username = trimmedUsername; + } + + if (email !== undefined) { + const trimmedEmail = email.trim(); + updatedData.email = trimmedEmail === '' ? null : trimmedEmail; + } + + if (bio !== undefined) { + const trimmedBio = bio.trim(); + updatedData.bio = trimmedBio === '' ? null : trimmedBio; + } + + if (imageUrl !== undefined) { + const trimmedImageUrl = imageUrl.trim(); + updatedData.imageUrl = trimmedImageUrl === '' ? null : trimmedImageUrl; + } + + await user.update(updatedData); + + const updatedUser = await User.findByPk(userId, { + attributes: { exclude: ['passwordHash'] } + }); + + console.log("User updated successfully:", updatedUser.username); + + res.json(updatedUser); + } catch (error) { + console.error("Error updating user:", error); + res.status(500).json({ + error: "Failed to update profile", + details: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' + }); + } +}); + +//route to get user stats (followers, following, friend count) +router.get("/:id/stats", async (req, res) => { + try { + const userId = req.params.id; + + const user = await User.findByPk(userId, { + include: [ + { + model: User, + as: "followers", + attributes: ["id"], + through: { attributes: [] } + }, + { + model: User, + as: "following", + attributes: ["id"], + through: { attributes: [] } + }, + { + model: Poll, + attributes: ["id"] + } + ], + attributes: ["id", "username"] + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + const followingIds = user.following.map(f => f.id); + const followerIds = user.followers.map(f => f.id); + const friendIds = followingIds.filter(id => followerIds.includes(id)); + + res.json({ + userId: user.id, + username: user.username, + followersCount: user.followers.length, + followingCount: user.following.length, + friendsCount: friendIds.length, + pollsCount: user.polls.length + }); + + } catch (error) { + console.error("Error fetching user stats:", error); + res.status(500).json({ error: "Failed to fetch user stats" }); + } +}); + +module.exports = router; diff --git a/app.js b/app.js index 5857036..26b5ca6 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,8 @@ const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); +const followsRouter = require("./api/follows"); + const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -18,7 +20,7 @@ app.use(express.json()); app.use( cors({ - origin: FRONTEND_URL, + origin: [FRONTEND_URL, "http://localhost:3000", "http://127.0.0.1:3000"], credentials: true, }) ); @@ -31,6 +33,8 @@ app.use(express.static(path.join(__dirname, "public"))); // serve static files f app.use("/api", apiRouter); // mount api router app.use("/auth", authRouter); // mount auth router +app.use("/api/follows", followsRouter); + // error handling middleware app.use((err, req, res, next) => { console.error(err.stack); @@ -39,7 +43,8 @@ app.use((err, req, res, next) => { const runApp = async () => { try { - await db.sync(); + // we have to keep it true, so that schema always matches the code before we happy with it + await db.sync({alter: true}); console.log("✅ Connected to the database"); app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); @@ -51,4 +56,4 @@ const runApp = async () => { runApp(); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/auth/index.js b/auth/index.js index 07968c5..6366d32 100644 --- a/auth/index.js +++ b/auth/index.js @@ -1,26 +1,53 @@ const express = require("express"); const jwt = require("jsonwebtoken"); +const bcrypt = require("bcrypt"); const { User } = require("../database"); const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; -// Middleware to authenticate JWT tokens -const authenticateJWT = (req, res, next) => { - const token = req.cookies.token; +const authenticateJWT = async (req, res, next) => { + let token = null; + + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + } + + if (!token && req.cookies && req.cookies.token) { + token = req.cookies.token; + } + //commit + + if (token) { + try { + const decoded = jwt.verify(token, JWT_SECRET); + const user = await User.findByPk(decoded.id); + if (user) { + req.user = user; + } + } catch (error) { + console.error("JWT verification failed:", error); + } + } + + next(); +}; - if (!token) { - return res.status(401).send({ error: "Access token required" }); +// Middleware that requires authentication +const requireAuth = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ error: "Authentication required" }); } + next(); +}; - jwt.verify(token, JWT_SECRET, (err, user) => { - if (err) { - return res.status(403).send({ error: "Invalid or expired token" }); - } - req.user = user; - next(); - }); +const requireAdmin = (req, res, next) => { + if (!req.user || req.user.role !== "admin") { + return res.status(403).send({ error: "Admin privileges required" }); + } + next(); }; // Auth0 authentication route @@ -32,30 +59,24 @@ router.post("/auth0", async (req, res) => { return res.status(400).send({ error: "Auth0 ID is required" }); } - // Try to find existing user by auth0Id first let user = await User.findOne({ where: { auth0Id } }); if (!user && email) { - // If no user found by auth0Id, try to find by email user = await User.findOne({ where: { email } }); - if (user) { - // Update existing user with auth0Id user.auth0Id = auth0Id; await user.save(); } } if (!user) { - // Create new user if not found const userData = { auth0Id, email: email || null, - username: username || email?.split("@")[0] || `user_${Date.now()}`, // Use email prefix as username if no username provided - passwordHash: null, // Auth0 users don't have passwords + username: username || email?.split("@")[0] || `user_${Date.now()}`, + passwordHash: null, }; - // Ensure username is unique let finalUsername = userData.username; let counter = 1; while (await User.findOne({ where: { username: finalUsername } })) { @@ -67,37 +88,42 @@ router.post("/auth0", async (req, res) => { user = await User.create(userData); } - // Generate JWT token with auth0Id included const token = jwt.sign( { id: user.id, username: user.username, auth0Id: user.auth0Id, email: user.email, + role: user.role, }, JWT_SECRET, { expiresIn: "24h" } ); + // Set cookie res.cookie("token", token, { httpOnly: true, secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 24 * 60 * 60 * 1000, // 24 hours + sameSite: process.env.NODE_ENV === "production" ? "none" : "strict", + maxAge: 24 * 60 * 60 * 1000, }); - res.send({ + // Return token in response + res.json({ message: "Auth0 authentication successful", user: { id: user.id, username: user.username, auth0Id: user.auth0Id, email: user.email, + role: user.role, + imageUrl: user.imageUrl }, + token: token }); } catch (error) { console.error("Auth0 authentication error:", error); - res.sendStatus(500); + res.status(500).json({ error: "Authentication failed" }); } }); @@ -135,21 +161,30 @@ router.post("/signup", async (req, res) => { username: user.username, auth0Id: user.auth0Id, email: user.email, + role: user.role, }, JWT_SECRET, { expiresIn: "24h" } ); res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", httpOnly: true, secure: true, - sameSite: "strict", + sameSite: "none", maxAge: 24 * 60 * 60 * 1000, // 24 hours }); res.send({ message: "User created successfully", - user: { id: user.id, username: user.username }, + user: { + id: user.id, + username: user.username, + email: user.email, + imageUrl: user.imageUrl + }, + token: token }); } catch (error) { console.error("Signup error:", error); @@ -162,49 +197,41 @@ router.post("/login", async (req, res) => { try { const { username, password } = req.body; - if (!username || !password) { - res.status(400).send({ error: "Username and password are required" }); - return; - } - - // Find user const user = await User.findOne({ where: { username } }); - user.checkPassword(password); if (!user) { - return res.status(401).send({ error: "Invalid credentials" }); + return res.status(401).json({ error: "Invalid credentials" }); } - // Check password - if (!user.checkPassword(password)) { - return res.status(401).send({ error: "Invalid credentials" }); + // Check if user is disabled + if (user.disabled) { + return res.status(403).json({ error: "Account is disabled. Please contact support." }); + } + + const validPassword = await bcrypt.compare(password, user.passwordHash); + if (!validPassword) { + return res.status(401).json({ error: "Invalid credentials" }); } - // Generate JWT token const token = jwt.sign( - { - id: user.id, - username: user.username, - auth0Id: user.auth0Id, - email: user.email, - }, - JWT_SECRET, + { id: user.id, username: user.username }, + process.env.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: "Login successful", - user: { id: user.id, username: user.username }, + res.json({ + token, + user: { + id: user.id, + username: user.username, + email: user.email, + bio: user.bio, + imageUrl: user.imageUrl, + role: user.role + } }); } catch (error) { console.error("Login error:", error); - res.sendStatus(500); + res.status(500).json({ error: "Login failed" }); } }); @@ -215,19 +242,39 @@ router.post("/logout", (req, res) => { }); // Get current user route (protected) -router.get("/me", (req, res) => { - const token = req.cookies.token; +router.get("/me", async (req, res) => { + let token = null; + + // Try to get token from Authorization header first + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + token = authHeader.substring(7); + } + + // If no token in header, try to get from cookies + if (!token && req.cookies && req.cookies.token) { + token = req.cookies.token; + } if (!token) { - return res.send({}); + return res.status(401).json({ error: "No token provided" }); } - jwt.verify(token, JWT_SECRET, (err, user) => { - if (err) { - return res.status(403).send({ error: "Invalid or expired token" }); + try { + const decoded = jwt.verify(token, JWT_SECRET); + const user = await User.findByPk(decoded.id, { + attributes: { exclude: ["passwordHash"] }, + }); + + if (!user) { + return res.status(404).json({ error: "User not found" }); } - res.send({ user: user }); - }); + + res.json(user); + } catch (err) { + console.error("Token verification failed:", err); + return res.status(403).json({ error: "Invalid token" }); + } }); -module.exports = { router, authenticateJWT }; +module.exports = { router, authenticateJWT, requireAuth, requireAdmin }; \ No newline at end of file diff --git a/database/ballot.js b/database/ballot.js new file mode 100644 index 0000000..0bb0888 --- /dev/null +++ b/database/ballot.js @@ -0,0 +1,25 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Ballot = db.define( + "ballot", + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + anonToken: { + type: DataTypes.UUID, + unique: true, + allowNull: true, + }, + submittedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + }, + { underscored: true } +); + +module.exports = Ballot; diff --git a/database/ballotRanking.js b/database/ballotRanking.js new file mode 100644 index 0000000..1753bf7 --- /dev/null +++ b/database/ballotRanking.js @@ -0,0 +1,20 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const BallotRanking = db.define( + "ballotRanking", + { + rank: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + underscored: true, + indexes: [ + { unique: true, fields: ["ballot_id", "rank"] }, + ], + } +); + +module.exports = BallotRanking; \ No newline at end of file diff --git a/database/index.js b/database/index.js index e498df6..a8bdacc 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,135 @@ const db = require("./db"); const User = require("./user"); +const Poll = require("./poll"); +const PollOption = require("./pollOption"); +const Ballot = require("./ballot"); +const BallotRanking = require("./ballotRanking"); +const PollAllowedUser = require("./pollAllowedUser"); +const PollResultValue = require("./pollResultValue"); +const PollResult = require("./pollResult"); +const UserFollow = require("./userFollow"); +const PollViewPermission = require("./pollViewPermission"); +const PollVotePermission = require("./pollVotePermission"); + +// User and Poll relationships +User.hasMany(Poll, { foreignKey: "creator_id" }); +Poll.belongsTo(User, { as: "creator", foreignKey: "creator_id" }); + +// Poll and PollOption relationships +Poll.hasMany(PollOption, { foreignKey: "poll_id", onDelete: "CASCADE",as: "PollOptions"}); +PollOption.belongsTo(Poll, { foreignKey: "poll_id", as: "Poll"}); + +// Poll and Ballot relationships +Poll.hasMany(Ballot, { foreignKey: "poll_id", onDelete: "CASCADE" }); +Ballot.belongsTo(Poll, { foreignKey: "poll_id" }); + +// User and Ballot relationships +User.hasMany(Ballot, { foreignKey: "user_id" }); +Ballot.belongsTo(User, { foreignKey: "user_id" }); + +// Ballot and BallotRanking relationships +Ballot.hasMany(BallotRanking, { foreignKey: "ballot_id", onDelete: "CASCADE" }); +BallotRanking.belongsTo(Ballot, { foreignKey: "ballot_id" }); + +// PollOption and BallotRanking relationships +PollOption.hasMany(BallotRanking, { foreignKey: "option_id" }); +BallotRanking.belongsTo(PollOption, { foreignKey: "option_id" }); + +// Poll and PollResult relationships +Poll.hasOne(PollResult, { foreignKey: "poll_id", onDelete: "CASCADE" }); +PollResult.belongsTo(Poll, { foreignKey: "poll_id" }); + +// PollOption and PollResultValue relationships +PollOption.hasMany(PollResultValue, { foreignKey: "option_id" }); +PollResultValue.belongsTo(PollOption, { foreignKey: "option_id" }); + +// PollResult and PollResultValue relationships +PollResult.hasMany(PollResultValue, { foreignKey: "poll_result_id" }); +PollResultValue.belongsTo(PollResult, { foreignKey: "poll_result_id" }); + +// Poll AllowedUsers relationships (existing allowed user system) +Poll.belongsToMany(User, { + through: PollAllowedUser, + as: "allowedUsers", + foreignKey: "poll_id", +}); +User.belongsToMany(Poll, { + through: PollAllowedUser, + as: "allowedPolls", + foreignKey: "user_id", +}); + +// User Follow Relationships (mutual following system) +User.belongsToMany(User, { + through: UserFollow, + as: "following", + foreignKey: "follower_id", + otherKey: "following_id", +}); + +User.belongsToMany(User, { + through: UserFollow, + as: "followers", + foreignKey: "following_id", + otherKey: "follower_id", +}); + +// Additional follow relationships for easier access +User.hasMany(UserFollow, { as: "followingRelations", foreignKey: "follower_id" }); +User.hasMany(UserFollow, { as: "followerRelations", foreignKey: "following_id" }); + +UserFollow.belongsTo(User, { as: "follower", foreignKey: "follower_id" }); +UserFollow.belongsTo(User, { as: "following", foreignKey: "following_id" }); + +// New Poll View Permission Relationships +Poll.belongsToMany(User, { + through: PollViewPermission, + as: "viewAllowedUsers", + foreignKey: "poll_id", +}); +User.belongsToMany(Poll, { + through: PollViewPermission, + as: "viewablePolls", + foreignKey: "user_id", +}); + +// New Poll Vote Permission Relationships +Poll.belongsToMany(User, { + through: PollVotePermission, + as: "voteAllowedUsers", + foreignKey: "poll_id", +}); +User.belongsToMany(Poll, { + through: PollVotePermission, + as: "votablePolls", + foreignKey: "user_id", +}); + +// Direct relationships for permission models +PollViewPermission.belongsTo(Poll, { foreignKey: "poll_id" }); +PollViewPermission.belongsTo(User, { foreignKey: "user_id" }); + +PollVotePermission.belongsTo(Poll, { foreignKey: "poll_id" }); +PollVotePermission.belongsTo(User, { foreignKey: "user_id" }); + +// Additional direct relationships from Poll and User to permission models +Poll.hasMany(PollViewPermission, { foreignKey: "poll_id", onDelete: "CASCADE" }); +Poll.hasMany(PollVotePermission, { foreignKey: "poll_id", onDelete: "CASCADE" }); + +User.hasMany(PollViewPermission, { foreignKey: "user_id", onDelete: "CASCADE" }); +User.hasMany(PollVotePermission, { foreignKey: "user_id", onDelete: "CASCADE" }); module.exports = { db, User, -}; + Poll, + PollOption, + Ballot, + BallotRanking, + PollAllowedUser, + PollResult, + PollResultValue, + UserFollow, + PollViewPermission, + PollVotePermission, +}; \ No newline at end of file diff --git a/database/poll.js b/database/poll.js new file mode 100644 index 0000000..6e4fbe7 --- /dev/null +++ b/database/poll.js @@ -0,0 +1,53 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Poll = db.define( + "poll", + { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + imageUrl: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: + "https://online.eou.edu/wp-content/uploads/2020/10/eou_article_psychology-competitiveness_header.jpg", + validate: { + isUrl: true, + }, + }, + description: DataTypes.TEXT, + allowAnonymous: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + status: { + type: DataTypes.ENUM("draft", "published", "closed"), + defaultValue: "draft", + }, + endAt: DataTypes.DATE, + allowListOnly: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + publishedAt: DataTypes.DATE, + viewRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", + }, + voteRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", + }, + }, + { + underscored: true, + }, +); + +module.exports = Poll; \ No newline at end of file diff --git a/database/pollAllowedUser.js b/database/pollAllowedUser.js new file mode 100644 index 0000000..c32165c --- /dev/null +++ b/database/pollAllowedUser.js @@ -0,0 +1,9 @@ +const db = require("./db"); + +const PollAllowedUser = db.define( + "pollAllowedUser", + {}, + { timestamps: false, underscored: true } +); + +module.exports = PollAllowedUser; diff --git a/database/pollOption.js b/database/pollOption.js new file mode 100644 index 0000000..f0bfed1 --- /dev/null +++ b/database/pollOption.js @@ -0,0 +1,20 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollOption = db.define( + "pollOption", + { + text: { + type: DataTypes.STRING, + allowNull: false, + }, + position: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + }, + }, + { underscored: true } +); + +module.exports = PollOption; \ No newline at end of file diff --git a/database/pollResult.js b/database/pollResult.js new file mode 100644 index 0000000..451b29f --- /dev/null +++ b/database/pollResult.js @@ -0,0 +1,18 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollResult = db.define( + "pollResult", + { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + totalBallots: { type: DataTypes.INTEGER, allowNull: false }, + completedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }, + { underscored: true } +); + +module.exports = PollResult; diff --git a/database/pollResultValue.js b/database/pollResultValue.js new file mode 100644 index 0000000..f25d0ad --- /dev/null +++ b/database/pollResultValue.js @@ -0,0 +1,15 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollResultValue = db.define( + "pollResultValue", + { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + roundNumber: { type: DataTypes.INTEGER, allowNull: false }, + optionText: { type: DataTypes.STRING, allowNull: false }, + votes: { type: DataTypes.INTEGER, allowNull: false }, + }, + { underscored: true } +); + +module.exports = PollResultValue; diff --git a/database/pollViewPermission.js b/database/pollViewPermission.js new file mode 100644 index 0000000..0ea360d --- /dev/null +++ b/database/pollViewPermission.js @@ -0,0 +1,35 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollViewPermission = db.define("pollViewPermission", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + poll_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'polls', + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, +}, { + indexes: [ + { + unique: true, + fields: ['poll_id', 'user_id'] + } + ] +}); + +module.exports = PollViewPermission; \ No newline at end of file diff --git a/database/pollVotePermission.js b/database/pollVotePermission.js new file mode 100644 index 0000000..561b95a --- /dev/null +++ b/database/pollVotePermission.js @@ -0,0 +1,35 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const PollVotePermission = db.define("pollVotePermission", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + poll_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'polls', + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, +}, { + indexes: [ + { + unique: true, + fields: ['poll_id', 'user_id'] + } + ] +}); + +module.exports = PollVotePermission; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index e58b595..8978f97 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,23 +1,126 @@ const db = require("./db"); -const { User } = require("./index"); +const { User, Poll, PollOption } = require("./index"); const seed = async () => { try { db.logging = false; - await db.sync({ force: true }); // Drop and recreate tables + await db.sync({ force: true }); const users = await User.bulkCreate([ { username: "admin", passwordHash: User.hashPassword("admin123") }, { username: "user1", passwordHash: User.hashPassword("user111") }, { username: "user2", passwordHash: User.hashPassword("user222") }, + { username: "pollmaster", passwordHash: User.hashPassword("polls123") }, ]); console.log(`👤 Created ${users.length} users`); - // Create more seed data here once you've created your models - // Seed files are a great way to test your database schema! + const now = new Date(); + const polls = await Poll.bulkCreate([ + { + title: "What's your favorite programming language?", + description: "Let us know what your fav language is", + creator_id: users[0].id, + status: "published", + endAt: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000), + allowAnonymous: true, + publishedAt: now, + }, + { + title: "Best time for team meetings?", + description: "Lets find a time that works for everyone", + creator_id: users[1].id, + status: "published", + endAt: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000), + allowAnonymous: false, + publishedAt: now, + }, + { + title: "Weekend activity preferences", + description: "What should we do this weekend?", + creator_id: users[2].id, + status: "published", + endAt: new Date(now.getTime() + 2 * 60 * 60 * 1000), + allowAnonymous: true, + publishedAt: now, + }, + { + title: "Office lunch options", + description: "Vote for next weeks catered lunch", + creator_id: users[3].id, + status: "published", + endAt: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000), + allowAnonymous: false, + publishedAt: now, + }, + { + title: "Project deadline preference", + description: "When should we target the release?", + creator_id: users[0].id, + status: "published", + endAt: new Date(now.getTime() + 24 * 60 * 60 * 1000), + allowAnonymous: false, + publishedAt: now, + }, + { + title: "Ended Poll Example", + description: "This poll has already ended", + creator_id: users[1].id, + status: "closed", + endAt: new Date(now.getTime() - 24 * 60 * 60 * 1000), + allowAnonymous: true, + publishedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000), + }, + ]); + + console.log(`📊 Created ${polls.length} polls`); + + const pollOptions = await PollOption.bulkCreate([ + // Options for "What's your favorite programming language?" + { text: "JavaScript", position: 1, poll_id: polls[0].id }, + { text: "Python", position: 2, poll_id: polls[0].id }, + { text: "Java", position: 3, poll_id: polls[0].id }, + { text: "C++", position: 4, poll_id: polls[0].id }, + + // Options for "Best time for team meetings?" + { text: "9:00 AM", position: 1, poll_id: polls[1].id }, + { text: "1:00 PM", position: 2, poll_id: polls[1].id }, + { text: "3:00 PM", position: 3, poll_id: polls[1].id }, + + // Options for "Weekend activity preferences" + { text: "Hiking", position: 1, poll_id: polls[2].id }, + { text: "Movie night", position: 2, poll_id: polls[2].id }, + { text: "Board games", position: 3, poll_id: polls[2].id }, + { text: "Cooking together", position: 4, poll_id: polls[2].id }, + + // Options for "Office lunch options" + { text: "Pizza", position: 1, poll_id: polls[3].id }, + { text: "Chinese food", position: 2, poll_id: polls[3].id }, + { text: "Sandwiches", position: 3, poll_id: polls[3].id }, + { text: "Mexican food", position: 4, poll_id: polls[3].id }, + + // Options for "Project deadline preference" + { text: "End of this month", position: 1, poll_id: polls[4].id }, + { text: "Next month", position: 2, poll_id: polls[4].id }, + { text: "In 6 weeks", position: 3, poll_id: polls[4].id }, + + // Options for "Ended Poll Example" + { text: "Option A", position: 1, poll_id: polls[5].id }, + { text: "Option B", position: 2, poll_id: polls[5].id }, + { text: "Option C", position: 3, poll_id: polls[5].id }, + ]); + + console.log(`📝 Created ${pollOptions.length} poll options`); + + console.log("🌱 Seeded the database successfully!"); + console.log("\n📊 Sample polls created:"); + polls.forEach((poll, index) => { + const timeLeft = poll.endAt > now ? + `${Math.ceil((poll.endAt - now) / (1000 * 60 * 60 * 24))} days left` : + 'Ended'; + console.log(` ${index + 1}. "${poll.title}" - ${timeLeft}`); + }); - console.log("🌱 Seeded the database"); } catch (error) { console.error("Error seeding database:", error); if (error.message.includes("does not exist")) { @@ -27,4 +130,4 @@ const seed = async () => { db.close(); }; -seed(); +seed(); \ No newline at end of file diff --git a/database/user.js b/database/user.js index 755c757..0fc1519 100644 --- a/database/user.js +++ b/database/user.js @@ -1,46 +1,84 @@ -const { DataTypes } = require("sequelize"); +const { DataTypes, Model } = require("sequelize"); const db = require("./db"); const bcrypt = require("bcrypt"); -const User = db.define("user", { - username: { - type: DataTypes.STRING, +class User extends Model { + static hashPassword(password) { + return bcrypt.hashSync(password, 10); + } + + static comparePassword(password, hash) { + return bcrypt.compareSync(password, hash); + } + + checkPassword(password) { + return bcrypt.compareSync(password, this.passwordHash); + } +} + +User.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + auth0Id: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + len: [3, 30], + }, + }, + email: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isEmail: true, + }, + }, + passwordHash: { + type: DataTypes.STRING, + allowNull: true, + }, + imageUrl: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: "https://t3.ftcdn.net/jpg/05/16/27/58/360_F_516275801_f3Fsp17x6HQK0xQgDQEELoTuERO4SsWV.jpg", + }, + bio: { + type: DataTypes.TEXT, + allowNull: true, + }, + disabled: { + type: DataTypes.BOOLEAN, allowNull: false, - unique: true, - validate: { - len: [3, 20], + defaultValue: false, }, - }, - email: { - type: DataTypes.STRING, - allowNull: true, - unique: true, - validate: { - isEmail: true, + role: { + type: DataTypes.ENUM("user", "admin"), + defaultValue: "user", + }, + viewRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", + }, + voteRestriction: { + type: DataTypes.ENUM("public", "followers", "friends", "custom"), + defaultValue: "public", }, }, - auth0Id: { - type: DataTypes.STRING, - allowNull: true, - unique: true, - }, - passwordHash: { - type: DataTypes.STRING, - allowNull: true, - }, -}); - -// Instance method to check password -User.prototype.checkPassword = function (password) { - if (!this.passwordHash) { - return false; // Auth0 users don't have passwords + { + sequelize: db, + modelName: "User", + tableName: "users", } - return bcrypt.compareSync(password, this.passwordHash); -}; - -// Class method to hash password -User.hashPassword = function (password) { - return bcrypt.hashSync(password, 10); -}; +); -module.exports = User; +module.exports = User; \ No newline at end of file diff --git a/database/userFollow.js b/database/userFollow.js new file mode 100644 index 0000000..d85556f --- /dev/null +++ b/database/userFollow.js @@ -0,0 +1,46 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const UserFollow = db.define("userfollow", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + follower_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + following_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, +}, { + indexes: [ + { + unique: true, + fields: ['follower_id', 'following_id'] + } + ], + validate: { + notSelfFollow() { + if (this.follower_id === this.following_id) { + throw new Error('Users cannot follow themselves'); + } + } + } +}); + +module.exports = UserFollow; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index af0cf82..8270d31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,27 @@ { - "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": { + "auth0": "^4.27.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-jwt": "^8.5.1", "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.2.0", "morgan": "^1.10.0", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "start-dev": "^1.0.0" }, "devDependencies": { "nodemon": "^3.1.10" @@ -26,6 +30,282 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@beamwind/core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@beamwind/core/-/core-2.3.0.tgz", + "integrity": "sha512-unXFgXRhPYVRPYS0ew675MPH10v9C5rQmY37r6p5FQ6SbamFaW3CNO/mYoLuRV/gBvK96l9OrTDr3EExnQfHMA==", + "license": "MIT", + "dependencies": { + "tiny-css-prefixer": "^1.1.4" + } + }, + "node_modules/@beamwind/preflight": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@beamwind/preflight/-/preflight-2.0.2.tgz", + "integrity": "sha512-pGuIpblgufW5qIWp/75LcKt8pUes9WeR5cOPJdnDS8mxn9C4oDJ0VPACOYsuXa8B7n0qIFjnY8Y3LbM6LkVyPw==", + "license": "MIT" + }, + "node_modules/@beamwind/preset-tailwind": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@beamwind/preset-tailwind/-/preset-tailwind-2.0.2.tgz", + "integrity": "sha512-ceEZqzDHnrAfSSeXXPDoVT9N0WcY16I4Sr0yEy3c8yEM/oSkDXA/LoS0xnYEuht+r1ie5o+wy2NR2YnUcqMzpw==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", + "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^2.30.0" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "license": "MIT" + }, + "node_modules/@start-dev/bundle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/bundle/-/bundle-1.0.0.tgz", + "integrity": "sha512-5BUW5XB7UYv3TAczpc83NrJcVbc/Iq24BwYLt42oZ5lWIhi+d2eU93h6jWiG5Rodh//5RQaYcnan+EExmiJDmg==", + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "rollup": "^2.34.2", + "rollup-plugin-node-polyfills": "^0.2.1", + "vm2": "^3.9.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/core": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@start-dev/core/-/core-1.0.1.tgz", + "integrity": "sha512-sCVRo8wB0pLU7Y61JkeSTPBfHdQO3Ts5etWhxC808ueMOXzkBEblABNSS8pSdvP+hCOhTtNGyC6QljAeaRbNxg==", + "license": "MIT", + "dependencies": { + "@start-dev/bundle": "^1.0.0", + "@start-dev/dark-mode-selector": "^1.0.0", + "@start-dev/find-package-locations": "^1.0.0", + "@start-dev/get-package-exports": "^1.0.0", + "@start-dev/rewrite-imports": "^1.0.0", + "@start-dev/websocket-rpc": "^1.0.0", + "@types/find-cache-dir": "^3.2.0", + "beamwind": "^2.0.2", + "chalk": "^4.1.0", + "find-cache-dir": "^3.3.1", + "mime": "^2.4.6", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/dark-mode-selector": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/dark-mode-selector/-/dark-mode-selector-1.0.0.tgz", + "integrity": "sha512-/cAzS9zAW9Kqd7g2CiIAqDcDY05uuf/B9z1rousCf/8QqkTYaMjTHU8BQnox10RkdA8+yypGSzRtiKWjIomU/Q==", + "license": "MIT", + "dependencies": { + "@start-dev/state": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "beamwind": "*", + "react": "*" + } + }, + "node_modules/@start-dev/find-package-locations": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/find-package-locations/-/find-package-locations-1.0.0.tgz", + "integrity": "sha512-FWmYE6MlacNdXq7paibQchBCX9nb5nHMyiJpYMqmWV4u27hgVHT2lKnaevRWSFOyG/8EN6BA45h4iH2TalkXWw==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/get-package-exports": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/get-package-exports/-/get-package-exports-1.0.0.tgz", + "integrity": "sha512-WkQn9kapJJO1LCHuRvgOuEfLrDg1kpAd3t5dsl2uNhTmw6u6u+ct3lh07/QiW3138S7a0oLXKWxtv9HANN5RXQ==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/rewrite-imports": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/rewrite-imports/-/rewrite-imports-1.0.0.tgz", + "integrity": "sha512-+9cIugsJQVx9HiGXPaWWhvXmnhVNNWUou2XAsjuV8hTXlGMjzIBmHDIOCd/qwB5Zrc0INK75tC+yVjwdxolkmg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@start-dev/state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/state/-/state-1.0.0.tgz", + "integrity": "sha512-zjnJzc/k+7/AzCiUe3EeuQn7PFTXVGSDTAZgiN71/dQ03HOyZyUfpM2Mi9xM4UosiK8tn7ToEnthSfus0THryg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@start-dev/websocket-rpc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@start-dev/websocket-rpc/-/websocket-rpc-1.0.0.tgz", + "integrity": "sha512-NtGSzMAE4/cFmLIEkSc0Dsd4nGE8Cd+7Xmjp7gAH1uoUE2xkaS9GKgnkPpNkWSteSSX7yAb63ExhOmQ7PGVmxg==", + "license": "MIT", + "dependencies": { + "sucrase": "^3.16.0", + "ws": "^7.4.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -35,6 +315,64 @@ "@types/ms": "*" } }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/find-cache-dir": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", + "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -50,6 +388,48 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/validator": { "version": "13.15.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", @@ -69,6 +449,63 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -83,11 +520,43 @@ "node": ">= 8" } }, + "node_modules/auth0": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.27.0.tgz", + "integrity": "sha512-4FGgjzKCH/f7rQLQVR5dM30asjOObeW3PyHa8bQrS4rKkuv22JoNxox26fb1FZ3hI4zEgbVbPm9x7pHrljZzrw==", + "license": "MIT", + "dependencies": { + "jose": "^4.13.2", + "undici-types": "^6.15.0", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/auth0/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/auth0/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/basic-auth": { @@ -122,6 +591,17 @@ "node": ">= 18" } }, + "node_modules/beamwind": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/beamwind/-/beamwind-2.0.2.tgz", + "integrity": "sha512-XyXjZDz4krw1czYr6UA9igxVEg8T6WmL631fTQ/aXKUrcnRQe8QZ8Lhr3OJCilIKzAAYxEhtPe9R84V3+MXTEw==", + "license": "MIT", + "dependencies": { + "@beamwind/core": "^2.0.2", + "@beamwind/preflight": "^2.0.0", + "@beamwind/preset-tailwind": "^2.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -159,7 +639,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -185,6 +664,18 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -223,6 +714,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -248,11 +776,43 @@ "fsevents": "~2.3.2" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -326,6 +886,20 @@ "node": ">= 0.10" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -343,6 +917,15 @@ } } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -384,6 +967,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -399,6 +988,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -444,6 +1039,12 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -495,6 +1096,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-jwt": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -525,6 +1146,52 @@ "node": ">= 0.8" } }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -543,11 +1210,16 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -604,6 +1276,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -707,6 +1400,17 @@ ], "license": "MIT" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -735,6 +1439,21 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -745,6 +1464,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -758,6 +1486,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -774,6 +1508,45 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -807,6 +1580,23 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwks-rsa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", + "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.20", + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -817,12 +1607,41 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -865,6 +1684,67 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -895,6 +1775,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -920,7 +1812,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -929,6 +1820,15 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -999,6 +1899,17 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1118,6 +2029,48 @@ "wrappy": "1" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1127,6 +2080,55 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -1229,7 +2231,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -1238,6 +2239,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -1312,47 +2334,150 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" } }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "node_modules/rollup-plugin-inject/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "license": "MIT" + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" + "rollup-plugin-inject": "^3.0.0" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" + "estree-walker": "^0.6.1" } }, - "node_modules/retry-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", - "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", "license": "MIT" }, "node_modules/router": { @@ -1523,6 +2648,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1595,6 +2741,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1608,6 +2766,13 @@ "node": ">=10" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "license": "MIT" + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1617,6 +2782,21 @@ "node": ">= 10.x" } }, + "node_modules/start-dev": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/start-dev/-/start-dev-1.0.0.tgz", + "integrity": "sha512-AgSsh5mHOrweIHzIfqu6os5V0BoqA5R28v/J8zZBUZlD/GAB6uyWKkTnStvSOyfoqt8SDXQo6J6QyEl3GMjE9g==", + "license": "MIT", + "dependencies": { + "@start-dev/core": "^1.0.0" + }, + "bin": { + "start-dev": "lib/index.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1626,6 +2806,168 @@ "node": ">= 0.8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1639,6 +2981,45 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-css-prefixer": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/tiny-css-prefixer/-/tiny-css-prefixer-1.1.4.tgz", + "integrity": "sha512-QLnDGLUmMkNlSuWogBaSu92uQnvWhcQwUc+MG8L4YgBkjSmBkAz29pTq+8Rh3YWR1TwPrjuZDKWzO7v4PXkipQ==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1677,6 +3058,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1740,6 +3127,38 @@ "node": ">= 0.8" } }, + "node_modules/vm2": { + "version": "3.9.19", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz", + "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==", + "deprecated": "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.", + "license": "MIT", + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/win-node-env": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/win-node-env/-/win-node-env-0.6.1.tgz", @@ -1767,12 +3186,121 @@ "@types/node": "*" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -1781,6 +3309,12 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 7e0a0af..49b9767 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,19 @@ "license": "ISC", "description": "", "dependencies": { + "auth0": "^4.27.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-jwt": "^8.5.1", "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.2.0", "morgan": "^1.10.0", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "start-dev": "^1.0.0" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/services/pollPermissions.js b/services/pollPermissions.js new file mode 100644 index 0000000..dcf4fcb --- /dev/null +++ b/services/pollPermissions.js @@ -0,0 +1,108 @@ +const { User, UserFollow, PollViewPermission, PollVotePermission } = require("../database"); +const { Op } = require("sequelize"); + +const checkPollViewPermission = async (poll, userId) => { + if (poll.viewRestriction === "public") { + return true; + } + + if (!userId) { + return false; + } + + if (poll.creator_id === userId) { + return true; + } + + switch (poll.viewRestriction) { + case "followers": + const isFollowing = await UserFollow.findOne({ + where: { + follower_id: userId, + following_id: poll.creator_id + } + }); + return !!isFollowing; + + case "friends": + const [userFollowsCreator, creatorFollowsUser] = await Promise.all([ + UserFollow.findOne({ + where: { follower_id: userId, following_id: poll.creator_id } + }), + UserFollow.findOne({ + where: { follower_id: poll.creator_id, following_id: userId } + }) + ]); + return !!(userFollowsCreator && creatorFollowsUser); + + case "custom": + const hasCustomPermission = await PollViewPermission.findOne({ + where: { + poll_id: poll.id, + user_id: userId + } + }); + return !!hasCustomPermission; + + default: + return false; + } +}; + +const checkPollVotePermission = async (poll, userId) => { + const canView = await checkPollViewPermission(poll, userId); + if (!canView) { + return false; + } + + if (poll.voteRestriction === "public") { + return true; + } + + if (!userId) { + return false; + } + + if (poll.creator_id === userId) { + return true; + } + + switch (poll.voteRestriction) { + case "followers": + const isFollowing = await UserFollow.findOne({ + where: { + follower_id: userId, + following_id: poll.creator_id + } + }); + return !!isFollowing; + + case "friends": + const [userFollowsCreator, creatorFollowsUser] = await Promise.all([ + UserFollow.findOne({ + where: { follower_id: userId, following_id: poll.creator_id } + }), + UserFollow.findOne({ + where: { follower_id: poll.creator_id, following_id: userId } + }) + ]); + return !!(userFollowsCreator && creatorFollowsUser); + + case "custom": + const hasCustomPermission = await PollVotePermission.findOne({ + where: { + poll_id: poll.id, + user_id: userId + } + }); + return !!hasCustomPermission; + + default: + return false; + } +}; + +module.exports = { + checkPollViewPermission, + checkPollVotePermission, +}; \ No newline at end of file