Finish admin dashboard and update sql code
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal 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
201
LICENSE
Normal 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
34
README.md
Normal 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
|
||||||
|
```
|
||||||
47
backend/controllers/category.js
Normal file
47
backend/controllers/category.js
Normal 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!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
90
backend/controllers/history.js
Normal file
90
backend/controllers/history.js
Normal 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" });
|
||||||
|
}
|
||||||
|
};
|
||||||
269
backend/controllers/product.js
Normal file
269
backend/controllers/product.js
Normal 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",
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// );
|
||||||
53
backend/controllers/recommendation.js
Normal file
53
backend/controllers/recommendation.js
Normal 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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
302
backend/controllers/review.js
Normal file
302
backend/controllers/review.js
Normal 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,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
164
backend/controllers/search.js
Normal file
164
backend/controllers/search.js
Normal 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
308
backend/controllers/user.js
Normal 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
56
backend/index.js
Normal 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
1592
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
backend/package.json
Normal file
28
backend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/routes/category.js
Normal file
14
backend/routes/category.js
Normal 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
14
backend/routes/history.js
Normal 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
34
backend/routes/product.js
Normal 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;
|
||||||
8
backend/routes/recommendation.js
Normal file
8
backend/routes/recommendation.js
Normal 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
9
backend/routes/review.js
Normal 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
14
backend/routes/search.js
Normal 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
42
backend/routes/user.js
Normal 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
12
backend/utils/database.js
Normal 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
49
backend/utils/helper.js
Normal 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
12
backend/utils/mail.js
Normal 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
1
frontend
Submodule
Submodule frontend added at 99cc24f229
456
mysql-code/Init-Data.sql
Normal file
456
mysql-code/Init-Data.sql
Normal 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
125
mysql-code/Schema.sql
Normal 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
|
||||||
|
);
|
||||||
163
recommondation-engine/app.py
Normal file
163
recommondation-engine/app.py
Normal 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()
|
||||||
34
recommondation-engine/server.py
Normal file
34
recommondation-engine/server.py
Normal 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)
|
||||||
Reference in New Issue
Block a user