commit 68d2b950c0d86e6cf3f8f68606991529133ff91b Author: estherdev03 Date: Sun Apr 20 05:56:31 2025 -0600 Finish admin dashboard and update sql code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9d2afa --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Node_modules +*/node_modules +.DS_Store + +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..55e9604 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +### Some ground rules +1. Add both node_modules from Slient and Server to your ```gitignore``` file +2. Make a brach with the following naming conventionp, refix it with your name ```Your-Name Branch-Name```. +--- + +### Frontend +1. `cd frontend` into the dir and then type command +```Bash + #Install the needed lib with the command bellow + npm install + #Start The Server + npm run dev +``` +--- + +### Backend +1. `cd backend` into the dir and then type command +```Bash + #Install the needed lib with the command bellow + npm install + #Start The Server + npm run dev +``` +--- + +### Database +1. MySql Version 9.2.0 +2. To Create the DataBase use the command bellow: +```Bash + 1. mysql -u root + 2. use Marketplace; + 3. \. PathToYour/Schema.sql + 3. \. PathToYour/Init-Data.sql +``` diff --git a/backend/controllers/category.js b/backend/controllers/category.js new file mode 100644 index 0000000..90cbd94 --- /dev/null +++ b/backend/controllers/category.js @@ -0,0 +1,47 @@ +const db = require("../utils/database"); + +exports.getAllCategoriesWithPagination = async (req, res) => { + const limit = +req.query?.limit; + const page = +req.query?.page; + const offset = (page - 1) * limit; + try { + const [data, _] = await db.execute( + "SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?", + [limit.toString(), offset.toString()] + ); + + const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category"); + const { count: total } = result[0]; + return res.json({ data, total }); + } catch (error) { + res.json({ error: "Cannot fetch categories from database!" }); + } +}; + +exports.addCategory = async (req, res) => { + const { name } = req.body; + + try { + const [result] = await db.execute( + "INSERT INTO Category (Name) VALUES (?)", + [name] + ); + res.json({ message: "Adding new category successfully!" }); + } catch (error) { + res.json({ error: "Cannot add new category!" }); + } +}; + +exports.removeCategory = async (req, res) => { + const { id } = req.params; + + try { + const [result] = await db.execute( + `DELETE FROM Category WHERE CategoryID = ?`, + [id] + ); + res.json({ message: "Delete category successfully!" }); + } catch (error) { + res.json({ error: "Cannot remove category from database!" }); + } +}; diff --git a/backend/controllers/history.js b/backend/controllers/history.js new file mode 100644 index 0000000..1c85042 --- /dev/null +++ b/backend/controllers/history.js @@ -0,0 +1,90 @@ +const db = require("../utils/database"); + +exports.HistoryByUserId = async (req, res) => { + const { id } = req.body; + try { + const [data] = await db.execute( + ` + WITH RankedImages AS ( + SELECT + P.ProductID, + P.Name AS ProductName, + P.Price, + P.Date AS DateUploaded, + U.Name AS SellerName, + I.URL AS ProductImage, + C.Name AS Category, + ROW_NUMBER() OVER (PARTITION BY P.ProductID ORDER BY I.URL) AS RowNum + FROM Product P + JOIN Image_URL I ON P.ProductID = I.ProductID + JOIN User U ON P.UserID = U.UserID + JOIN Category C ON P.CategoryID = C.CategoryID + JOIN History H ON H.ProductID = P.ProductID + WHERE H.UserID = ? + ) + SELECT + ProductID, + ProductName, + Price, + DateUploaded, + SellerName, + ProductImage, + Category + FROM RankedImages + WHERE RowNum = 1; + `, + [id], + ); + + res.json({ + success: true, + message: "Products fetched successfully", + data, + }); + } catch (error) { + console.error("Error finding products:", error); + return res.status(500).json({ + found: false, + error: "Database error occurred", + }); + } +}; + +exports.AddHistory = async (req, res) => { + const { userID, productID } = req.body; + console.log(userID); + try { + // Use parameterized query to prevent SQL injection + const [result] = await db.execute( + `INSERT INTO History (UserID, ProductID) VALUES (?, ?)`, + [userID, productID], + ); + + res.json({ + success: true, + message: "Product added to history successfully", + }); + } catch (error) { + console.error("Error adding favorite product:", error); + return res.json({ error: "Could not add favorite product" }); + } +}; + +exports.DelHistory = async (req, res) => { + const { userID, productID } = req.body; + console.log(userID); + try { + // Use parameterized query to prevent SQL injection + const [result] = await db.execute(`DELETE FROM History WHERE UserID=?`, [ + userID, + ]); + + res.json({ + success: true, + message: "Product deleted from History successfully", + }); + } catch (error) { + console.error("Error adding favorite product:", error); + return res.json({ error: "Could not add favorite product" }); + } +}; diff --git a/backend/controllers/product.js b/backend/controllers/product.js new file mode 100644 index 0000000..aa537c0 --- /dev/null +++ b/backend/controllers/product.js @@ -0,0 +1,269 @@ +const db = require("../utils/database"); + +exports.addFavorite = async (req, res) => { + const { userID, productID } = req.body; + console.log(userID); + try { + // Use parameterized query to prevent SQL injection + const [result] = await db.execute( + `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, + [userID, productID] + ); + + res.json({ + success: true, + message: "Product added to favorites successfully", + }); + } catch (error) { + console.error("Error adding favorite product:", error); + return res.json({ error: "Could not add favorite product" }); + } +}; + +exports.removeFavorite = async (req, res) => { + const { userID, productID } = req.body; + console.log(userID); + try { + // Use parameterized query to prevent SQL injection + const [result] = await db.execute( + `DELETE FROM Favorites WHERE UserID = ? AND ProductID = ?`, + [userID, productID] + ); + + res.json({ + success: true, + message: "Product removed from favorites successfully", + }); + } catch (error) { + console.error("Error removing favorite product:", error); + return res.json({ error: "Could not remove favorite product" }); + } +}; + +exports.getFavorites = async (req, res) => { + const { userID } = req.body; + + try { + const [favorites] = await db.execute( + ` + SELECT + p.ProductID, + p.Name, + p.Description, + p.Price, + p.CategoryID, + p.UserID, + p.Date, + u.Name AS SellerName, + MIN(i.URL) AS image_url + FROM Favorites f + JOIN Product p ON f.ProductID = p.ProductID + JOIN User u ON p.UserID = u.UserID + LEFT JOIN Image_URL i ON p.ProductID = i.ProductID + WHERE f.UserID = ? + GROUP BY + p.ProductID, + p.Name, + p.Description, + p.Price, + p.CategoryID, + p.UserID, + p.Date, + u.Name; + `, + [userID] + ); + + res.json({ + success: true, + favorites: favorites, + }); + } catch (error) { + console.error("Error retrieving favorites:", error); + res.status(500).json({ error: "Could not retrieve favorite products" }); + } +}; + +// Get all products along with their image URLs +exports.getAllProducts = async (req, res) => { + try { + const [data, fields] = await db.execute(` + SELECT + P.ProductID, + P.Name AS ProductName, + P.Price, + P.Date AS DateUploaded, + U.Name AS SellerName, + MIN(I.URL) AS ProductImage, + C.Name AS Category +FROM Product P +JOIN Image_URL I ON P.ProductID = I.ProductID +JOIN User U ON P.UserID = U.UserID +JOIN Category C ON P.CategoryID = C.CategoryID +GROUP BY + P.ProductID, + P.Name, + P.Price, + P.Date, + U.Name, + C.Name; + `); + + res.json({ + success: true, + message: "Products fetched successfully", + data, + }); + } catch (error) { + console.error("Error finding products:", error); + return res.status(500).json({ + found: false, + error: "Database error occurred", + }); + } +}; + +exports.getProductById = async (req, res) => { + const { id } = req.params; + console.log("Received Product ID:", id); + + try { + const [data] = await db.execute( + ` + SELECT p.*,U.Name AS SellerName,U.Email as SellerEmail,U.Phone as SellerPhone, i.URL AS image_url + FROM Product p + LEFT JOIN Image_URL i ON p.ProductID = i.ProductID + JOIN User U ON p.UserID = U.UserID + WHERE p.ProductID = ? + `, + [id] + ); + + // Log raw data for debugging + console.log("Raw Database Result:", data); + + if (data.length === 0) { + console.log("No product found with ID:", id); + return res.status(404).json({ + success: false, + message: "Product not found", + }); + } + + // Collect all image URLs + const images = data + .map((row) => row.image_url) + .filter((url) => url !== null); + + // Create product object with all details from first row and collected images + const product = { + ...data[0], // Base product details + images: images, // Collected image URLs + }; + + // Log processed product for debugging + console.log("Processed Product:", product); + + res.json({ + success: true, + message: "Product fetched successfully", + data: product, + }); + } catch (error) { + console.error("Full Error Details:", error); + return res.status(500).json({ + success: false, + message: "Database error occurred", + error: error.message, + }); + } +}; + +exports.getProductWithPagination = async (req, res) => { + const limit = +req.query.limit; + const page = +req.query.page; + + const offset = (page - 1) * limit; + + try { + const [data, fields] = await db.execute( + ` + SELECT + P.ProductID, + P.Name AS ProductName, + P.Price, + P.Date AS DateUploaded, + U.Name AS SellerName, + MIN(I.URL) AS ProductImage, + C.Name AS Category + FROM Product P + LEFT JOIN Image_URL I ON P.ProductID = I.ProductID + LEFT JOIN User U ON P.UserID = U.UserID + LEFT JOIN Category C ON P.CategoryID = C.CategoryID + GROUP BY + P.ProductID, + P.Name, + P.Price, + P.Date, + U.Name, + C.Name + ORDER BY P.ProductID ASC + LIMIT ? OFFSET ? + `, + [limit.toString(), offset.toString()] + ); + + const [result] = await db.execute( + `SELECT COUNT(*) AS totalProd FROM Product` + ); + const { totalProd } = result[0]; + + return res.json({ totalProd, products: data }); + } catch (error) { + res.json({ error: "Error fetching products!" }); + } +}; + +exports.removeProduct = async (req, res) => { + const { id } = req.params; + + try { + const [result] = await db.execute( + `DELETE FROM Product WHERE ProductID = ?`, + [id] + ); + res.json({ message: "Delete product successfully!" }); + } catch (error) { + res.json({ error: "Cannot remove product from database!" }); + } +}; + +// db_con.query( +// "SELECT ProductID FROM product WHERE ProductID = ?", +// [productID], +// (err, results) => { +// if (err) { +// console.error("Error checking product:", err); +// return res.json({ error: "Database error" }); +// } + +// if (results.length === 0) { +// return res.json({ error: "Product does not exist" }); +// } +// }, +// ); + +// db_con.query( +// "INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", +// [userID, productID], +// (err, result) => { +// if (err) { +// console.error("Error adding favorite product:", err); +// return res.json({ error: "Could not add favorite product" }); +// } +// res.json({ +// success: true, +// message: "Product added to favorites successfully", +// }); +// }, +// ); diff --git a/backend/controllers/recommendation.js b/backend/controllers/recommendation.js new file mode 100644 index 0000000..63c9516 --- /dev/null +++ b/backend/controllers/recommendation.js @@ -0,0 +1,53 @@ +const db = require("../utils/database"); + +// TODO: Get the recommondaed product given the userID +exports.RecommondationByUserId = async (req, res) => { + const { id } = req.body; + try { + const [data, fields] = await db.execute( + ` + WITH RankedImages AS ( + SELECT + P.ProductID, + P.Name AS ProductName, + P.Price, + P.Date AS DateUploaded, + U.Name AS SellerName, + I.URL AS ProductImage, + C.Name AS Category, + ROW_NUMBER() OVER (PARTITION BY P.ProductID ORDER BY I.URL) AS RowNum + FROM Product P + JOIN Image_URL I ON P.ProductID = I.ProductID + JOIN User U ON P.UserID = U.UserID + JOIN Category C ON P.CategoryID = C.CategoryID + JOIN Recommendation R ON P.ProductID = R.RecommendedProductID + WHERE R.UserID = ? + ) + SELECT + ProductID, + ProductName, + Price, + DateUploaded, + SellerName, + ProductImage, + Category + FROM RankedImages + WHERE RowNum = 1; + `, + [id], + ); + + console.log(data); + res.json({ + success: true, + message: "Products fetched successfully", + data, + }); + } catch (error) { + console.error("Error finding products:", error); + return res.status(500).json({ + found: false, + error: "Database error occurred", + }); + } +}; diff --git a/backend/controllers/review.js b/backend/controllers/review.js new file mode 100644 index 0000000..e792409 --- /dev/null +++ b/backend/controllers/review.js @@ -0,0 +1,302 @@ +const db = require("../utils/database"); + +/** + * Get reviews for a specific product + * Returns both reviews for the product and reviews by the product owner for other products + */ +exports.getReviews = async (req, res) => { + const { id } = req.params; + console.log("Received Product ID:", id); + + try { + // First query: Get reviews for this specific product + const [productReviews] = await db.execute( + `SELECT + R.ReviewID, + R.UserID, + R.ProductID, + R.Comment, + R.Rating, + R.Date AS ReviewDate, + U.Name AS ReviewerName, + P.Name AS ProductName, + 'product' AS ReviewType + FROM Review R + JOIN User U ON R.UserID = U.UserID + JOIN Product P ON R.ProductID = P.ProductID + WHERE R.ProductID = ?`, + [id], + ); + + // // Second query: Get reviews written by the product owner for other products + // const [sellerReviews] = await db.execute( + // `SELECT + // R.ReviewID, + // R.UserID, + // R.ProductID, + // R.Comment, + // R.Rating, + // R.Date AS ReviewDate, + // U.Name AS ReviewerName, + // P.Name AS ProductName, + // 'seller' AS ReviewType + // FROM Review R + // JOIN User U ON R.UserID = U.UserID + // JOIN Product P ON R.ProductID = P.ProductID + // WHERE R.UserID = ( + // SELECT UserID + // FROM Product + // WHERE ProductID = ? + // ) + // AND R.ProductID != ?`, + // [id, id], + // ); + + // Combine the results + const combinedReviews = [...productReviews]; + + // Log data for debugging + console.log("Combined Reviews:", combinedReviews); + + res.json({ + success: true, + message: "Reviews fetched successfully", + data: combinedReviews, + }); + } catch (error) { + console.error("Full Error Details:", error); + return res.status(500).json({ + success: false, + message: "Database error occurred", + error: error.message, + }); + } +}; + +/** + * Submit a new review for a product + */ +exports.submitReview = async (req, res) => { + const { productId, userId, rating, comment } = req.body; + + // Validate required fields + if (!productId || !userId || !rating || !comment) { + return res.status(400).json({ + success: false, + message: "Missing required fields", + }); + } + + // Validate rating is between 1 and 5 + if (rating < 1 || rating > 5) { + return res.status(400).json({ + success: false, + message: "Rating must be between 1 and 5", + }); + } + + try { + // Check if user has already reviewed this product + const [existingReview] = await db.execute( + `SELECT ReviewID FROM Review WHERE ProductID = ? AND UserID = ?`, + [productId, userId], + ); + + if (existingReview.length > 0) { + return res.status(400).json({ + success: false, + message: "You have already reviewed this product", + }); + } + + // Check if user is trying to review their own product + const [productOwner] = await db.execute( + `SELECT UserID FROM Product WHERE ProductID = ?`, + [productId], + ); + + if (productOwner.length > 0 && productOwner[0].UserID === userId) { + return res.status(400).json({ + success: false, + message: "You cannot review your own product", + }); + } + + // Insert the review into the database + const [result] = await db.execute( + `INSERT INTO Review ( + ProductID, + UserID, + Rating, + Comment, + Date + ) VALUES (?, ?, ?, ?, NOW())`, + [productId, userId, rating, comment], + ); + + // Get the inserted review id + const reviewId = result.insertId; + + // Fetch the newly created review to return to client + const [newReview] = await db.execute( + `SELECT + R.ReviewID, + R.ProductID, + R.UserID, + R.Rating, + R.Comment, + R.Date AS ReviewDate, + U.Name AS ReviewerName, + P.Name AS ProductName + FROM Review R + JOIN User U ON R.UserID = U.UserID + JOIN Product P ON R.ProductID = P.ProductID + WHERE R.ReviewID = ?`, + [reviewId], + ); + + res.status(201).json({ + success: true, // Fixed from false to true + message: "Review submitted successfully", + data: newReview[0], + }); + } catch (error) { + console.error("Error submitting review:", error); + return res.status(500).json({ + success: false, + message: "Database error occurred", + error: error.message, + }); + } +}; + +// /** +// * Update an existing review +// */ +// exports.updateReview = async (req, res) => { +// const { reviewId } = req.params; +// const { rating, comment } = req.body; +// const userId = req.body.userId; // Assuming you have middleware that validates the user + +// // Validate required fields +// if (!reviewId || !rating || !comment) { +// return res.status(400).json({ +// success: false, +// message: "Missing required fields", +// }); +// } + +// // Validate rating is between 1 and 5 +// if (rating < 1 || rating > 5) { +// return res.status(400).json({ +// success: false, +// message: "Rating must be between 1 and 5", +// }); +// } + +// try { +// // Check if review exists and belongs to the user +// const [existingReview] = await db.execute( +// `SELECT ReviewID, UserID FROM Review WHERE ReviewID = ?`, +// [reviewId], +// ); + +// if (existingReview.length === 0) { +// return res.status(404).json({ +// success: false, +// message: "Review not found", +// }); +// } + +// if (existingReview[0].UserID !== userId) { +// return res.status(403).json({ +// success: false, +// message: "You can only update your own reviews", +// }); +// } + +// // Update the review +// await db.execute( +// `UPDATE Review +// SET Rating = ?, Comment = ?, Date = NOW() +// WHERE ReviewID = ?`, +// [rating, comment, reviewId], +// ); + +// // Fetch the updated review +// const [updatedReview] = await db.execute( +// `SELECT +// R.ReviewID, +// R.ProductID, +// R.UserID, +// R.Rating, +// R.Comment, +// R.Date AS ReviewDate, +// U.Name AS ReviewerName, +// P.Name AS ProductName +// FROM Review R +// JOIN User U ON R.UserID = U.UserID +// JOIN Product P ON R.ProductID = P.ProductID +// WHERE R.ReviewID = ?`, +// [reviewId], +// ); + +// res.json({ +// success: true, +// message: "Review updated successfully", +// data: updatedReview[0], +// }); +// } catch (error) { +// console.error("Error updating review:", error); +// return res.status(500).json({ +// success: false, +// message: "Database error occurred", +// error: error.message, +// }); +// } +// }; + +// /** +// * Delete a review +// */ +// exports.deleteReview = async (req, res) => { +// const { reviewId } = req.params; +// const userId = req.body.userId; // Assuming you have middleware that validates the user + +// try { +// // Check if review exists and belongs to the user +// const [existingReview] = await db.execute( +// `SELECT ReviewID, UserID FROM Review WHERE ReviewID = ?`, +// [reviewId], +// ); + +// if (existingReview.length === 0) { +// return res.status(404).json({ +// success: false, +// message: "Review not found", +// }); +// } + +// if (existingReview[0].UserID !== userId) { +// return res.status(403).json({ +// success: false, +// message: "You can only delete your own reviews", +// }); +// } + +// // Delete the review +// await db.execute(`DELETE FROM Review WHERE ReviewID = ?`, [reviewId]); + +// res.json({ +// success: true, +// message: "Review deleted successfully", +// }); +// } catch (error) { +// console.error("Error deleting review:", error); +// return res.status(500).json({ +// success: false, +// message: "Database error occurred", +// error: error.message, +// }); +// } +// }; diff --git a/backend/controllers/search.js b/backend/controllers/search.js new file mode 100644 index 0000000..ba6700c --- /dev/null +++ b/backend/controllers/search.js @@ -0,0 +1,164 @@ +const db = require("../utils/database"); + +exports.searchProductsByName = async (req, res) => { + const { name } = req.query; + + if (name.length === 0) { + console.log("Searching for products with no name", name); + } + + console.log("Searching for products with name:", name); + + try { + // Modify SQL to return all products when no search term is provided + const sql = ` + SELECT p.*, i.URL as image + FROM Product p + LEFT JOIN Image_URL i ON p.ProductID = i.ProductID + ${name ? "WHERE p.Name LIKE ?" : ""} + ORDER BY p.ProductID + `; + + const params = name ? [`%${name}%`] : []; + console.log("Executing SQL:", sql); + console.log("With parameters:", params); + + const [data] = await db.execute(sql, params); + + console.log("Raw Database Result:", data); + + if (data.length === 0) { + console.log("No products found matching:", name); + return res.status(404).json({ + success: false, + message: "No products found matching your search", + }); + } + + // Group products by ProductID to handle multiple images per product + const productsMap = new Map(); + + data.forEach((row) => { + if (!productsMap.has(row.ProductID)) { + const product = { + ProductID: row.ProductID, + Name: row.Name, + Description: row.Description, + Price: row.Price, + images: row.image, + }; + productsMap.set(row.ProductID, product); + } else if (row.image_url) { + productsMap.get(row.ProductID).images.push(row.image_url); + } + }); + + const products = Array.from(productsMap.values()); + + console.log("Processed Products:", products); + + res.json({ + success: true, + message: "Products fetched successfully", + data: products, + count: products.length, + }); + } catch (error) { + console.error("Database Error:", error); + return res.status(500).json({ + success: false, + message: "Database error occurred", + error: error.message || "Unknown database error", + }); + } +}; + +// exports.searchProductsByName = async (req, res) => { +// const { name } = req.query; + +// // Add better validation and error handling +// if (!name || typeof name !== "string") { +// return res.status(400).json({ +// success: false, +// message: "Valid search term is required", +// }); +// } + +// console.log("Searching for products with name:", name); + +// try { +// // Log the SQL query and parameters for debugging +// const sql = ` +// SELECT p.*, i.URL AS image_url +// FROM Product p +// LEFT JOIN Image_URL i ON p.ProductID = i.ProductID +// WHERE p.Name LIKE ? +// `; +// const params = [`%${name}%`]; +// console.log("Executing SQL:", sql); +// console.log("With parameters:", params); + +// const [data] = await db.execute(sql, params); + +// // Log raw data for debugging +// console.log("Raw Database Result:", data); + +// if (data.length === 0) { +// console.log("No products found matching:", name); +// return res.status(404).json({ +// success: false, +// message: "No products found matching your search", +// }); +// } + +// // Group products by ProductID to handle multiple images per product +// const productsMap = new Map(); + +// data.forEach((row) => { +// if (!productsMap.has(row.ProductID)) { +// // Create a clean object without circular references +// const product = { +// ProductID: row.ProductID, +// Name: row.Name, +// Description: row.Description, +// Price: row.Price, +// // Add any other product fields you need +// images: row.image_url ? [row.image_url] : [], +// }; +// productsMap.set(row.ProductID, product); +// } else if (row.image_url) { +// // Add additional image to existing product +// productsMap.get(row.ProductID).images.push(row.image_url); +// } +// }); + +// // Convert map to array of products +// const products = Array.from(productsMap.values()); + +// // Log processed products for debugging +// console.log("Processed Products:", products); + +// res.json({ +// success: true, +// message: "Products fetched successfully", +// data: products, +// count: products.length, +// }); +// } catch (error) { +// // Enhanced error logging +// console.error("Database Error Details:", { +// message: error.message, +// code: error.code, +// errno: error.errno, +// sqlState: error.sqlState, +// sqlMessage: error.sqlMessage, +// sql: error.sql, +// }); + +// return res.status(500).json({ +// success: false, +// message: "Database error occurred", +// error: error.message || "Unknown database error", +// }); +// } +// }; diff --git a/backend/controllers/user.js b/backend/controllers/user.js new file mode 100644 index 0000000..bcfc46a --- /dev/null +++ b/backend/controllers/user.js @@ -0,0 +1,308 @@ +const crypto = require("crypto"); +const db = require("../utils/database"); +const { sendVerificationEmail } = require("../utils/helper"); + +exports.sendVerificationCode = async (req, res) => { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ error: "Email is required" }); + } + + try { + // Generate a random 6-digit code + const verificationCode = crypto.randomInt(100000, 999999).toString(); + console.log( + `Generated verification code for ${email}: ${verificationCode}` + ); + + // Check if email already exists in verification table + const [results, fields] = await db.execute( + "SELECT * FROM AuthVerification WHERE Email = ?", + [email] + ); + + if (results.length > 0) { + // Update existing record + const [result] = await db.execute( + `UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP + WHERE Email = ?`, + [verificationCode, email] + ); + + // Send email and respond + await sendVerificationEmail(email, verificationCode); + res.json({ success: true, message: "Verification code sent" }); + } else { + // Insert new record + const [result] = await db.execute( + "INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)", + [email, verificationCode] + ); + // Send email and respond + await sendVerificationEmail(email, verificationCode); + res.json({ success: true, message: "Verification code sent" }); + } + } catch (error) { + console.error("Error:", error); + res.status(500).json({ error: "Server error" }); + } +}; + +exports.verifyCode = async (req, res) => { + const { email, code } = req.body; + + if (!email || !code) { + return res.status(400).json({ error: "Email and code are required" }); + } + + console.log(`Attempting to verify code for ${email}: ${code}`); + + try { + // Check verification code + const [results, fields] = await db.execute( + "SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)", + [email, code] + ); + if (results.length === 0) { + console.log(`Invalid or expired verification code for email ${email}`); + return res + .status(400) + .json({ error: "Invalid or expired verification code" }); + } + + const userId = results[0].UserID; + + // Mark as authenticated + const [result] = await db.execute( + "UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?", + [email] + ); + res.json({ + success: true, + message: "Verification successful", + userId, + }); + } catch (error) { + console.log("Error: ", error); + res.status(500).json({ error: "Database error!" }); + } +}; + +exports.completeSignUp = async (req, res) => { + const data = req.body; + + try { + const [results, fields] = await db.execute( + `SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`, + [data.email] + ); + + if (results.length === 0) { + return res.status(400).json({ error: "Email not verified" }); + } + + // Create the user + const [createResult] = await db.execute( + `INSERT INTO User (Name, Email, UCID, Password, Phone, Address) + VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')` + ); + + // Insert role using the user's ID + const [insertResult] = await db.execute( + `INSERT INTO UserRole (UserID, Client, Admin) + VALUES (LAST_INSERT_ID(), ${data.client || true}, ${ + data.admin || false + })` + ); + + // Delete verification record + const [deleteResult] = await db.execute( + `DELETE FROM AuthVerification WHERE Email = '${data.email}'` + ); + + res.json({ + success: true, + message: "User registration completed successfully", + name: data.name, + email: data.email, + UCID: data.UCID, + }); + } catch (error) { + console.log("Error: ", error); + res.status(500).json({ error: "Database error!" }); + } +}; + +exports.getAllUser = async (req, res) => { + try { + const [users, fields] = await db.execute("SELECT * FROM User;"); + res.json({ users }); + } catch (error) { + console.error("Errors: ", error); + return res.status(500).json({ error: "\nCould not fetch users!" }); + } +}; + +exports.findUserByEmail = async (req, res) => { + const { email } = req.body; + + // Input validation + if (!email) { + return res.status(400).json({ + found: false, + error: "Email is required", + }); + } + + try { + // Query to find user with matching email and password + const query = "SELECT * FROM User WHERE email = ?"; + const [data, fields] = await db.execute(query, [email]); + + // Check if user was found + if (data && data.length > 0) { + console.log(data); + const user = data[0]; + + // Return all user data except password + return res.json({ + found: true, + userID: user.UserID, + name: user.Name, + email: user.Email, + UCID: user.UCID, + phone: user.Phone, + address: user.Address, + // Include any other fields your user might have + // Make sure the field names match exactly with your database column names + }); + } else { + // User not found or invalid credentials + return res.json({ + found: false, + error: "Invalid email or password", + }); + } + } catch (error) { + console.error("Error finding user:", error); + return res.status(500).json({ + found: false, + error: "Database error occurred", + }); + } +}; + +exports.updateUser = async (req, res) => { + try { + const userId = req.body?.userId; + const name = req.body?.name; + const email = req.body?.email; + const phone = req.body?.phone; + const UCID = req.body?.UCID; + const address = req.body?.address; + + if (!userId) { + return res.status(400).json({ error: "User ID is required" }); + } + + // Build updateData manually + const updateData = {}; + if (name) updateData.name = name; + if (email) updateData.email = email; + if (phone) updateData.phone = phone; + if (UCID) updateData.UCID = UCID; + if (address) updateData.address = address; + + if (Object.keys(updateData).length === 0) { + return res.status(400).json({ error: "No valid fields to update" }); + } + + const updateFields = []; + const values = []; + + Object.entries(updateData).forEach(([key, value]) => { + updateFields.push(`${key} = ?`); + values.push(value); + }); + + values.push(userId); + + const query = `UPDATE User SET ${updateFields.join(", ")} WHERE userId = ?`; + const [updateResult] = await db.execute(query, values); + + if (updateResult.affectedRows === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ success: true, message: "User updated successfully" }); + } catch (error) { + console.error("Error updating user:", error); + return res.status(500).json({ error: "Could not update user" }); + } +}; + +exports.deleteUser = async (req, res) => { + const { userId } = req.body; + + if (!userId) { + return res.status(400).json({ error: "User ID is required" }); + } + + try { + // Delete from UserRole first (assuming foreign key constraint) + const [result1] = await db.execute( + "DELETE FROM UserRole WHERE UserID = ?", + [userId] + ); + + // Then delete from User table + const [result2] = await db.execute("DELETE FROM User WHERE UserID = ?", [ + userId, + ]); + + if (result2.affectedRows === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ success: true, message: "User deleted successfully" }); + } catch (error) { + console.error("Error: ", error); + return res.status(500).json({ error: "Could not delete user!" }); + } +}; + +exports.getUsersWithPagination = async (req, res) => { + const limit = +req.query.limit; + const page = +req.query.page; + + const offset = (page - 1) * limit; + try { + const [users, fields] = await db.execute( + "SELECT * FROM User LIMIT ? OFFSET ?", + [limit.toString(), offset.toString()] + ); + + const [result] = await db.execute("SELECT COUNT(*) AS count FROM User"); + const { count: total } = result[0]; + + res.json({ users, total }); + } catch (error) { + console.error("Errors: ", error); + return res.status(500).json({ error: "\nCould not fetch users!" }); + } +}; + +exports.isAdmin = async (req, res) => { + const { id } = req.params; + try { + const [result] = await db.execute( + "SELECT R.Admin FROM marketplace.userrole R WHERE R.UserID = ?", + [id] + ); + const { Admin } = result[0]; + res.json({ isAdmin: Admin }); + } catch (error) { + res.json({ error: "Cannot verify admin status!" }); + } +}; diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..937a10e --- /dev/null +++ b/backend/index.js @@ -0,0 +1,56 @@ +const express = require("express"); +const cors = require("cors"); + +const db = require("./utils/database"); + +const userRouter = require("./routes/user"); +const productRouter = require("./routes/product"); +const searchRouter = require("./routes/search"); +const recommendedRouter = require("./routes/recommendation"); +const history = require("./routes/history"); +const review = require("./routes/review"); +const categoryRouter = require("./routes/category"); + +const { generateEmailTransporter } = require("./utils/mail"); +const { + // cleanupExpiredCodes, + checkDatabaseConnection, +} = require("./utils/helper"); + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +// Configure email transporter for Zoho +const transporter = generateEmailTransporter(); +// Test the email connection +transporter + .verify() + .then(() => { + console.log("Email connection successful!"); + }) + .catch((error) => { + console.error("Email connection failed:", error); + }); + +checkDatabaseConnection(db); + +//Routes +app.use("/api/user", userRouter); +app.use("/api/product", productRouter); +app.use("/api/search", searchRouter); +app.use("/api/engine", recommendedRouter); +app.use("/api/history", history); +app.use("/api/review", review); +app.use("/api/category", categoryRouter); + +// Set up a scheduler to run cleanup every hour +// clean_up_time = 30*60*1000; +// setInterval(cleanupExpiredCodes, clean_up_time); + +app.listen(3030, () => { + console.log(`Running Backend on http://localhost:3030/`); + console.log(`Send verification code: POST /send-verification`); + console.log(`Verify code: POST /verify-code`); +}); diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..9f44399 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1592 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "mysql": "^2.18.1", + "mysql2": "^3.12.0", + "nodemailer": "^6.10.0" + }, + "devDependencies": { + "nodemon": "^3.1.9" + } + }, + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "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/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "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": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "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", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "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", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.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", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/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/minimatch": { + "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" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "license": "MIT", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/mysql/node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql2": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", + "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "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/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..2eb28df --- /dev/null +++ b/backend/package.json @@ -0,0 +1,28 @@ +{ + "name": "server", + "version": "1.0.0", + "main": "index.js", + "type": "commonjs", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "nodemon index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "mysql": "^2.18.1", + "mysql2": "^3.12.0", + "nodemailer": "^6.10.0" + }, + "devDependencies": { + "nodemon": "^3.1.9" + } +} diff --git a/backend/routes/category.js b/backend/routes/category.js new file mode 100644 index 0000000..9a04a08 --- /dev/null +++ b/backend/routes/category.js @@ -0,0 +1,14 @@ +const express = require("express"); +const { + getAllCategoriesWithPagination, + addCategory, + removeCategory, +} = require("../controllers/category"); + +const router = express.Router(); + +router.get("/getCategories", getAllCategoriesWithPagination); +router.post("/addCategory", addCategory); +router.delete("/:id", removeCategory); + +module.exports = router; diff --git a/backend/routes/history.js b/backend/routes/history.js new file mode 100644 index 0000000..6713233 --- /dev/null +++ b/backend/routes/history.js @@ -0,0 +1,14 @@ +// routes/product.js +const express = require("express"); +const { + HistoryByUserId, + DelHistory, + AddHistory, +} = require("../controllers/history"); +const router = express.Router(); + +router.post("/getHistory", HistoryByUserId); +router.post("/delHistory", DelHistory); +router.post("/addHistory", AddHistory); + +module.exports = router; diff --git a/backend/routes/product.js b/backend/routes/product.js new file mode 100644 index 0000000..7c61fe4 --- /dev/null +++ b/backend/routes/product.js @@ -0,0 +1,34 @@ +// routes/product.js +const express = require("express"); +const { + addFavorite, + getFavorites, + removeFavorite, + getAllProducts, + getProductById, + getProductWithPagination, + removeProduct, +} = require("../controllers/product"); +const router = express.Router(); + +// Add detailed logging middleware +router.use((req, res, next) => { + console.log(`Incoming ${req.method} request to ${req.path}`); + next(); +}); + +router.post("/addFavorite", addFavorite); +router.post("/getFavorites", getFavorites); +router.post("/delFavorite", removeFavorite); + +router.get("/getProduct", getAllProducts); + +//Get products with pagination +router.get("/getProductWithPagination", getProductWithPagination); + +router.get("/:id", getProductById); // Simplified route + +//Remove product +router.delete("/:id", removeProduct); + +module.exports = router; diff --git a/backend/routes/recommendation.js b/backend/routes/recommendation.js new file mode 100644 index 0000000..e252a34 --- /dev/null +++ b/backend/routes/recommendation.js @@ -0,0 +1,8 @@ +// routes/product.js +const express = require("express"); +const { RecommondationByUserId } = require("../controllers/recommendation"); +const router = express.Router(); + +router.post("/recommended", RecommondationByUserId); + +module.exports = router; diff --git a/backend/routes/review.js b/backend/routes/review.js new file mode 100644 index 0000000..5b26a87 --- /dev/null +++ b/backend/routes/review.js @@ -0,0 +1,9 @@ +// routes/product.js +const express = require("express"); +const { getReviews, submitReview } = require("../controllers/review"); +const router = express.Router(); + +router.get("/:id", getReviews); +router.post("/add", submitReview); + +module.exports = router; diff --git a/backend/routes/search.js b/backend/routes/search.js new file mode 100644 index 0000000..d59f785 --- /dev/null +++ b/backend/routes/search.js @@ -0,0 +1,14 @@ +// routes/product.js +const express = require("express"); +const { searchProductsByName } = require("../controllers/search"); +const router = express.Router(); + +// Add detailed logging middleware +router.use((req, res, next) => { + console.log(`Incoming ${req.method} request to ${req.path}`); + next(); +}); + +router.get("/getProduct", searchProductsByName); + +module.exports = router; diff --git a/backend/routes/user.js b/backend/routes/user.js new file mode 100644 index 0000000..f5818ec --- /dev/null +++ b/backend/routes/user.js @@ -0,0 +1,42 @@ +const express = require("express"); +const { + sendVerificationCode, + verifyCode, + completeSignUp, + getAllUser, + findUserByEmail, + updateUser, + deleteUser, + getUsersWithPagination, + isAdmin, +} = require("../controllers/user"); + +const router = express.Router(); + +// Generate and send verification code for signup +router.post("/send-verification", sendVerificationCode); + +// Verify the code +router.post("/verify-code", verifyCode); + +// Create a users and Complete user registration after verification +router.post("/complete-signup", completeSignUp); + +//Fetch all users data: +router.get("/fetch_all_users", getAllUser); + +//Fetch user with pagination +router.get("/getUserWithPagination", getUsersWithPagination); + +//Fetch One user Data with all fields: +router.post("/find_user", findUserByEmail); + +//Update A uses Data: +router.post("/update", updateUser); + +//Delete A uses Data: +router.post("/delete", deleteUser); + +router.get("/isAdmin/:id", isAdmin); + +module.exports = router; diff --git a/backend/utils/database.js b/backend/utils/database.js new file mode 100644 index 0000000..f31f76f --- /dev/null +++ b/backend/utils/database.js @@ -0,0 +1,12 @@ +const mysql = require("mysql2"); + +//Create a pool of connections to allow multiple query happen at the same time +const pool = mysql.createPool({ + host: "localhost", + user: "root", + database: "Marketplace", + password: "12345678", +}); + +//Export a promise for promise-based query +module.exports = pool.promise(); diff --git a/backend/utils/helper.js b/backend/utils/helper.js new file mode 100644 index 0000000..9d7513b --- /dev/null +++ b/backend/utils/helper.js @@ -0,0 +1,49 @@ +const { generateEmailTransporter } = require("./mail"); +const db = require("./database"); + +// Helper function to send email +async function sendVerificationEmail(email, verificationCode) { + const transporter = generateEmailTransporter(); + + // Send the email with Zoho + await transporter.sendMail({ + from: "campusplug@zohomailcloud.ca", + to: email, + subject: "Campus Plug: Signup Verification Code", + text: `Your verification code is: ${verificationCode}. This code will expire in 15 minutes.`, + html: `

