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/.gitignore b/.gitignore index 400bedc..8f30558 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .env .vercel +.replit \ No newline at end of file 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/group.js b/api/group.js new file mode 100644 index 0000000..539f4bc --- /dev/null +++ b/api/group.js @@ -0,0 +1,264 @@ +const express = require("express"); +const router = express.Router(); +const { authenticateJWT } = require("../auth"); +const { User, Group, Invite, UserGroups, Receipts } = require("../database"); + +router.patch("/:editGroup", async (req, res) => { + try { + const id = Number(req.params.editGroup); + const groupTOPatch = await Group.findByPk(id); + // if (!Group) { + // res.status(500).json({ error: "groups does not exists" }); + // } + await groupTOPatch.update({ + groupName: req.body.groupName, + description: req.body.description, + }); + + await groupTOPatch.save(); + res.sendStatus(200); + } catch (err) { + console.error("can not update groups"); + res.status(400).json({ error: "not able to work" }); + } +}); + +router.get("/myGroups", async (req, res) => { + try { + const userId = 1; + //req.user?.id; + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + const getGroups = await user.getGroups(); + res.status(200).send(getGroups); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Could not fetch groups" }); + } +}); + +router.get("./get", async (req, res) => { + try { + const displayGroups = await Group.findAll(); + console.log(displayGroups); + res.status(200).send(displayGroups); + } catch (err) { + res.send("can not display all the groups").send(400); + } +}); + +// create a group +router.post("/create", authenticateJWT, async (req, res) => { + const userId = req.user?.id; + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const { description, groupName } = req.body; + console.log("Request body:", req.body); + + try { + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + const newGroup = await Group.create({ + Owner: userId, + description, + groupName, + }); + + res + .status(201) + .json({ message: "Group created successfully", group: newGroup }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to create group" }); + } +}); + +//delete a group (only owner is supposed to this) +router.delete("/delete/:id", authenticateJWT, async (req, res) => { + try { + const userId = req.user?.id; //logged in user + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const groupId = Number(req.params.id); + + if (!groupId) { + return res.status(400).json({ error: "Group ID is required" }); + } + + const group = await Group.findByPk(groupId); + if (!group) { + return res.status(404).json({ error: "Group does not exist" }); + } + + //check if logged-in user is the owner + if (group.Owner !== userId) { + //403, Forbidden status means the server understood the request but refuses to authorize it + return res.status(403).json({ error: "Forbidden: Not group owner" }); + } + + await group.destroy(); + res.status(200).json({ message: "Group deleted successfully" }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Could not delete group" }); + } +}); + +//Create an Invite +router.post("/invite", authenticateJWT, async (req, res) => { + try { + const userId = req.user?.id; // logged in user + + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const { receiverId, GroupId } = req.body; + + const group = await Group.findByPk(GroupId); + if (!group) { + return res.status(404).json({ error: "Group not found" }); + } + + const receiver = await User.findByPk(receiverId); + if (!receiver) { + return res.status(404).json({ error: "Receiver user not found" }); + } + + if (receiverId === userId) { + return res.status(400).json({ error: "Cannot invite yourself" }); + } + + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json({ error: "Sender user not found" }); + } + + // Check if the sender/user is part of the group. + // This line fetches all groups the user belongs to, but filters only for the current GroupId. + // If the returned array is empty, it means the user is not a member of that group. + const groups = await user.getGroups({ where: { id: GroupId } }); + if (groups.length === 0) { + return res + .status(403) + .json({ error: "You must be part of the group to send invites" }); + } + + //Check if there is already a pending invite from the same sender to the same receiver for the same group. + const existingInvite = await Invite.findOne({ + where: { + senderId: userId, + receiverId, + GroupId, + status: "pending", + }, + }); + + if (existingInvite) { + return res.status(400).json({ error: "Invite already sent and pending" }); + } + + const invite = await Invite.create({ + senderId: userId, + receiverId, + GroupId, + status: "pending", + }); + + res.status(200).json(invite); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to create invite" }); + } +}); + +//Invite a user to a group(adding) +router.post("/invite/:invId/accept", authenticateJWT, async (req, res) => { + try { + const invite = await Invite.findByPk(req.params.invId); + + if (!invite || invite.status !== "pending") { + return res + .status(404) + .json({ error: "Invite not found or already handled" }); + } + + const user = await User.findByPk(invite.receiverId); //invite.receiverId + console.log(user); + const group = await Group.findByPk(invite.GroupId); //invite.GroupId + console.log(group); + if (!user || !group) { + return res.status(404).json({ error: "User or group not found" }); + } + + //this auto adds a user to a group, and updates the status to accepted + await group.addUser(user); + invite.status = "accepted"; + await invite.save(); + + res.status(200).json({ message: "Invite accepted, user added to group" }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to accept invite" }); + } +}); + +//decline an invite +router.post("/invite/:id/decline", authenticateJWT, async (req, res) => { + try { + const invite = await Invite.findByPk(req.params.id); + if (!invite || invite.status !== "pending") { + return res + .status(404) + .json({ error: "Invite not found or already handled" }); + } + + const user = await User.findByPk(invite.receiverId); + const group = await Group.findByPk(invite.GroupId); + + if (!user || !group) { + return res.status(404).json({ error: "User or group not found" }); + } + + //decline invite + invite.status = "declined"; + await invite.save(); + res.status(200).json({ message: "Invite declined" }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to decline invite" }); + } +}); + +//get a repcipt based of group +router.get("/:id", async (req, res) => { + try { + const getUrl = Number(req.params.id); + const group = await Group.findByPk(getUrl); + if (!Group) { + res.sendStatus(404); + } + if (!Receipts) { + res.sendStatus(404); + } + const groupReceipts = await Receipts.findAll({ + where: { Group_Id: group.id }, + }); + res.status(200).send(groupReceipts); + } catch (err) { + console.error("can not get groups based of repcipts"); + res.status(400).json({ error: "not able to find the group" }); + } +}); + +module.exports = router; diff --git a/api/index.js b/api/index.js index f08162e..c721057 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,14 @@ const express = require("express"); const router = express.Router(); -const testDbRouter = require("./test-db"); +const users = require("./users"); +const group = require("./group"); +const receiptsRouter = require("./receipts"); -router.use("/test-db", testDbRouter); +//DB API +router.use("/users", users); +router.use("/group", group); +router.use("/receipts", receiptsRouter); module.exports = router; + + diff --git a/api/receipts.js b/api/receipts.js new file mode 100644 index 0000000..3899565 --- /dev/null +++ b/api/receipts.js @@ -0,0 +1,168 @@ +const express = require("express"); +const router = express.Router(); +const { authenticateJWT } = require("../auth"); +const { Receipts, Item, Group } = require("../database"); + +// GET all receipts +router.get("/", async (req, res) => { + try { + const receipts = await Receipts.findAll(); + res.status(200).send(receipts); + } catch (error) { + console.error("Error fetching receipts:", error); + res.status(500).send("Failed to fetch receipts."); + } +}); + +// GET a specific receipt by id +router.get("/:id", async (req, res) => { + try { + const receiptId = Number(req.params.id); + const receipt = await Receipts.findByPk(receiptId); + if (!receipt) { + res.status(400).send("Receipt not found"); + } + res.status(200).send(receipt); + } catch (error) { + console.error("Error fetching receipt by ID:", error); + res.status(500).send("Failed to find the receipt you were looking for."); + } +}); + +// GET all items associated with a receipt +router.get("/:id/items", async (req, res) => { + try { + const receiptId = Number(req.params.id); + const receipt = await Receipts.findByPk(receiptId); + if (!receipt) { + res.status(400).send("Receipt not found"); + } + const items = await receipt.getItems(); + res.status(200).send(items); + } catch (error) { + console.error("Error fetching items by receipt ID:", error); + res.status(500).send("Failed to find the items you were looking for."); + } +}); + +/* POST a receipt and its items, assuming that the request includes: +- receipt: Object +{ + title: string, + body: string, + User_Id: int + Group_Id: int +} +- items: Array of Objects +[ + {name: string, price: float}, ... +] +*/ + +//update this where it belongs to user, group โ + +router.post("/", authenticateJWT, async (req, res) => { + try { + const { receipt, items } = req.body; + + + const userId = req.user ? req.user.id : null; + + + const newReceipt = await Receipts.create({ + ...receipt, + User_Id: userId, + uploaded_by: userId, + }); + + const newReceiptId = newReceipt.id; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + item.Receipt_id = newReceiptId; + await newReceipt.createItem(item); + } + + res.status(200).send(newReceipt); + } catch (error) { + console.error("Error posting receipt:", error); + res.status(500).send("Failed to post receipt."); + } +}); + + +// DELETE a receipt +router.delete("/:id", authenticateJWT, async (req, res) => { + try { + const userId = req.user?.id; + const receiptId = Number(req.params.id); + const receipt = await Receipts.findByPk(receiptId); + + if (!receipt) { + return res.status(404).send("Receipt not found"); + } + + if (receipt.UploadedBy !== userId) { + return res.status(403).send("Not authorized to delete this receipt"); + } + + await receipt.destroy(); + res.status(200).send("Receipt deleted"); + } catch (error) { + console.error("Error deleting receipt:", error); + res.status(500).send("Failed to delete receipt."); + } +}); + +//(HERE NEW POST) +//upload a receipt +router.post("/:id/Upload", authenticateJWT, async (req, res) => { + try { + const userId = req.user?.id; // logged in user + if (!userId) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const groupId = Number(req.params.id); + if (!groupId) { + return res.status(400).json({ error: "Group ID is required" }); + } + + const group = await Group.findByPk(groupId); + if (!group) { + return res.status(404).json({ error: "Group does not exist" }); + } + + const { receipt, items, Category } = req.body; + + if (!receipt || !items || !Array.isArray(items) || items.length === 0) { + return res.status(400).json({ error: "Receipt and items are required" }); + } + + // Create the receipt + const newReceipt = await Receipts.create({ + ...receipt, + GroupId: groupId, + uploaded_by: userId, + category: Category, + }); + + const receiptItems = items.map((item) => ({ + ...item, + ReceiptId: newReceipt.id, + })); + + await Item.bulkCreate(receiptItems); + + res.status(201).json({ + message: "Receipt uploaded successfully", + receipt: newReceipt, + items: receiptItems, + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to upload receipt to group" }); + } +}); + +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..a831a34 --- /dev/null +++ b/api/users.js @@ -0,0 +1,130 @@ +const express = require("express"); +const router = express.Router(); +const { authenticateJWT } = require("../auth"); +const { User, Group, Receipts } = require("../database"); +//const { adminAuthenticate, authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); // Used for case-insenitive matching + +// get all users +router.get("/Allusers", authenticateJWT, async (req, res) => { + try { + const { page = 1, limit = 10, search = "" } = req.query; + const offset = (page - 1) * limit; + + // Build search filter + const whereClause = search + ? { + [Op.or]: [ + { username: { [Op.iLike]: `%${search}%` } }, + /*{ email: { [Op.iLike]: `%${search}%`} },*/ + ], + } + : {}; + + // This fetches the users and total count + const { count, rows: users } = await User.findAndCountAll({ + where: whereClause, + limit: parseInt(limit), + offset: parseInt(offset), + order: [["username", "ASC"]], + }); + + res.status(200).json({ + totalUsers: count, + currentPage: parseInt(page), + totalPages: Math.ceil(count / limit), + users, + }); + } catch (err) { + console.error("error finding all users", err); + res.status(500).json({ message: "internal server error" }); + } +}); + +router.get("/me", authenticateJWT, async (req, res) => { + try { + const userId = req.user?.id; // logged in user + + if (!userId) { + return res.status(404).json({ error: "Unauthorized" }); + } + + const userInfo = await User.findByPk(userId); + if (!userInfo) { + return res.status(404).json({ error: "user not found" }); + } + res.send({ + message: "Display user specific info successful", + userInfo: { + username: userInfo.username, + profilePic: userInfo.profilePic, + email: userInfo.email, + firstName: userInfo.firstName, + lastName: userInfo.lastName, + }, + }); + } catch (err) { + console.error("User info not fetched", err); + res.status(500).json({ message: "internal server error" }); + } +}); +//to update user info. +router.patch("/:editUser", async (req, res) => { + try { + const id = Number(req.params.editUser); + const userPatch = await User.findByPk(id); + await userPatch.update({ + firstName: req.body.firstName, + lastName: req.body.lastName, + email: req.body.email, + profilePic: req.body.profilePic, + }); + await userPatch.save(); + res.sendStatus(200); + } catch (err) { + console.error("can not update user", err); + res.sendStatus(400); + } +}); + +// Search Route (autocomplete) +router.get("/search", async (req, res) => { + try { + const { query } = req.query; + + if (!query) { + return res.status(400).json({ message: "Query is required" }); + } + + const users = await User.findAll({ + where: { + username: { + [Op.iLike]: `%${query}%`, // Matches input anywhere, it is case-insensitive + }, + }, + attributes: ["id", "username"], // Returns specific user + limit: 10, + }); + + res.json(users); + } catch (error) { + console.error("Error searching users:", error); + res.status(500).json({ message: "Internal server error" }); + } +}); +//get recpit based of user +router.get("/:id", async (req, res) => { + try { + const urlId = Number(req.params.id); + const user = await User.findByPk(urlId); + const userReceipts = await Receipts.findAll({ + where: { User_Id: user.id }, + }); + res.status(200).send(userReceipts); + } catch (err) { + console.error("cant not recive and a recpit baseed of id", err); + res.status(500).json({ error: "can not get repictpt based of of user" }); + } +}); + +module.exports = router; diff --git a/app.js b/app.js index 5857036..ab1cc9b 100644 --- a/app.js +++ b/app.js @@ -9,12 +9,13 @@ 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"; // body parser middleware -app.use(express.json()); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use( cors({ @@ -28,6 +29,13 @@ app.use(cookieParser()); app.use(morgan("dev")); // logging middleware app.use(express.static(path.join(__dirname, "public"))); // serve static files from public folder + +//test for group +app.use((req, res, next) => { + req.user = { id: 1 }; // mock user id 1 + next(); +}); + app.use("/api", apiRouter); // mount api router app.use("/auth", authRouter); // mount auth router @@ -41,9 +49,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..2d6ddc3 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", @@ -104,7 +106,7 @@ router.post("/auth0", async (req, res) => { // Signup route router.post("/signup", async (req, res) => { try { - const { username, password } = req.body; + const { firstName, lastName, username, password, email, profilePic } = req.body; if (!username || !password) { return res @@ -126,7 +128,7 @@ router.post("/signup", async (req, res) => { // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ username, passwordHash }); + const user = await User.create({ username, passwordHash, firstName, lastName, email, profilePic }); // Generate JWT token const token = jwt.sign( @@ -140,16 +142,11 @@ 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", - user: { id: user.id, username: user.username }, + user: { id: user.id, username: user.username, firstName: user.firstName, lastName: user.lastName, profilePic: user.profileName, email: user.email }, }); } catch (error) { console.error("Signup error:", error); @@ -191,16 +188,11 @@ 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", - user: { id: user.id, username: user.username }, + user: { id: user.id, username: user.username, firstName: user.firstName, lastname: user.lastName, email: user.email, profilePic: user.profilePic, }, }); } catch (error) { console.error("Login error:", error); 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/groups.js b/database/groups.js new file mode 100644 index 0000000..70d3d79 --- /dev/null +++ b/database/groups.js @@ -0,0 +1,31 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Group = db.define("groups", { + Owner: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "users", + key: "id", + }, + }, + + // maybe something we can use? + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + + groupName: { + type: DataTypes.TEXT, + allowNull: true, + }, + + Receipt_Id: { + type: DataTypes.INTEGER, + allowNull: true, + }, +}); + +module.exports = Group; diff --git a/database/index.js b/database/index.js index e498df6..8db67b8 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,46 @@ const db = require("./db"); const User = require("./user"); +const Receipts = require("./receipts") +const Item = require("./item"); +const Group = require("./groups"); +const Invite = require("./invite"); + +// 1. Users and Receipts +User.hasMany(Receipts, { foreignKey: "User_Id" }); +Receipts.belongsTo(User, { foreignKey: "User_Id" }); + +// 2. Receipts and Items +Receipts.hasMany(Item, { foreignKey: "Receipt_Id" }); +Item.belongsTo(Receipts, { foreignKey: "Receipt_Id" }); + +// 3. Users and Groups (Owner) +User.hasMany(Group, { foreignKey: "Owner" }); + +// 4. Users and Groups (Members) +Group.belongsToMany(User, { through: "UserGroups" }); +User.belongsToMany(Group, { through: "UserGroups" }); + +// 5. Group has Receipts +Group.hasMany(Receipts, { foreignKey: "Group_Id" }); +Receipts.belongsTo(Group, { foreignKey: "Group_Id" }); + +//Keeps the group but owner is set to null, is user for some reason removed +Group.belongsTo(User, { foreignKey: "Owner", onDelete: "SET NULL" }); + +// 6. Invites +User.hasMany(Invite, {as: "sentInvites", foreignKey: "senderId" }); +User.hasMany(Invite, {as: "receivedInvites", foreignKey: "receiverId"}); + +Invite.belongsTo(User, {as: "sender", foreignKey: "senderId" }); +Invite.belongsTo(User, {as: "receiver", foreignKey: "receiverId" }); +Invite.belongsTo(Group, { foreignKey: "GroupId" }); + module.exports = { db, User, + Receipts, + Item, + Group, + Invite, }; diff --git a/database/invite.js b/database/invite.js new file mode 100644 index 0000000..2bee832 --- /dev/null +++ b/database/invite.js @@ -0,0 +1,27 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Invite = db.define("invite", { + senderId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + receiverId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + GroupId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + status: { + type: DataTypes.ENUM("pending", "accepted", "declined"), + defaultValue: "pending", + allowNull: false, + }, +}); + +module.exports = Invite; diff --git a/database/item.js b/database/item.js new file mode 100644 index 0000000..3f506ea --- /dev/null +++ b/database/item.js @@ -0,0 +1,21 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Item = db.define("items", { + name: { + type: DataTypes.STRING, + allowNull: false, + }, + + price: { + type: DataTypes.FLOAT, + allowNull: false, + }, + + Receipt_Id: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}); + +module.exports = Item; diff --git a/database/receipts.js b/database/receipts.js new file mode 100644 index 0000000..52c1a5a --- /dev/null +++ b/database/receipts.js @@ -0,0 +1,42 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Receipts = db.define("receipts", { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + + body: { + type: DataTypes.TEXT, + allowNull: false, + }, + + User_Id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + + Group_Id: { + type: DataTypes.INTEGER, + allowNull: true, + }, + + // Defines foreign key constraint linking this column to the primary key + uploaded_by: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "users", + key: "id", + }, + }, + + category: { + type: DataTypes.STRING, + allowNull: true, + }, + +}); + +module.exports = Receipts; diff --git a/database/seed.js b/database/seed.js index e58b595..5217527 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,5 @@ const db = require("./db"); -const { User } = require("./index"); +const { User, Item, Receipts, Group } = require("./index"); const seed = async () => { try { @@ -7,15 +7,81 @@ const seed = async () => { await db.sync({ force: true }); // Drop and recreate tables const users = await User.bulkCreate([ - { username: "admin", passwordHash: User.hashPassword("admin123") }, - { username: "user1", passwordHash: User.hashPassword("user111") }, - { username: "user2", passwordHash: User.hashPassword("user222") }, + { + username: "admin", + passwordHash: User.hashPassword("admin123"), + firstName: "Alice", + lastName: "Admin", + email: "admin@example.com", + profilePic: "https://i.pravatar.cc/150?img=1", + }, + { + username: "user1", + passwordHash: User.hashPassword("user111"), + firstName: "Bob", + lastName: "Builder", + email: "bob@example.com", + profilePic: "https://i.pravatar.cc/150?img=1", + }, + { + username: "user2", + passwordHash: User.hashPassword("user222"), + firstName: "Carol", + lastName: "Coder", + email: "carol@example.com", + profilePic: "https://i.pravatar.cc/150?img=1", + }, + { + username: "user3", + passwordHash: User.hashPassword("user333"), + firstName: "Mike", + lastName: "Snake", + email: "Snake@example.com", + profilePic: "https://i.pravatar.cc/150?img=1", + }, ]); 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 groups = await Group.bulkCreate([ + { + Owner: users[0].id, + groupName: "Family", + description: "Family group for shared expenses", + }, + { + Owner: users[1].id, + groupName: "Friends", + description: "Friends group for outings", + }, + ]); + + console.log(`๐ฅ Created ${groups.length} groups`); + + const receipts = await Receipts.bulkCreate([ + { + title: "Grocery Shopping", + body: "Bought fruits and vegetables", + User_Id: users[0].id, + Group_Id: groups[0].id, + }, + { + title: "Electronics Purchase", + body: "Bought a new laptop", + User_Id: users[1].id, + Group_Id: groups[1].id, + }, + ]); + + console.log(`๐งพ Created ${receipts.length} receipts`); + + const items = await Item.bulkCreate([ + { name: "Apple", price: 1.2, Receipt_Id: receipts[0].id }, + { name: "Banana", price: 0.8, Receipt_Id: receipts[0].id }, + { name: "Laptop", price: 1200, Receipt_Id: receipts[1].id }, + ]); + + console.log(`๐ฆ Created ${items.length} items`); console.log("๐ฑ Seeded the database"); } catch (error) { diff --git a/database/user.js b/database/user.js index 755c757..dc21d02 100644 --- a/database/user.js +++ b/database/user.js @@ -2,7 +2,23 @@ const { DataTypes } = require("sequelize"); const db = require("./db"); const bcrypt = require("bcrypt"); -const User = db.define("user", { +const User = db.define("users", { + firstName: { + type: DataTypes.STRING, + allowNull: false, + validate: { + len: [1, 30], + } + }, + + lastName: { + type: DataTypes.STRING, + allowNull: false, + validate: { + len: [1, 30], + } + }, + username: { type: DataTypes.STRING, allowNull: false, @@ -11,19 +27,31 @@ const User = db.define("user", { len: [3, 20], }, }, + email: { type: DataTypes.STRING, - allowNull: true, + allowNull: false, unique: true, validate: { isEmail: true, }, }, + + profilePic: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: "https://i.imgur.com/placeholder.png", + validate: { + isUrl: true, + }, + }, + auth0Id: { type: DataTypes.STRING, allowNull: true, unique: true, }, + passwordHash: { type: DataTypes.STRING, allowNull: true, diff --git a/package-lock.json b/package-lock.json index af0cf82..f294f4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { - "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": { + "axios": "^1.11.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -17,7 +18,9 @@ "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", + "tesseract.js": "^6.0.1" }, "devDependencies": { "nodemon": "^3.1.10" @@ -26,6 +29,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", @@ -83,6 +101,23 @@ "node": ">= 8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -90,6 +125,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", @@ -135,6 +179,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -156,9 +206,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -248,6 +298,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -343,6 +405,15 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -408,6 +479,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", @@ -438,6 +598,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -525,6 +700,63 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/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/form-data/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/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -651,6 +883,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -691,6 +938,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -774,6 +1027,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -951,16 +1210,16 @@ } }, "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", - "on-headers": "~1.0.2" + "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" @@ -1017,6 +1276,26 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -1101,9 +1380,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -1118,6 +1397,15 @@ "wrappy": "1" } }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1290,6 +1578,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -1349,6 +1643,12 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, "node_modules/retry-as-promised": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", @@ -1608,6 +1908,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", @@ -1639,6 +2074,30 @@ "node": ">=4" } }, + "node_modules/tesseract.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz", + "integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^6.0.0", + "wasm-feature-detect": "^1.2.11", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz", + "integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==", + "license": "Apache-2.0" + }, "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 +2136,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1740,6 +2205,28 @@ "node": ">= 0.8" } }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/win-node-env": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/win-node-env/-/win-node-env-0.6.1.tgz", @@ -1773,6 +2260,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", @@ -1781,6 +2289,15 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } } } } diff --git a/package.json b/package.json index 7e0a0af..a65c62a 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": { @@ -13,6 +13,7 @@ "license": "ISC", "description": "", "dependencies": { + "axios": "^1.11.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -21,7 +22,9 @@ "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", + "tesseract.js": "^6.0.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 @@
- -To get started, open your terminal and install the packages:
npm installdiff --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..80bff3b --- /dev/null +++ b/socket-server.js @@ -0,0 +1,41 @@ +const { Server } = require("socket.io"); + +let io; + +const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; + +const corsOptions = + process.env.NODE_ENV === "production" + ? { + cors: { + origin: FRONTEND_URL, + credentials: true, + }, + } + : { + cors: { + origin: "*", + methods: ["GET", "POST"], + }, + }; + +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"] } ] }