Finish admin dashboard and update sql code

This commit is contained in:
estherdev03
2025-04-20 05:56:31 -06:00
commit 68d2b950c0
28 changed files with 4144 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Node_modules
*/node_modules
.DS_Store
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

201
LICENSE Normal file
View File

@@ -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.

34
README.md Normal file
View File

@@ -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
```

View File

@@ -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!" });
}
};

View File

@@ -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" });
}
};

View File

@@ -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",
// });
// },
// );

View File

@@ -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",
});
}
};

View File

@@ -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,
// });
// }
// };

View File

@@ -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",
// });
// }
// };

308
backend/controllers/user.js Normal file
View File

@@ -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!" });
}
};

56
backend/index.js Normal file
View File

@@ -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`);
});

1592
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
backend/package.json Normal file
View File

@@ -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"
}
}

View File

@@ -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;

14
backend/routes/history.js Normal file
View File

@@ -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;

34
backend/routes/product.js Normal file
View File

@@ -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;

View File

@@ -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;

9
backend/routes/review.js Normal file
View File

@@ -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;

14
backend/routes/search.js Normal file
View File

@@ -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;

42
backend/routes/user.js Normal file
View File

@@ -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;

12
backend/utils/database.js Normal file
View File

@@ -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();

49
backend/utils/helper.js Normal file
View File

@@ -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: `<p>Your verification code is: <strong>${verificationCode}</strong></p><p>This code will expire in 15 minutes.</p>`,
});
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,
};

12
backend/utils/mail.js Normal file
View File

@@ -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
},
});

1
frontend Submodule

Submodule frontend added at 99cc24f229

456
mysql-code/Init-Data.sql Normal file
View File

@@ -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 ()
);

125
mysql-code/Schema.sql Normal file
View File

@@ -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
);

View File

@@ -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()

View File

@@ -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)