Your verification code is: ${verificationCode}

This code will expire in 15 minutes.

`, + }); + + console.log(`Verification code sent to ${email}`); +} + +// Clean up expired verification codes (run this periodically) +// function cleanupExpiredCodes() { +// db.query( +// "DELETE FROM AuthVerification WHERE Date < DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND Authenticated = 0", +// (err, result) => { +// if (err) { +// console.error("Error cleaning up expired codes:", err); +// } else { +// console.log(`Cleaned up ${result} expired verification codes`); +// } +// } +// ); +// } + +const checkDatabaseConnection = async (db) => { + try { + const connection = await db.getConnection(); + //If no error, release the connection + console.log("Connect to database successfully!"); + connection.release(); + } catch (error) { + console.log("Cannot connect to database: ", error.sqlMessage); + } +}; + +module.exports = { + sendVerificationEmail, + // cleanupExpiredCodes, + checkDatabaseConnection, +}; diff --git a/backend/utils/mail.js b/backend/utils/mail.js new file mode 100644 index 0000000..6810672 --- /dev/null +++ b/backend/utils/mail.js @@ -0,0 +1,12 @@ +const nodemailer = require("nodemailer"); + +exports.generateEmailTransporter = () => + nodemailer.createTransport({ + host: "smtp.zohocloud.ca", + secure: true, + port: 465, + auth: { + user: "campusplug@zohomailcloud.ca", //Zoho email + pass: "e0YRrNSeJZQd", //Zoho password + }, + }); diff --git a/frontend b/frontend new file mode 160000 index 0000000..99cc24f --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 99cc24f2299732f52dcaedfc412e5be338e5385c diff --git a/mysql-code/Init-Data.sql b/mysql-code/Init-Data.sql new file mode 100644 index 0000000..0d2f8e7 --- /dev/null +++ b/mysql-code/Init-Data.sql @@ -0,0 +1,456 @@ +-- Inserting sample data into the Marketplace database +SET + FOREIGN_KEY_CHECKS = 0; + +TRUNCATE TABLE Product_Category; + +TRUNCATE TABLE Favorites; + +TRUNCATE TABLE History; + +TRUNCATE TABLE Recommendation; + +TRUNCATE TABLE Transaction; + +TRUNCATE TABLE Review; + +TRUNCATE TABLE Image_URL; + +TRUNCATE TABLE Product; + +TRUNCATE TABLE Category; + +TRUNCATE TABLE UserRole; + +TRUNCATE TABLE User; + +TRUNCATE TABLE AuthVerification; + +SET + FOREIGN_KEY_CHECKS = 1; + +-- Insert Users +INSERT INTO + User ( + UserID, + Name, + Email, + UCID, + Password, + Phone, + Address + ) +VALUES + ( + 1, + 'John Doe', + 'john.doe@example.com', + 'U123456', + 'hashedpassword1', + '555-123-4567', + '123 Main St, Calgary, AB' + ), + ( + 2, + 'Jane Smith', + 'jane.smith@example.com', + 'U234567', + 'hashedpassword2', + '555-234-5678', + '456 Oak Ave, Calgary, AB' + ); + +-- Insert User Roles +INSERT INTO + UserRole (UserID, Client, Admin) +VALUES + (1, TRUE, TRUE), + (2, TRUE, FALSE); + +-- Insert Categories +INSERT INTO + Category (Name) +VALUES + ('Textbooks'), + ('Electronics'), + ('Furniture'), + ('Clothing'), + ('Sports Equipment'), + ('Musical Instruments'), + ('Art Supplies'), + ('Kitchen Appliances'), + ('Gaming'), + ( 'Bicycles'), + ( 'Computer Accessories'), + ( 'Stationery'), + ( 'Fitness Equipment'), + ( 'Winter Sports'), + ( 'Lab Equipment'), + ( 'Camping Gear'), + ( 'School Supplies'), + ( 'Office Furniture'), + ( 'Books (-textbook)'), + ( 'Math & Science Resources'), + ( 'Engineering Tools'), + ( 'Backpacks & Bags'), + ( 'Audio Equipment'), + ( 'Dorm Essentials'), + ( 'Smartphones & Tablets'), + ( 'Winter Clothing'), + ( 'Photography Equipment'), + ( 'Event Tickets'), + ( 'Software Licenses'), + ( 'Transportation (Car Pool)'); + +-- Insert Products +INSERT INTO + Product ( + ProductID, + Name, + Price, + StockQuantity, + UserID, + Description, + CategoryID, + Date + ) +VALUES + ( + 1, + 'Calculus Textbook 8th Edition', + 79.99, + 5, + 1, + 'Like new calculus textbook, minor highlighting', + 1, + '2024-10-15 10:00:00' + ), + ( + 2, + 'HP Laptop', + 699.99, + 1, + 1, + '2023 HP Pavilion, 16GB RAM, 512GB SSD', + 2, + '2024-10-10 14:30:00' + ), + ( + 3, + 'Dorm Desk', + 120.00, + 1, + 2, + 'Sturdy desk perfect for studying, minor scratches', + 3, + '2024-10-12 09:15:00' + ), + ( + 4, + 'University Hoodie', + 35.00, + 3, + 2, + 'Size L, university logo, worn twice', + 4, + '2024-10-14 16:45:00' + ), + ( + 5, + 'Basketball', + 25.50, + 1, + 2, + 'Slightly used indoor basketball', + 5, + '2024-10-11 11:20:00' + ), + ( + 6, + 'Acoustic Guitar', + 175.00, + 1, + 1, + 'Beginner acoustic guitar with case', + 6, + '2024-10-09 13:10:00' + ), + ( + 7, + 'Physics Textbook', + 65.00, + 2, + 2, + 'University Physics 14th Edition, good condition', + 1, + '2024-10-08 10:30:00' + ), + ( + 8, + 'Mini Fridge', + 85.00, + 1, + 1, + 'Small dorm fridge, works perfectly', + 8, + '2024-10-13 15:00:00' + ), + ( + 9, + 'PlayStation 5 Controller', + 55.00, + 1, + 2, + 'Extra controller, barely used', + 9, + '2024-10-07 17:20:00' + ), + ( + 10, + 'Mountain Bike', + 350.00, + 1, + 1, + 'Trek mountain bike, great condition, new tires', + 10, + '2024-10-06 14:00:00' + ), + ( + 11, + 'Wireless Mouse', + 22.99, + 3, + 1, + 'Logitech wireless mouse with battery', + 11, + '2024-10-05 09:30:00' + ), + ( + 12, + 'Chemistry Lab Coat', + 30.00, + 2, + 2, + 'Size M, required for chem labs', + 15, + '2024-10-04 13:45:00' + ), + ( + 13, + 'Graphing Calculator', + 75.00, + 1, + 1, + 'TI-84 Plus, perfect working condition', + 12, + '2024-10-03 11:15:00' + ), + ( + 14, + 'Yoga Mat', + 20.00, + 1, + 2, + 'Thick yoga mat, barely used', + 13, + '2024-10-02 16:00:00' + ), + ( + 15, + 'Winter Jacket', + 120.00, + 1, + 1, + 'Columbia winter jacket, size XL, very warm', + 26, + '2024-10-01 10:20:00' + ), + ( + 16, + 'Computer Science Textbook', + 70.00, + 1, + 2, + 'Introduction to Algorithms, like new', + 1, + '2024-09-30 14:30:00' + ), + ( + 17, + 'Desk Lamp', + 15.00, + 2, + 2, + 'LED desk lamp with adjustable brightness', + 24, + '2024-09-29 12:00:00' + ), + ( + 18, + 'Scientific Calculator', + 25.00, + 1, + 1, + 'Casio scientific calculator', + 12, + '2024-09-28 11:30:00' + ), + ( + 19, + 'Bluetooth Speaker', + 45.00, + 1, + 1, + 'JBL Bluetooth speaker, great sound', + 23, + '2024-09-27 15:45:00' + ), + ( + 20, + 'Backpack', + 40.00, + 1, + 2, + 'North Face backpack, lots of pockets', + 22, + '2024-09-26 09:15:00' + ); + +INSERT INTO + Image_URL (URL, ProductID) +VALUES + ('/Pictures/Dell1.jpg', 1), + ('/Pictures/Dell2.jpg', 1), + ('/Pictures/Dell3.jpg', 1), + ('/Pictures/HP-Laptop1.jpg', 2), + ('/Pictures/HP-Laptop1.jpg', 2), + ('/Pictures/Dorm-Desk.jpg', 3), + ('/Pictures/University-Hoodie.jpg', 4), + ('/Pictures/Basketball.jpg', 5), + ('/Pictures/Acoustic-Guitar.jpg', 6), + ('/Pictures/Physics-Textbook.jpg', 7), + ('/Pictures/Mini-Fridge.jpg', 8), + ('/Pictures/Controller.jpg', 9), + ('/Pictures/Mountain-Bike.jpg', 10), + ('/Pictures/Wireless-Mouse.jpg', 11), + ('/Pictures/Lab-Coat.jpg', 12), + ('/Pictures/Calculator.jpg', 13), + ('/Pictures/Yoga-Mat.jpg', 14), + ('/Pictures/Winter-Jacket.jpg', 15), + ('/Pictures/CS-Textbook.jpg', 16), + ('/Pictures/Desk-Lamp.jpg', 17), + ('/Pictures/HP-Calculator.jpg', 18), + ('/Pictures/Bluetooth-Speaker.jpg', 19), + ('/Pictures/Backpack.jpg', 20); + +-- Insert Product-Category relationships (products with multiple categories) +INSERT INTO + Product_Category (ProductID, CategoryID) +VALUES + (1, 1), + (1, 17), + (1, 20), -- Calculus book: Textbooks, School Supplies, Math Resources + (2, 2), + (2, 11), + (2, 25), -- Laptop: Electronics, Computer Accessories, Smartphones & Tablets + (3, 3), + (3, 18), + (3, 24), -- Desk: Furniture, Office Furniture, Dorm Essentials + (4, 4), + (4, 26), -- Hoodie: Clothing, Winter Clothing + (5, 5), + (5, 13), -- Basketball: Sports Equipment, Fitness Equipment + (6, 6), + (6, 23), -- Guitar: Musical Instruments, Audio Equipment + (7, 1), + (7, 15), + (7, 20), -- Physics book: Textbooks, Lab Equipment, Math & Science Resources + (8, 8), + (8, 24), -- Mini Fridge: Kitchen Appliances, Dorm Essentials + (9, 9), + (9, 2), -- PS5 Controller: Gaming, Electronics + (10, 10), + (10, 5), + (10, 13), -- Mountain Bike: Bicycles, Sports Equipment, Fitness Equipment + (11, 11), + (11, 2), -- Mouse: Computer Accessories, Electronics + (12, 15), + (12, 17), -- Lab Coat: Lab Equipment, School Supplies + (13, 12), + (13, 17), + (13, 20), -- Calculator: Stationery, School Supplies, Math & Science Resources + (14, 13), + (14, 5), -- Yoga Mat: Fitness Equipment, Sports Equipment + (15, 26), + (15, 4), + (15, 14), -- Winter Jacket: Winter Clothing, Clothing, Winter Sports + (16, 1), + (16, 17), + (16, 19), -- CS Book: Textbooks, School Supplies, Books (Non-textbook) + (17, 24), + (17, 2), -- Desk Lamp: Dorm Essentials, Electronics + (18, 12), + (18, 17), + (18, 20), -- Scientific Calculator: Stationery, School Supplies, Math & Science + (19, 23), + (19, 2), + (19, 24), -- Bluetooth Speaker: Audio Equipment, Electronics, Dorm Essentials + (20, 22), + (20, 17), + (20, 24); + +-- Insert History records +INSERT INTO + History (HistoryID, UserID, ProductID) +VALUES + (1, 1, 1), + (2, 1, 3), + (3, 1, 5), + (4, 1, 7), + (5, 1, 9), + (6, 1, 11), + (7, 2, 2), + (8, 2, 4), + (9, 2, 5), + (10, 1, 15), + (11, 1, 18); + +-- Insert Favorites +INSERT INTO + Favorites (UserID, ProductID) +VALUES + (1, 2), -- User 1 likes HP Laptop + (1, 7), -- User 1 likes Physics Textbook + (2, 3), -- User 2 likes Dorm Desk + (2, 10), -- User 2 likes Mountain Bike + (1, 6), -- User 3 likes Acoustic Guitar + (1, 5), -- User 4 likes Basketball + (2, 8); + +-- User 5 likes Mini Fridge +-- Insert Transactions +INSERT INTO + Transaction ( + TransactionID, + UserID, + ProductID, + Date, + PaymentStatus + ) +VALUES + (1, 1, 1, '2024-10-16 10:30:00', 'Completed'), + (2, 1, 6, '2024-10-15 15:45:00', 'Completed'), + (3, 1, 8, '2024-10-14 12:20:00', 'Pending'), + (4, 2, 10, '2024-10-13 17:10:00', 'Completed'), + (5, 2, 4, '2024-10-12 14:30:00', 'Completed'); + +INSERT INTO + Review (UserID, ProductID, Comment, Rating, Date) +VALUES + ( + 1, + 1, + 'This is a great fake product! Totally recommend it.', + 5, + NOW () + ); diff --git a/mysql-code/Schema.sql b/mysql-code/Schema.sql new file mode 100644 index 0000000..a18261b --- /dev/null +++ b/mysql-code/Schema.sql @@ -0,0 +1,125 @@ +-- MySql Version 9.2.0 +CREATE DATABASE Marketplace; + +USE Marketplace; + +-- User Entity +CREATE TABLE User ( + UserID INT AUTO_INCREMENT PRIMARY KEY, + Name VARCHAR(100) NOT NULL, + Email VARCHAR(100) UNIQUE NOT NULL, + UCID VARCHAR(20) UNIQUE NOT NULL, + Password VARCHAR(255) NOT NULL, + Phone VARCHAR(20), + Address VARCHAR(255) +); + +CREATE TABLE UserRole ( + UserID INT, + Client BOOLEAN DEFAULT True, + Admin BOOLEAN DEFAULT FALSE, + PRIMARY KEY (UserID), + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE +); + +-- Category Entity (must be created before Product or else error) +CREATE TABLE Category ( + CategoryID INT AUTO_INCREMENT PRIMARY KEY, + Name VARCHAR(255) NOT NULL +); + +-- Product Entity +CREATE TABLE Product ( + ProductID INT PRIMARY KEY, + Name VARCHAR(255) NOT NULL, + Price DECIMAL(10, 2) NOT NULL, + StockQuantity INT, + UserID INT, + Description TEXT, + CategoryID INT, + Date DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, + FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE SET NULL +); + +-- Fixed Image_URL table +CREATE TABLE Image_URL ( + URL VARCHAR(255), + ProductID INT, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE +); + +-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product) +CREATE TABLE Review ( + ReviewID INT AUTO_INCREMENT PRIMARY KEY, + UserID INT, + ProductID INT, + Comment TEXT, + Rating INT CHECK ( + Rating >= 1 + AND Rating <= 5 + ), + Date DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE +); + +-- Transaction Entity (Many-to-One with User, Many-to-One with Product) +CREATE TABLE Transaction ( + TransactionID INT PRIMARY KEY, + UserID INT, + ProductID INT, + Date DATETIME DEFAULT CURRENT_TIMESTAMP, + PaymentStatus VARCHAR(50), + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL +); + +-- Recommendation Entity (Many-to-One with User, Many-to-One with Product) +CREATE TABLE Recommendation ( + RecommendationID_PK INT AUTO_INCREMENT PRIMARY KEY, + UserID INT, + RecommendedProductID INT, + Date DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE +); + +-- History Entity (Many-to-One with User, Many-to-One with Product) +CREATE TABLE History ( + HistoryID INT AUTO_INCREMENT PRIMARY KEY, + UserID INT, + ProductID INT, + Date DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE +); + +-- Favorites Entity (Many-to-One with User, Many-to-One with Product) +CREATE TABLE Favorites ( + FavoriteID INT AUTO_INCREMENT PRIMARY KEY, + UserID INT, + ProductID INT, + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE, + UNIQUE (UserID, ProductID) + +); + +-- Product-Category Junction Table (Many-to-Many) +CREATE TABLE Product_Category ( + ProductID INT, + CategoryID INT, + PRIMARY KEY (ProductID, CategoryID), + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE, + FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE +); + +-- Login Authentication table +CREATE TABLE AuthVerification ( + UserID INT AUTO_INCREMENT PRIMARY KEY, + Email VARCHAR(100) UNIQUE NOT NULL, + VerificationCode VARCHAR(6) NOT NULL, + Authenticated BOOLEAN DEFAULT FALSE, + Date DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/recommondation-engine/app.py b/recommondation-engine/app.py new file mode 100644 index 0000000..39afd3d --- /dev/null +++ b/recommondation-engine/app.py @@ -0,0 +1,163 @@ +# pip install mysql.connector +# + + +import mysql.connector +from sklearn.metrics.pairwise import cosine_similarity +import numpy as np +import logging +from unittest import result + +def database(): + db_connection = mysql.connector.connect( + host = "localhost", + port = "3306", + user = "root", + database = "Marketplace" + ) + return db_connection + +def get_popular_products(): + pass + + +def delete_user_recommendation(userID, Array): + db_con = database() + cursor = db_con.cursor() + + try: + for item in Array: + #Product ID starts form index 1 + item_value = item + 1 + print(item_value) + # Use parameterized queries to prevent SQL injection + cursor.execute(f"INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});") + + db_con.commit() + + #results = cursor.fetchall() + #print(results) + except: + pass + +def get_all_products(): + + db_con = database() + cursor = db_con.cursor() + + cursor.execute("SELECT CategoryID FROM Category") + categories = cursor.fetchall() + + select_clause = "SELECT p.ProductID" + for category in categories: + category_id = category[0] + select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`" + + final_query = f""" + {select_clause} + FROM Product p + LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID + LEFT JOIN Category c ON pc.CategoryID = c.CategoryID + GROUP BY p.ProductID; + """ + + cursor.execute(final_query) + results = cursor.fetchall() + + final = [] + for row in results: + text_list = list(row) + text_list.pop(0) + final.append(text_list) + + cursor.close() + db_con.close() + return final + +def get_user_history(user_id): + db_con = database() + cursor = db_con.cursor() + + cursor.execute("SELECT CategoryID FROM Category") + categories = cursor.fetchall() + + select_clause = "SELECT p.ProductID" + for category in categories: + category_id = category[0] # get the uid of the catefory and then append that to the new column + select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`" + + final_query = f""" + {select_clause} + FROM Product p + LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID + LEFT JOIN Category c ON pc.CategoryID = c.CategoryID + where p.ProductID in (select ProductID from History where UserID = {user_id}) + GROUP BY p.ProductID; + """ + + cursor.execute(final_query) + results = cursor.fetchall() + final = [] + for row in results: + text_list = list(row) + text_list.pop(0) + final.append(text_list) + + cursor.close() + db_con.close() + return final + +def get_recommendations(user_id, top_n=10): + try: + # Get all products and user history with their category vectors + all_products = get_all_products() + user_history = get_user_history(user_id) + # if not user_history: + # #Cold start: return popular products + # return get_popular_products(top_n) + # Calculate similarity between all products and user history + user_profile = np.mean(user_history, axis=0) # Average user preferences + similarities = cosine_similarity([user_profile], all_products) + # finds the indices of the top N products that have the highest + # cosine similarity with the user's profile and sorted from most similar to least similar. + product_indices = similarities[0].argsort()[-top_n:][::-1] + print("product", product_indices) + + # Get the recommended product IDs + recommended_products = [all_products[i][0] for i in product_indices] # Product IDs + + # Upload the recommendations to the database + history_upload(user_id, product_indices) # Pass the indices directly to history_upload + + # Return recommended product IDs + return recommended_products + except Exception as e: + logging.error(f"Recommendation error for user {user_id}: {str(e)}") + # return get_popular_products(top_n) # Fallback to popular products + +def history_upload(userID, anrr): + db_con = database() + cursor = db_con.cursor() + + try: + for item in anrr: + #Product ID starts form index 1 + item_value = item + 1 + print(item_value) + # Use parameterized queries to prevent SQL injection + cursor.execute(f"INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});") + + # Commit the changes + db_con.commit() + + # If you need results, you'd typically fetch them after a SELECT query + #results = cursor.fetchall() + #print(results) + + except Exception as e: + print(f"Error: {e}") + db_con.rollback() + finally: + # Close the cursor and connection + cursor.close() + db_con.close() diff --git a/recommondation-engine/server.py b/recommondation-engine/server.py new file mode 100644 index 0000000..8960c03 --- /dev/null +++ b/recommondation-engine/server.py @@ -0,0 +1,34 @@ + + + + +from flask import Flask, request, jsonify +from flask_cors import CORS +from app import get_recommendations + + +app = Flask(__name__) +CORS(app) # Enable CORS for all routes + +@app.route('/api/user/session', methods=['POST']) +def handle_session_data(): + try: + data = request.get_json() + user_id = data.get('userId') + email = data.get('email') + is_authenticated = data.get('isAuthenticated') + + if not user_id or not email or is_authenticated is None: + return jsonify({'error': 'Invalid data'}), 400 + + get_recommendations(user_id) + + print(f"Received session data: User ID: {user_id}, Email: {email}, Authenticated: {is_authenticated}") + return jsonify({'message': 'Session data received successfully'}) + + except Exception as e: + print(f"Error: {e}") + return jsonify({'error': 'Server error'}), 500 + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=5000)