diff --git a/.env.example b/.env.example deleted file mode 100644 index 848ad61..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DATABASE_URL=postgres://hostname:5432/database-name -JWT_SECRET=super-secret-jwt-key -FRONTEND_URL=http://localhost:3000 diff --git a/README.md b/README.md index b8ac36b..3a553bd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Capstone I Backend +# Capstone II 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` +You will also need to create the database: by default it is called `capstone-2`, but you are welcome to rename it in `database/db.js` After that, you can get started with these commands diff --git a/api/forum.js b/api/forum.js new file mode 100644 index 0000000..5d33f6b --- /dev/null +++ b/api/forum.js @@ -0,0 +1,68 @@ +const express = require('express'); +const router = express.Router(); +const {Forum, Post, Reply, User} = require('../database') + +// Get posts for a specific forum +router.get('/:forumId/posts', async (req, res) => { + const { forumId } = req.params; + console.log(`Fetching posts for forum ID: ${forumId}`); + try { + const posts = await Post.findAll({ + where: { forumId }, + include: [{ model: User, attributes: ['username'] }], + order: [['createdAt', 'DESC']], + }); + res.json(posts); + } catch (err) { + console.error('Error fetching posts: ', err); + res.status(500).json({ error: 'Failed to fetch posts for this forum.' }); + } + }); + + router.get('/:forumId/posts/:postId', async (req, res) => { + const { postId, forumId } = req.params; + console.log(`Fetching post for post ID: ${postId}`); + try { + const post = await Post.findOne({ + where: { forumId, id: postId }, + include: [{ model: User, attributes: ['username']}], + }); + res.json(post); + } catch (err) { + console.error('Error fetching post by id: ', err); + res.status(500).json({ error: 'Failed to fetch posts for this forum.' }); + } + }); + +//Create a new post in a forum +router.post('/:forumId/post/', async(req, res) => { + const { forumId } = req.params; + try { + const { title, content, userId, likes = 0 } = req.body; + const newPost = await Post.create({ + title, + content, + likes, + userId, + forumId: forumId, + }); + + res.status(201).json(newPost); + } catch (error) { + console.error(error); + res.status(500).send("Error from the post new post route"); + } +}); + +// Get all forums +router.get('/', async (req, res) => { + try { + const forums = await Forum.findAll(); + res.json(forums); + } catch (err) { + console.error('Error fetching forums:', err); + res.status(500).json({ error: 'Failed to fetch forums.' }); + } + }); + +module.exports = router; \ No newline at end of file diff --git a/api/index.js b/api/index.js index f08162e..7e8785d 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,15 @@ const express = require("express"); const router = express.Router(); const testDbRouter = require("./test-db"); +const forumRouter = require("./forum"); +const postRouter = require("./post"); +const postLikesRouter = require("./postlikes"); +const replyLikesRouter = require("./replylikes"); router.use("/test-db", testDbRouter); +router.use("/forum", forumRouter); +router.use("/post", postRouter); +router.use("/postlikes", postLikesRouter); +router.use("/replylikes", replyLikesRouter); module.exports = router; diff --git a/api/post.js b/api/post.js new file mode 100644 index 0000000..90b5ff5 --- /dev/null +++ b/api/post.js @@ -0,0 +1,64 @@ +const express = require('express'); +const router = express.Router(); +const {Forum, Post, Reply, User} = require('../database'); + +//Get all posts +router.get("/", async (req, res) => { + try { + const posts = await Post.findAll(); + res.status(200).json(posts); + } catch (error) { + console.error(error); + res.status(500).send("Error from the get all posts route"); + } +}); + +//Get a specific post +router.get("/:id", async (req, res) => { + try { + const postID = Number(req.params.id); + console.log(postID); + const post = await Post.findByPk(postID); + if (!post) { + return res.status(404).send("Post not found"); + } + res.status(200).json(post); + } catch (error) { + console.error(error); + res.status(500).send("Error from the get single post route"); + } +}); + +router.delete("/:postId", async (req, res) => { + try{ + const post = await Post.findByPk(req.params.postId); + if (!post) { + return res.status(404).send("Post not found"); + } + + await post.destroy(); + res.status(200).send("Post deleted successfully"); + } catch (error) { + console.error(error); + res.status(500).send("Error from the delete existing post route"); + } +}); + +// Get all replies from a post +router.get('/:postId/replies', async (req, res) => { + const { postId } = req.params; + + try { + const replies = await Reply.findAll({ + where: { postId }, + include: [{ model: User, attributes: ['username'] }], + order: [['createdAt', 'DESC']], + }); + res.json(replies); + } catch (err) { + console.error('Error fetching replies:', err); + res.status(500).json({ error: 'Failed to fetch replies for this post.' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/postlikes.js b/api/postlikes.js new file mode 100644 index 0000000..4059146 --- /dev/null +++ b/api/postlikes.js @@ -0,0 +1,70 @@ +const express = require("express"); +const router = express.Router(); +const { Postlikes, Post, User } = require("../database"); + +// Check if the user has liked a specific post +router.get("/:postId/likes/:userId", async (req, res) => { + try { + const { postId, userId } = req.params; + + if (!userId) { + return res.status(401).json({ error: "User not authenticated." }); + } + + if (!postId || isNaN(postId)) { + return res.status(400).json({ error: "Invalid post ID." }); + } + + const like = await Postlikes.findOne({ where: { postId, userId } }); + + return res.status(200).json({ liked: !!like }); + } catch (error) { + console.error("Error checking post like:", error); + return res.status(500).json({ error: "Internal server error." }); + } +}); + +// Like a post +router.post("/:postId/like/:userId", async (req, res) => { + const { postId, userId } = req.params; + + try { + const existing = await Postlikes.findOne({ where: { userId, postId } }); + if (existing) { + return res.status(400).json({ error: "You already liked this post" }); + } + + await Postlikes.create({ userId, postId }); + + // Increment post.likes count + await Post.increment("likes", { where: { id: postId } }); + + res.status(201).json({ message: "Post liked" }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to like post" }); + } +}); + +// Unlike a post +router.delete("/:postId/unlike/:userId", async (req, res) => { + const { postId, userId } = req.params; + + try { + const deleted = await Postlikes.destroy({ where: { userId, postId } }); + + if (!deleted) { + return res.status(404).json({ error: "Like not found" }); + } + + // Decrement post.likes count + await Post.decrement("likes", { where: { id: postId } }); + + res.status(200).json({ message: "Post unliked" }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to unlike post" }); + } +}); + +module.exports = router; diff --git a/api/replylikes.js b/api/replylikes.js new file mode 100644 index 0000000..964b7af --- /dev/null +++ b/api/replylikes.js @@ -0,0 +1,70 @@ +const express = require("express"); +const router = express.Router(); +const { Replylikes, Reply } = require("../database"); + +// Check if the user has liked a specific reply +router.get("/:replyId/likes/:userId", async (req, res) => { + try { + const { replyId, userId } = req.params; + + if (!userId) { + return res.status(401).json({ error: "User not authenticated." }); + } + + if (!replyId || isNaN(replyId)) { + return res.status(400).json({ error: "Invalid reply ID." }); + } + + const like = await Replylikes.findOne({ where: { replyId, userId } }); + + return res.status(200).json({ liked: !!like }); + } catch (error) { + console.error("Error checking reply like:", error); + return res.status(500).json({ error: "Internal server error." }); + } +}); + +// Like a reply +router.post("/:replyId/like/:userId", async (req, res) => { + const { replyId, userId } = req.params; + + try { + const existing = await Replylikes.findOne({ where: { userId, replyId } }); + if (existing) { + return res.status(400).json({ error: "You already liked this reply" }); + } + + await Replylikes.create({ userId, replyId }); + + // Optional: increment reply.likes + await Reply.increment("likes", { where: { id: replyId } }); + + res.status(201).json({ message: "Reply liked" }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to like reply" }); + } +}); + +// Unlike a reply +router.delete("/:replyId/unlike/:userId", async (req, res) => { + const { replyId, userId } = req.params; + + try { + const deleted = await Replylikes.destroy({ where: { userId, replyId } }); + + if (!deleted) { + return res.status(404).json({ error: "Like not found" }); + } + + // Optional: decrement reply.likes + await Reply.decrement("likes", { where: { id: replyId } }); + + res.status(200).json({ message: "Reply unliked" }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to unlike reply" }); + } +}); + +module.exports = router; diff --git a/app.js b/app.js index 5857036..2574d33 100644 --- a/app.js +++ b/app.js @@ -9,7 +9,7 @@ const apiRouter = require("./api"); const { router: authRouter } = require("./auth"); const { db } = require("./database"); const cors = require("cors"); - +const initSocketServer = require("./socket-server"); const PORT = process.env.PORT || 8080; const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; @@ -41,9 +41,12 @@ const runApp = async () => { try { await db.sync(); console.log("✅ Connected to the database"); - app.listen(PORT, () => { + const server = app.listen(PORT, () => { console.log(`🚀 Server is running on port ${PORT}`); }); + + initSocketServer(server); + console.log("🧦 Socket server initialized"); } catch (err) { console.error("❌ Unable to connect to the database:", err); } diff --git a/auth/index.js b/auth/index.js index 07968c5..bd2b05b 100644 --- a/auth/index.js +++ b/auth/index.js @@ -6,6 +6,13 @@ const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; +const cookieSettings = { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: process.env.NODE_ENV === "production" ? "none" : "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours +}; + // Middleware to authenticate JWT tokens const authenticateJWT = (req, res, next) => { const token = req.cookies.token; @@ -79,12 +86,7 @@ router.post("/auth0", async (req, res) => { { expiresIn: "24h" } ); - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 24 * 60 * 60 * 1000, // 24 hours - }); + res.cookie("token", token, cookieSettings); res.send({ message: "Auth0 authentication successful", @@ -140,12 +142,7 @@ router.post("/signup", async (req, res) => { { expiresIn: "24h" } ); - res.cookie("token", token, { - httpOnly: true, - secure: true, - sameSite: "strict", - maxAge: 24 * 60 * 60 * 1000, // 24 hours - }); + res.cookie("token", token, cookieSettings); res.send({ message: "User created successfully", @@ -191,12 +188,7 @@ router.post("/login", async (req, res) => { { expiresIn: "24h" } ); - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 24 * 60 * 60 * 1000, // 24 hours - }); + res.cookie("token", token, cookieSettings); res.send({ message: "Login successful", diff --git a/database/db.js b/database/db.js index b251a9d..3b580f1 100644 --- a/database/db.js +++ b/database/db.js @@ -3,7 +3,7 @@ const { Sequelize } = require("sequelize"); const pg = require("pg"); // Feel free to rename the database to whatever you want! -const dbName = "capstone-1"; +const dbName = "capstone-2"; const db = new Sequelize( process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, diff --git a/database/index.js b/database/index.js index e498df6..621e317 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,62 @@ const db = require("./db"); -const User = require("./user"); +const User = require("./models/user"); +const Post = require("./models/post"); +const Reply = require("./models/reply"); +const Forum = require("./models/forum"); +const Postlikes = require("./models/postlikes"); +const Replylikes = require("./models/replylikes"); + +// Associations +Forum.hasMany(Post, { foreignKey: "forumId" }); // A forum can have many posts + +Post.belongsTo(Forum, { foreignKey: "forumId" }); // A post belongs to a forum + +Post.belongsTo(User, { foreignKey: "userId" }); // A post belongs to a user + +Post.hasMany(Reply, { foreignKey: "postId" }); // A post can have many replies + +Reply.belongsTo(Post, { foreignKey: "postId" }); // A reply belongs to a post + +Reply.belongsTo(User, { foreignKey: "userId" }); // A reply belongs to a user + +Reply.belongsTo(Reply, { foreignKey: "parentId", as: "parentReply" }); // A reply can have a parent reply + +Reply.hasMany(Reply, { foreignKey: "parentId", as: "childReplies" }); // A reply can have many child replies + +// Post <-> User likes +User.belongsToMany(Post, { + through: Postlikes, + foreignKey: "userId", + otherKey: "postId", + as: "likedPosts", +}); +Post.belongsToMany(User, { + through: Postlikes, + foreignKey: "postId", + otherKey: "userId", + as: "likedByUsers", +}); + +// Reply <-> User likes +User.belongsToMany(Reply, { + through: Replylikes, + foreignKey: "userId", + otherKey: "replyId", + as: "likedReplies", +}); +Reply.belongsToMany(User, { + through: Replylikes, + foreignKey: "replyId", + otherKey: "userId", + as: "likedByUsers", +}); module.exports = { db, User, + Post, + Forum, + Reply, + Postlikes, + Replylikes, }; diff --git a/database/models/forum.js b/database/models/forum.js new file mode 100644 index 0000000..7fffa43 --- /dev/null +++ b/database/models/forum.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); +const db = require('../db'); + +const Forum = db.define('forum', { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, +}); + +module.exports = Forum; \ No newline at end of file diff --git a/database/models/post.js b/database/models/post.js new file mode 100644 index 0000000..a7ce59c --- /dev/null +++ b/database/models/post.js @@ -0,0 +1,31 @@ +const { DataTypes } = require('sequelize'); +const db = require('../db'); + +const Post = db.define('post', { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + likes: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, +}); + +module.exports = Post; \ No newline at end of file diff --git a/database/models/postlikes.js b/database/models/postlikes.js new file mode 100644 index 0000000..a9ccd9f --- /dev/null +++ b/database/models/postlikes.js @@ -0,0 +1,22 @@ +const { DataTypes } = require('sequelize'); +const db = require('../db'); + +const Postlikes = db.define("Postlikes", { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + }, + postId: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + }, +}, + { + timestamps: false, + tableName: "postlikes", + freezeTableName: true, +}); + +module.exports = Postlikes; \ No newline at end of file diff --git a/database/models/reply.js b/database/models/reply.js new file mode 100644 index 0000000..2a3ef8d --- /dev/null +++ b/database/models/reply.js @@ -0,0 +1,31 @@ +const { DataTypes } = require('sequelize'); +const db = require('../db'); + +const Reply = db.define('reply', { + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + postId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + likes: { + type: DataTypes.INTEGER, + defaultValue: 0, + }, + createdAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, +}); + +module.exports = Reply; \ No newline at end of file diff --git a/database/models/replylikes.js b/database/models/replylikes.js new file mode 100644 index 0000000..85aac96 --- /dev/null +++ b/database/models/replylikes.js @@ -0,0 +1,22 @@ +const { DataTypes } = require("sequelize"); +const db = require("../db"); + +const Replylikes = db.define("Replylikes", { + userId: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + }, + replyId: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + }, +}, + { + timestamps: false, + tableName: "replylikes", + freezeTableName: true, +}); + +module.exports = Replylikes; diff --git a/database/models/user.js b/database/models/user.js new file mode 100644 index 0000000..b65001a --- /dev/null +++ b/database/models/user.js @@ -0,0 +1,51 @@ +const { DataTypes } = require("sequelize"); +const db = require("../db"); +const bcrypt = require("bcrypt"); + +const User = db.define("user", { + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + passwordHash: { + type: DataTypes.STRING, + allowNull: false, + }, + auth0Id: { + type: DataTypes.STRING, + allowNull: true, + unique: true, + }, + email: { + type: DataTypes.STRING, + unique: true, + validate: { + isEmail: true, + notEmpty: true, + }, + }, + isAdmin: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + isDisable: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, +}); +// Instance method to check password +User.prototype.checkPassword = function (password) { + if (!this.passwordHash) { + return false; // Auth0 users don't have passwords + } + return bcrypt.compareSync(password, this.passwordHash); +}; + +// Class method to hash password +User.hashPassword = function (password) { + return bcrypt.hashSync(password, 10); +}; +module.exports = User; diff --git a/database/seed.js b/database/seed.js index e58b595..300d30e 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,19 +1,174 @@ const db = require("./db"); const { User } = require("./index"); +const { Post } = require("./index"); +const { Reply } = require("./index"); +const { Forum } = require("./index"); +const { Postlikes } = require("./index"); +const { Replylikes } = require("./index"); const seed = async () => { try { db.logging = false; await db.sync({ force: true }); // Drop and recreate tables - const users = await User.bulkCreate([ - { username: "admin", passwordHash: User.hashPassword("admin123") }, - { username: "user1", passwordHash: User.hashPassword("user111") }, - { username: "user2", passwordHash: User.hashPassword("user222") }, - ]); + // USERS + const users = await User.bulkCreate([ + { username: "hailia", passwordHash: User.hashPassword("hai123"), email: "hailia@example.com" }, + { username: "alex", passwordHash: User.hashPassword("alex123"), email: "alex@example.com" }, + { username: "phone", passwordHash: User.hashPassword("phone123"), email: "phone@example.com" }, + { username: "darrel", passwordHash: User.hashPassword("darrel123"), email: "darrel@example.com" }, + ], { returning: true }); + + const [hailia, alex, phone, darrel] = users; + + // FORUMS + const forums = await Forum.bulkCreate([ + { name: "Projectile Motion" }, + { name: "Friction" }, + { name: "Free Fall" }, + { name: "Torque" }, + { name: "Inertia" }, + ], { returning: true }); + + const [projectileForum, frictionForum, freeFallForum, torqueForum, inertiaForum] = forums; + + // POSTS + const posts = await Post.bulkCreate([ + { + title: "How to calculate max height?", + content: "I’m confused about which formula to use when given initial velocity and angle.", + forumId: projectileForum.id, + userId: hailia.id, + }, + { + title: "How to calculate max width?", + content: "Idk bro physics is hard", + forumId: projectileForum.id, + userId: hailia.id, + }, + { + title: "Static vs kinetic friction", + content: "When does static friction switch to kinetic friction? Is it instantaneous?", + forumId: frictionForum.id, + userId: alex.id, + }, + { + title: "Does mass affect fall speed?", + content: "If I drop a feather and a rock, why don’t they fall the same?", + forumId: freeFallForum.id, + userId: phone.id, + }, + { + title: "What exactly is torque?", + content: "I don’t understand torque — is it just force but twisty?", + forumId: torqueForum.id, + userId: darrel.id, + }, + { + title: "How does inertia work?", + content: "Why do objects in motion stay in motion? Magic?", + forumId: inertiaForum.id, + userId: alex.id, + }, + ], { returning: true }); + + const [post1, post2, post3, post4, post5, post6] = posts; + + // REPLIES + const replies = await Reply.bulkCreate([ + // Projectile Motion + { + content: "Use v² = v₀² - 2g(y - y₀), or break velocity into vertical component!", + postId: post1.id, + userId: alex.id, + }, + { + content: "Ah, so I should use v₀y = v₀ * sin(θ)?", + postId: post1.id, + userId: hailia.id, + parentId: 1, + }, + { + content: "Yes, that’s it! Then solve for y using kinematic equations.", + postId: post1.id, + userId: phone.id, + parentId: 2, + }, + + // Friction + { + content: "Yes — and kinetic friction kicks in once the object is moving.", + postId: post3.id, + userId: hailia.id, + }, + { + content: "Static friction can vary but maxes out. Once exceeded, motion begins.", + postId: post3.id, + userId: darrel.id, + }, + + // Free Fall + { + content: "Air resistance slows the feather down. In a vacuum, they'd fall together.", + postId: post4.id, + userId: alex.id, + }, + { + content: "Try looking up the Galileo drop test!", + postId: post4.id, + userId: hailia.id, + parentId: 6, + }, + + // Torque + { + content: "Torque = force × distance from pivot point. Think of a wrench.", + postId: post5.id, + userId: phone.id, + }, + { + content: "So the longer the lever, the more torque?", + postId: post5.id, + userId: darrel.id, + parentId: 8, + }, + + // Inertia + { + content: "It's Newton’s 1st Law — no magic, just physics!", + postId: post6.id, + userId: hailia.id, + }, + { + content: "Inertia resists changes in motion. Mass affects it.", + postId: post6.id, + userId: alex.id, + }, + ]); - console.log(`👤 Created ${users.length} users`); + //Postlikes + const postlikes = await Postlikes.bulkCreate([ + { + postId: post1.id, + userId: alex.id, + }, + { + postId: post2.id, + userId: hailia.id, + }, + ]); + //Replylikes + const replylikes = await Replylikes.bulkCreate([ + { + replyId: 1, + userId: alex.id, + }, + { + replyId: 2, + userId: hailia.id, + } + ]); // Create more seed data here once you've created your models // Seed files are a great way to test your database schema! diff --git a/package-lock.json b/package-lock.json index af0cf82..2f758cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "capstone-i-backend", + "name": "capstone-2-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-2-backend", "version": "1.0.0", "license": "ISC", "dependencies": { @@ -17,7 +17,8 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "socket.io": "^4.8.1" }, "devDependencies": { "nodemon": "^3.1.10" @@ -26,6 +27,21 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "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", @@ -90,6 +106,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -408,6 +433,95 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1608,6 +1722,141 @@ "node": ">=10" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1773,6 +2022,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 7e0a0af..05c4e0c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "capstone-1-backend", + "name": "capstone-2-backend", "version": "1.0.0", "main": "index.js", "scripts": { @@ -21,7 +21,8 @@ "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "pg": "^8.16.2", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "socket.io": "^4.8.1" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index f63c4b5..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/index.html b/public/index.html index 6c972d9..77ae02c 100644 --- a/public/index.html +++ b/public/index.html @@ -3,13 +3,16 @@ - - Capstone I Backend + + Capstone II Backend -

