diff --git a/index.js b/index.js index ccb1ce0..482d07f 100644 --- a/index.js +++ b/index.js @@ -49,10 +49,9 @@ app.get("/", (req, res) => { }) // Register routers -app.use("/misc", require("./routes/misc")); - -// API routes app.use(checkHeaders) // Middleware to check Content-Type and API key headers +app.use("/misc", require("./routes/misc")); +app.use("/listings", require("./routes/listings/listings")); app.use("/", require("./routes/orders/reservation")); async function onDBSynchronise() { diff --git a/middleware/ListingsFileFilter.js b/middleware/ListingsFileFilter.js new file mode 100644 index 0000000..386ff2d --- /dev/null +++ b/middleware/ListingsFileFilter.js @@ -0,0 +1,17 @@ +const path = require('path'); + +const ListingsFileFilter = function (req, file, cb) { + const allowedMIMETypes = /jpeg|jpg|png|svg\+xml/; + const allowedExtensions = /jpeg|jpg|png|svg/; + + const mimetype = allowedMIMETypes.test(file.mimetype); + const extname = allowedExtensions.test(path.extname(file.originalname).toLowerCase()); + + if (mimetype && extname) { + cb(null, true); + } else { + cb(new Error('Only .jpeg, .jpg, .png, and .svg files are allowed'), false); + } +}; + +module.exports = ListingsFileFilter; \ No newline at end of file diff --git a/middleware/ListingsStoreFile.js b/middleware/ListingsStoreFile.js new file mode 100644 index 0000000..a51a673 --- /dev/null +++ b/middleware/ListingsStoreFile.js @@ -0,0 +1,23 @@ +const multer = require('multer'); +const path = require('path'); +const { v4: uuidv4 } = require('uuid'); +const ListingsFileFilter = require('./ListingsFileFilter'); + + +const storage = multer.diskStorage({ + destination: (req, file, callback) => { + callback(null, path.join(__dirname, '../FileStore')) + }, + filename: (req, file, callback) => { + callback(null, uuidv4() + path.extname(file.originalname)) + } +}) + +const ListingsStoreFile = multer({ + storage: storage, + limits: { fileSize: 1024 * 1024 * 10 }, + fileFilter: ListingsFileFilter +}) +.single('images') + +module.exports = ListingsStoreFile; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 71e36a1..7bf7dae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "nodemailer": "^6.9.13", "sequelize": "^6.37.3", "sqlite3": "^5.1.7", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "uuidv4": "^6.2.13" } }, "node_modules/@fastify/busboy": { @@ -978,6 +979,11 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "optional": true }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, "node_modules/@types/validator": { "version": "13.11.10", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", @@ -4445,6 +4451,23 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuidv4": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", + "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "dependencies": { + "@types/uuid": "8.3.4", + "uuid": "8.3.2" + } + }, + "node_modules/uuidv4/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", diff --git a/package.json b/package.json index 82624e6..221f45d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "nodemailer": "^6.9.13", "sequelize": "^6.37.3", "sqlite3": "^5.1.7", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "uuidv4": "^6.2.13" } } diff --git a/routes/listings/listings.js b/routes/listings/listings.js new file mode 100644 index 0000000..0647a63 --- /dev/null +++ b/routes/listings/listings.js @@ -0,0 +1,154 @@ +const express = require("express"); +const multer = require("multer"); +const router = express.Router(); +const { FoodListing } = require("../../models"); +const { Host } = require("../../models"); +const Universal = require("../../services/Universal"); +const FileManager = require("../../services/FileManager"); +const ListingsStoreFile = require("../../middleware/ListingsStoreFile"); + +router.post("/createHost", async (req, res) => { + // POST a new host before creating a food listing + const data = req.body; + try { + const newHost = await Host.create(data); + res.status(200).json({ + message: "Host created successfully!", + newHost, + }); + } catch (error) { + console.error("Error creating host:", error); + res.status(400).json({ + error: "One or more required payloads were not provided.", + }); + } +}); + +router.get("/hostInfo", async (req, res) => { + try { + // GET host info before displaying listing's host name + const hostInfo = await Host.findByPk( + "272d3d17-fa63-49c4-b1ef-1a3b7fe63cf4" + ); // hardcoded for now + if (hostInfo) { + res.status(200).json(hostInfo); + } else { + res.status(404).json({ error: "Host not found" }); + } + } catch (error) { + console.error("Error fetching host info:", error); + res.status(500).json({ error: "Failed to fetch host info" }); + } +}); + +router.get("/", async (req, res) => { + // GET all food listings + try { + const foodListings = await FoodListing.findAll(); + res.status(200).json(foodListings); + } catch (error) { + console.error("Error retrieving food listings:", error); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// Flow: addListing -> refreshes -> fetchListings -> Image component sources from /getImageForListing?listingID=&imageName= -> send file back down -> Image component renders + +router.get("/getImageForListing", async (req, res) => { + const { listingID, imageName } = req.query; + if (!listingID || !imageName) { + res.status(400).send("ERROR: Invalid request parameters."); + console.error("Invalid request parameters."); + return; + } + + // Find the listing + const findListing = await FoodListing.findByPk(listingID); + const findImageName = await FileManager.prepFile(imageName); + if (!findImageName) { + res.status(404).send("ERROR: Image not found."); + console.error("Image not found."); + return; + } + if (!findListing) { + res.status(404).send("ERROR: Listing not found."); + console.error("Listing not found."); + return; + } + if (findListing.images !== imageName) { + res.status(404).send("ERROR: Requested image does not belong to its corresponding listing."); + console.error("Requested image does not belong to its corresponding listing."); + return; + } + res.status(200).sendFile(findImageName.substring("SUCCESS: File path: ".length)) + return; +}); + +router.post("/addListing", async (req, res) => { + ListingsStoreFile(req, res, async (err) => { + console.log(req.body); + if ( + !req.body.title || + !req.body.shortDescription || + !req.body.longDescription || + !req.body.portionPrice || + !req.body.totalSlots || + !req.body.datetime + ) { + res.status(400).send( + "One or more required payloads were not provided" + ); + return; + } else { + if (err instanceof multer.MulterError) { + console.error("Multer error:", err); + res.status(400).send("Image upload error"); + } else if (err) { + console.error("Unknown error occured during upload:", err); + res.status(500).send("Internal server error"); + } else if (!req.file) { + res.status(400).send("No file was selected to upload"); + } else { + const uploadImageResponse = await FileManager.saveFile( + req.file.filename + ); + if (uploadImageResponse) { + const formattedDatetime = req.body.datetime + ":00.000Z"; + const listingDetails = { + listingID: Universal.generateUniqueID(), + title: req.body.title, + images: req.file.filename, + shortDescription: req.body.shortDescription, + longDescription: req.body.longDescription, + portionPrice: req.body.portionPrice, + totalSlots: req.body.totalSlots, + datetime: formattedDatetime, + approxAddress: "Yishun, Singapore", // hardcoded for now + address: + "1 North Point Dr, #01-164/165 Northpoint City, Singapore 768019", // hardcoded for now + hostID: "272d3d17-fa63-49c4-b1ef-1a3b7fe63cf4", // hardcoded for now + published: true, + }; + const addListingResponse = await FoodListing.create( + listingDetails + ); + if (addListingResponse) { + res.status(200).json({ + message: "Food listing created successfully", + listingDetails, + }); + return; + } else { + res.status(400).send("Failed to create food listing"); + return; + } + } else { + res.status(400).send("Failed to upload image"); + return; + } + } + } + }); +}); + +module.exports = router;