diff --git a/app.js b/app.js index b30c8ad..dac9445 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ app.set("view engine", "ejs"); app.use(logger("dev")); app.use(express.json()); -app.use(express.urlencoded({ extended: false })); +app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); diff --git a/controllers/category.js b/controllers/category.js index 5eff104..42fbdc7 100644 --- a/controllers/category.js +++ b/controllers/category.js @@ -1,6 +1,7 @@ const Category = require("../models/category.js"); const Item = require("../models/item.js"); const asyncHandler = require("express-async-handler"); +const { body, validationResult } = require("express-validator"); exports.index = asyncHandler(async (req, res, next) => { // get array of relevant variables for displaying category and its items @@ -24,20 +25,89 @@ exports.index = asyncHandler(async (req, res, next) => { }); }); -exports.category_create_get = asyncHandler(async (req, res, next) => { - res.send("ligma"); -}); +exports.category_create_get = (req, res, next) => { + res.render("createcategory"); +}; -exports.category_create_post = asyncHandler(async (req, res, next) => { - res.send("ligma"); -}); +exports.category_create_post = [ + // Validate and sanitize name + body("name", "Category must have a name!") + .trim() + .isLength({ min: 1 }) + .escape(), + + // Validate and sanitize description + body("description", "Category must have a description!") + .trim() + .isLength({ min: 1 }) + .escape(), + + // Process request after validation and sanitization. + asyncHandler(async (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + + // Create a new category with escaped and trimmed data. + const category = new Category({ + name: req.body.name, + simpleName: req.body.name.toLowerCase().replace(" ", ""), + description: req.body.description, + }); + + // if there are validation errors + if (!errors.isEmpty()) { + // Render the creation form again with sanitized values/error messages. + res.render("createcategory", { + errors: errors.array(), + }); + return; + } + // Data from form is valid. + else { + // Check if Category with same name already exists. + const categoryExists = await Category.findOne({ + name: req.body.name, + }).exec(); + if (categoryExists) { + // Category exists, redirect to its page. + res.redirect(`/${categoryExists.simpleName}`); + } + // else category is unique + else { + await category.save(); + // saved. Redirect to new category page. + res.redirect(`/${category.simpleName}`); + } + } + }), +]; exports.category_delete_get = asyncHandler(async (req, res, next) => { - res.send("ligma"); -}); - -exports.category_delete_post = asyncHandler(async (req, res, next) => { - res.send("ligma"); + // get current category from URL + const category = await Category.findOne({ + simpleName: req.params.category, + }) + .lean() + .exec(); + // check + if (category === null) { + const error = new Error("Category not found"); + return next(error); + } + // else, continue to find all relevant items (if any) + const items = await Item.find({ category: category._id }).lean().exec(); + // if items found + if (items.length > 0) { + // delete all items from db - for loop used for practicality reasons (forEach() doesn't play well with async) + for (let i = 0; i < items.length; i++) { + const itemID = items[i]._id; + await Item.findByIdAndDelete(itemID); + } + } + // delete category + await Category.findByIdAndDelete(category._id); + // redirects to Home + res.redirect("/"); }); exports.category_update_get = asyncHandler(async (req, res, next) => { diff --git a/controllers/item.js b/controllers/item.js index 444bf2c..14df402 100644 --- a/controllers/item.js +++ b/controllers/item.js @@ -1,6 +1,7 @@ const Category = require("../models/category.js"); const Item = require("../models/item.js"); const asyncHandler = require("express-async-handler"); +const { body, validationResult } = require("express-validator"); exports.index = asyncHandler(async (req, res, next) => { // get current item from URL @@ -23,24 +24,123 @@ exports.index = asyncHandler(async (req, res, next) => { }); }); -exports.item_detail = asyncHandler(async (req, res, next) => { - res.send("ligma"); -}); - exports.item_create_get = asyncHandler(async (req, res, next) => { - res.send("ligma"); + // get current category from URL + const category = await Category.findOne({ + simpleName: req.params.category, + }) + .lean() + .exec(); + // render create item page + res.render("createitem", { + category: category, + }); }); -exports.item_create_post = asyncHandler(async (req, res, next) => { - res.send("ligma"); -}); +exports.item_create_post = [ + // Validate and sanitize name + body("name", "Item must have a name!").trim().isLength({ min: 1 }).escape(), + + // Validate and sanitize description + body("description", "Item must have a description!") + .trim() + .isLength({ min: 1 }) + .escape(), + + // Validate and sanitize price (formatting has already been taken care of) + body("dollars", "Item must have a dollar amount!") + .trim() + .isLength({ min: 1 }) + .escape(), + body("cents", "Item must have a cent amount!") + .trim() + .isLength({ min: 2, max: 2 }) + .escape(), + + // Validate and sanitize price (formatting has already been taken care of) + body("quantity", "Item must have a quantity!") + .trim() + .isLength({ min: 1 }) + .escape(), + + // Process request after validation and sanitization. + asyncHandler(async (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + + // get current category from URL + const category = await Category.findOne({ + simpleName: req.params.category, + }) + .lean() + .exec(); + + console.log([ + req.body.name, + category._id, + req.body.description, + req.body.dollars, + req.body.cents, + req.body.quantity, + ]); + + // Create a new Item with escaped and trimmed data. + const item = new Item({ + name: req.body.name, + category: category._id, + description: req.body.description, + price: Number(`${req.body.dollars}.${req.body.cents}`), + quantity: req.body.quantity, + }); + + // if there are validation errors + if (!errors.isEmpty()) { + // Render the creation form again with sanitized values/error messages. + res.render("createcategory", { + errors: errors.array(), + }); + return; + } + // Data from form is valid. + else { + // Check if Item with same name already exists. + const itemExists = await Item.findOne({ + name: req.body.name, + category: category._id, + }) + .lean() + .exec(); + if (itemExists) { + // Item exists, redirect to its page. + res.redirect(`/${req.params.category}/${itemExists._id}`); + } + // else Item is unique + else { + // save item and redirect to its ID + item.save().then((uploadedItem) => { + res.redirect(`/${req.params.category}/${uploadedItem._id}`); + }); + } + } + }), +]; exports.item_delete_get = asyncHandler(async (req, res, next) => { - res.send("ligma"); -}); - -exports.item_delete_post = asyncHandler(async (req, res, next) => { - res.send("ligma"); + // get current item from URL + const item = await Item.findOne({ + _id: req.params.item, + }) + .lean() + .exec(); + // check + if (item === null) { + const error = new Error("Item not found"); + return next(error); + } + // delete item + await Item.findByIdAndDelete(item._id); + // redirects to parent category + res.redirect(`/${req.params.category}`); }); exports.item_update_get = asyncHandler(async (req, res, next) => { diff --git a/package-lock.json b/package-lock.json index 5ccafb3..00c740d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,18 +15,16 @@ "ejs": "^3.1.9", "express": "^4.18.2", "express-async-handler": "^1.2.0", + "express-validator": "^7.0.1", "http-errors": "~1.6.3", + "mongoose": "^7.5.0", "morgan": "~1.9.1" - }, - "devDependencies": { - "mongoose": "^7.5.0" } }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.0.tgz", "integrity": "sha512-Xfijy7HvfzzqiOAhAepF4SGN5e9leLkMvg/OPOF97XemjfVCYN/oWa75wnkc6mltMSTwY+XlbhWgUOJmkFspSw==", - "dev": true, "optional": true, "dependencies": { "sparse-bitfield": "^3.0.3" @@ -45,20 +43,17 @@ "node_modules/@types/node": { "version": "20.5.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", - "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", - "dev": true + "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==" }, "node_modules/@types/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==", - "dev": true + "integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==" }, "node_modules/@types/whatwg-url": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "dev": true, "dependencies": { "@types/node": "*", "@types/webidl-conversions": "*" @@ -222,7 +217,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/bson/-/bson-5.4.0.tgz", "integrity": "sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==", - "dev": true, "engines": { "node": ">=14.20.1" } @@ -468,6 +462,18 @@ "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" }, + "node_modules/express-validator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", + "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "^13.9.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -719,8 +725,7 @@ "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -751,11 +756,15 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", - "dev": true, "engines": { "node": ">=12.0.0" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -768,7 +777,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "dev": true, "optional": true }, "node_modules/merge-descriptors": { @@ -829,7 +837,6 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.8.1.tgz", "integrity": "sha512-wKyh4kZvm6NrCPH8AxyzXm3JBoEf4Xulo0aUWh3hCgwgYJxyQ1KLST86ZZaSWdj6/kxYUA3+YZuyADCE61CMSg==", - "dev": true, "dependencies": { "bson": "^5.4.0", "mongodb-connection-string-url": "^2.6.0", @@ -870,7 +877,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dev": true, "dependencies": { "@types/whatwg-url": "^8.2.1", "whatwg-url": "^11.0.0" @@ -880,7 +886,6 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.5.0.tgz", "integrity": "sha512-FpOWOb0AJuaVcplmEyIJ2eCbVGe4gOoniPD+pmft5BrGrNrsFcnYXlERdXtBApGHMHPwD7WbxTyhCbUNr72F3Q==", - "dev": true, "dependencies": { "bson": "^5.4.0", "kareem": "2.5.1", @@ -901,8 +906,7 @@ "node_modules/mongoose/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/morgan": { "version": "1.9.1", @@ -923,7 +927,6 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "dev": true, "engines": { "node": ">=4.0.0" } @@ -932,7 +935,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dev": true, "dependencies": { "debug": "4.x" }, @@ -944,7 +946,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -960,8 +961,7 @@ "node_modules/mquery/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/ms": { "version": "2.0.0", @@ -1032,7 +1032,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -1239,14 +1238,12 @@ "node_modules/sift": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==", - "dev": true + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -1256,7 +1253,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "dev": true, "dependencies": { "ip": "^2.0.0", "smart-buffer": "^4.2.0" @@ -1270,7 +1266,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "dev": true, "optional": true, "dependencies": { "memory-pager": "^1.0.2" @@ -1307,7 +1302,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, "dependencies": { "punycode": "^2.1.1" }, @@ -1343,6 +1337,14 @@ "node": ">= 0.4.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1355,7 +1357,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, "engines": { "node": ">=12" } @@ -1364,7 +1365,6 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" diff --git a/package.json b/package.json index 4e59f14..3a8c27a 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,8 @@ "express": "^4.18.2", "express-async-handler": "^1.2.0", "http-errors": "~1.6.3", - "morgan": "~1.9.1" - }, - "devDependencies": { + "morgan": "~1.9.1", + "express-validator": "^7.0.1", "mongoose": "^7.5.0" } } diff --git a/routes/index.js b/routes/index.js index ab8ed54..9372994 100644 --- a/routes/index.js +++ b/routes/index.js @@ -17,15 +17,15 @@ router.get("/:category", category_controller.index); router.get("/:category/update", category_controller.category_update_get); router.post("/:category/update", category_controller.category_update_post); router.get("/:category/delete", category_controller.category_delete_get); -router.post("/:category/delete", category_controller.category_delete_post); // item functions -router.get("/:category/:item", item_controller.index); +// create URL has to go first before :matchers router.get("/:category/createitem", item_controller.item_create_get); router.post("/:category/createitem", item_controller.item_create_post); +// +router.get("/:category/:item", item_controller.index); router.get("/:category/:item/update", item_controller.item_update_get); router.post("/:category/:item/update", item_controller.item_update_post); router.get("/:category/:item/delete", item_controller.item_delete_get); -router.post("/:category/:item/delete", item_controller.item_delete_post); module.exports = router; diff --git a/views/category.ejs b/views/category.ejs index 755ee2d..088bd75 100644 --- a/views/category.ejs +++ b/views/category.ejs @@ -19,7 +19,22 @@
<% }) %> + + + Create new item +
+ + Delete this category + +