Welcome to the Capstone I Backend!

+

Welcome to the Capstone II Backend!

To get started, open your terminal and install the packages:

npm install
diff --git a/public/style.css b/public/style.css index b4a0453..e10fa09 100644 --- a/public/style.css +++ b/public/style.css @@ -1,5 +1,5 @@ body { - background-color: #8fcb9b; + background-color: #bcd4de; font-family: Arial, sans-serif; } diff --git a/socket-server.js b/socket-server.js new file mode 100644 index 0000000..d887933 --- /dev/null +++ b/socket-server.js @@ -0,0 +1,35 @@ +const { Server } = require("socket.io"); + +let io; + +const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; +const corsOptions = + process.env.NODE_ENV === "production" + ? { + origin: FRONTEND_URL, + credentials: true, + } + : { + cors: "*", + }; + +const initSocketServer = (server) => { + try { + io = new Server(server, corsOptions); + + io.on("connection", (socket) => { + console.log(`🔗 User ${socket.id} connected to sockets`); + + socket.on("disconnect", () => { + console.log(`🔗 User ${socket.id} disconnected from sockets`); + }); + + // Define event handlers here... + }); + } catch (error) { + console.error("❌ Error initializing socket server:"); + console.error(error); + } +}; + +module.exports = initSocketServer; diff --git a/vercel.json b/vercel.json index 1b0a308..8be8eb5 100644 --- a/vercel.json +++ b/vercel.json @@ -9,7 +9,8 @@ "routes": [ { "src": "/(.*)", - "dest": "app.js" + "dest": "app.js", + "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] } ] }