diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41b..d3cb2ac4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,3 @@ { -} \ No newline at end of file + "postman.settings.dotenv-detection-notification-visibility": false +} diff --git a/app/server.js b/app/server.js index 5201d84d..e3bead38 100644 --- a/app/server.js +++ b/app/server.js @@ -1,31 +1,140 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const jwt = require('jsonwebtoken'); -const swaggerUi = require('swagger-ui-express'); -const YAML = require('yamljs'); -const swaggerDocument = YAML.load('./swagger.yaml'); // Replace './swagger.yaml' with the path to your Swagger file +const express = require("express"); +const bodyParser = require("body-parser"); +const jwt = require("jsonwebtoken"); +const swaggerUi = require("swagger-ui-express"); +const YAML = require("yamljs"); +const swaggerDocument = YAML.load("./swagger.yaml"); const app = express(); +require("dotenv").config(); + +// Using optional revealed key in case tester does not have .env file with secret key +const JWT_KEY = process.env.SECRET_KEY || "84345512"; + +const authenticate = (req, res, next) => { + const token = req.header("Authorization")?.split(" ")[1]; + + if (!token) { + res.status(401).json({ error: "No token provided" }); + } + + try { + const decoded = jwt.verify(token, JWT_KEY); + req.user = decoded; + next(); + } catch (err) { + res.status(403).json({ error: "Invalid or expired token" }); + } +}; app.use(bodyParser.json()); // Importing the data from JSON files -const users = require('../initial-data/users.json'); -const brands = require('../initial-data/brands.json'); -const products = require('../initial-data/products.json'); +const users = require("../initial-data/users.json"); +const brands = require("../initial-data/brands.json"); +const products = require("../initial-data/products.json"); // Error handling app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).send('Something broke!'); + console.error(err.stack); + res.status(500).send("Something broke!"); }); // Swagger -app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); // Starting the server -const PORT = process.env.PORT || 3000; +const PORT = 3000; app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); + console.log(`Server running on port ${PORT}`); +}); + +// Routes +app.get("/api/brands", (req, res) => { + res.status(200).json(brands); +}); + +app.get("/api/brands/:id/products", (req, res) => { + const brandId = String(req.params.id); + const filteredProducts = products.filter((product) => { + const categoryId = String(product.categoryId); + return categoryId === brandId; + }); + res.status(200).json(filteredProducts); +}); + +app.get("/api/products", (req, res) => { + res.status(200).json(products); +}); + +app.get("/api/me/cart", authenticate, (req, res) => { + const cart = req.user.cart; + return res.status(200).json(cart); +}); + +app.post("/api/login", (req, res) => { + if (req.body.username && req.body.password) { + const validUser = users.find((user) => { + return ( + user.login.username === req.body.username && + user.login.password === req.body.password + ); + }); + if (validUser) { + const userIndex = users.findIndex( + (user) => user.login.sha1 === validUser.login.sha1 + ); + const newAccessToken = jwt.sign( + { + username: validUser.login.username, + cart: validUser.cart, + }, + JWT_KEY, + { expiresIn: "30m" } + ); + users[userIndex].token = newAccessToken; + res.status(200).json(newAccessToken); + } else { + res.status(401).json({ error: "Invalid username or password" }); + } + } else { + res.status(400).json({ error: "Incorrectly formatted response" }); + } +}); + +app.post("/api/me/cart", authenticate, (req, res) => { + const item = req.body; + const cart = req.user.cart; + item.quantity = 1; + cart.push(item); + res.status(200).json(cart); +}); + +app.post("/api/me/cart/:productId", authenticate, (req, res) => { + const cart = req.user.cart; + const newQuantity = req.body.quantity; + const productId = req.params.productId; + const itemToUpdate = cart.find((item) => item.id === productId); + if (itemToUpdate) { + itemToUpdate.quantity = newQuantity; + res.status(200).json(itemToUpdate); + } else { + const productToAdd = products.find((item) => item.id === productId); + productToAdd.quantity = newQuantity; + cart.push(productToAdd); + res.status(200).json(productToAdd); + } +}); + +app.delete("/api/me/cart/:productId", authenticate, (req, res) => { + let cart = req.user.cart; + const productId = req.params.productId; + const itemToRemove = cart.find((item) => item.id === productId); + if (itemToRemove) { + cart = cart.filter((item) => item.id != productId); + res.status(200).json(cart); + } else { + res.status(200).json(cart); + } }); module.exports = app; diff --git a/initial-data/brands.json b/initial-data/brands.json index 139062c9..86a147eb 100644 --- a/initial-data/brands.json +++ b/initial-data/brands.json @@ -1,22 +1,22 @@ [ - { - "id": "1", - "name" : "Oakley" - }, - { - "id": "2", - "name" : "Ray Ban" - }, - { - "id": "3", - "name" : "Levi's" - }, - { - "id": "4", - "name" : "DKNY" - }, - { - "id": "5", - "name" : "Burberry" - } -] \ No newline at end of file + { + "id": "1", + "name": "Oakley" + }, + { + "id": "2", + "name": "Ray Ban" + }, + { + "id": "3", + "name": "Levi's" + }, + { + "id": "4", + "name": "DKNY" + }, + { + "id": "5", + "name": "Burberry" + } +] diff --git a/initial-data/products.json b/initial-data/products.json index eb85b83f..1facca96 100644 --- a/initial-data/products.json +++ b/initial-data/products.json @@ -1,90 +1,134 @@ [ - { - "id": "1", - "categoryId": "1", - "name": "Superglasses", - "description": "The best glasses in the world", - "price":150, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "2", - "categoryId": "1", - "name": "Black Sunglasses", - "description": "The best glasses in the world", - "price":100, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "3", - "categoryId": "1", - "name": "Brown Sunglasses", - "description": "The best glasses in the world", - "price":50, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "4", - "categoryId": "2", - "name": "Better glasses", - "description": "The best glasses in the world", - "price":1500, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "5", - "categoryId": "2", - "name": "Glasses", - "description": "The most normal glasses in the world", - "price":150, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "6", - "categoryId": "3", - "name": "glas", - "description": "Pretty awful glasses", - "price":10, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "7", - "categoryId": "3", - "name": "QDogs Glasses", - "description": "They bark", - "price":1500, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "8", - "categoryId": "4", - "name": "Coke cans", - "description": "The thickest glasses in the world", - "price":110, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "9", - "categoryId": "4", - "name": "Sugar", - "description": "The sweetest glasses in the world", - "price":125, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "10", - "categoryId": "5", - "name": "Peanut Butter", - "description": "The stickiest glasses in the world", - "price":103, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - }, - { - "id": "11", - "categoryId": "5", - "name": "Habanero", - "description": "The spiciest glasses in the world", - "price":153, - "imageUrls":["https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg","https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg"] - } -] \ No newline at end of file + { + "id": "1", + "categoryId": "1", + "name": "Superglasses", + "description": "The best glasses in the world", + "price": 150, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "2", + "categoryId": "1", + "name": "Black Sunglasses", + "description": "The best glasses in the world", + "price": 100, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "3", + "categoryId": "1", + "name": "Brown Sunglasses", + "description": "The best glasses in the world", + "price": 50, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "4", + "categoryId": "2", + "name": "Better glasses", + "description": "The best glasses in the world", + "price": 1500, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "5", + "categoryId": "2", + "name": "Glasses", + "description": "The most normal glasses in the world", + "price": 150, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "6", + "categoryId": "3", + "name": "glas", + "description": "Pretty awful glasses", + "price": 10, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "7", + "categoryId": "3", + "name": "QDogs Glasses", + "description": "They bark", + "price": 1500, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "8", + "categoryId": "4", + "name": "Coke cans", + "description": "The thickest glasses in the world", + "price": 110, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "9", + "categoryId": "4", + "name": "Sugar", + "description": "The sweetest glasses in the world", + "price": 125, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "10", + "categoryId": "5", + "name": "Peanut Butter", + "description": "The stickiest glasses in the world", + "price": 103, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + }, + { + "id": "11", + "categoryId": "5", + "name": "Habanero", + "description": "The spiciest glasses in the world", + "price": 153, + "imageUrls": [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg" + ] + } +] diff --git a/initial-data/users.json b/initial-data/users.json index 9a6231e8..84396912 100644 --- a/initial-data/users.json +++ b/initial-data/users.json @@ -1,104 +1,104 @@ [ - { - "gender": "female", - "cart":[], - "name": { - "title": "mrs", - "first": "susanna", - "last": "richards" - }, - "location": { - "street": "2343 herbert road", - "city": "duleek", - "state": "donegal", - "postcode": 38567 - }, - "email": "susanna.richards@example.com", - "login": { - "username": "yellowleopard753", - "password": "jonjon", - "salt": "eNuMvema", - "md5": "a8be2a69c8c91684588f4e1a29442dd7", - "sha1": "f9a60bbf8b550c10712e470d713784c3ba78a68e", - "sha256": "4dca9535634c102fbadbe62dc5b37cd608f9f3ced9aacf42a5669e5a312690a0" - }, - "dob": "1954-10-09 10:47:17", - "registered": "2003-08-03 01:12:24", - "phone": "031-941-6700", - "cell": "081-032-7884", - "picture": { - "large": "https://randomuser.me/api/portraits/women/55.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/55.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/55.jpg" - }, - "nat": "IE" + { + "gender": "female", + "cart": [], + "name": { + "title": "mrs", + "first": "susanna", + "last": "richards" }, - { - "gender": "male", - "cart":[], - "name": { - "title": "mr", - "first": "salvador", - "last": "jordan" - }, - "location": { - "street": "9849 valley view ln", - "city": "burkburnett", - "state": "delaware", - "postcode": 78623 - }, - "email": "salvador.jordan@example.com", - "login": { - "username": "lazywolf342", - "password": "tucker", - "salt": "oSngghny", - "md5": "30079fb24f447efc355585fcd4d97494", - "sha1": "dbeb2d0155dad0de0ab9bbe21c062e260a61d741", - "sha256": "4f9416fa89bfd251e07da3ca0aed4d077a011d6ef7d6ed75e1d439c96d75d2b2" - }, - "dob": "1955-07-28 22:32:14", - "registered": "2010-01-10 06:52:31", - "phone": "(944)-261-2164", - "cell": "(888)-556-7285", - "picture": { - "large": "https://randomuser.me/api/portraits/men/4.jpg", - "medium": "https://randomuser.me/api/portraits/med/men/4.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/men/4.jpg" - }, - "nat": "US" + "location": { + "street": "2343 herbert road", + "city": "duleek", + "state": "donegal", + "postcode": 38567 }, - { - "gender": "female", - "cart":[], - "name": { - "title": "mrs", - "first": "natalia", - "last": "ramos" - }, - "location": { - "street": "7934 avenida de salamanca", - "city": "madrid", - "state": "aragón", - "postcode": 43314 - }, - "email": "natalia.ramos@example.com", - "login": { - "username": "greenlion235", - "password": "waters", - "salt": "w10ZFgoO", - "md5": "19f6fb510c58be44b2df1816d88b739d", - "sha1": "18e545aee27156ee6be35596631353a14ee03007", - "sha256": "2b23b25939ece8ba943fe9abcb3074105867c267d122081a2bc6322f935ac809" - }, - "dob": "1947-03-05 15:23:07", - "registered": "2004-07-19 02:44:19", - "phone": "903-556-986", - "cell": "696-867-013", - "picture": { - "large": "https://randomuser.me/api/portraits/women/54.jpg", - "medium": "https://randomuser.me/api/portraits/med/women/54.jpg", - "thumbnail": "https://randomuser.me/api/portraits/thumb/women/54.jpg" - }, - "nat": "ES" - } -] \ No newline at end of file + "email": "susanna.richards@example.com", + "login": { + "username": "yellowleopard753", + "password": "jonjon", + "salt": "eNuMvema", + "md5": "a8be2a69c8c91684588f4e1a29442dd7", + "sha1": "f9a60bbf8b550c10712e470d713784c3ba78a68e", + "sha256": "4dca9535634c102fbadbe62dc5b37cd608f9f3ced9aacf42a5669e5a312690a0" + }, + "dob": "1954-10-09 10:47:17", + "registered": "2003-08-03 01:12:24", + "phone": "031-941-6700", + "cell": "081-032-7884", + "picture": { + "large": "https://randomuser.me/api/portraits/women/55.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/55.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/55.jpg" + }, + "nat": "IE" + }, + { + "gender": "male", + "cart": [], + "name": { + "title": "mr", + "first": "salvador", + "last": "jordan" + }, + "location": { + "street": "9849 valley view ln", + "city": "burkburnett", + "state": "delaware", + "postcode": 78623 + }, + "email": "salvador.jordan@example.com", + "login": { + "username": "lazywolf342", + "password": "tucker", + "salt": "oSngghny", + "md5": "30079fb24f447efc355585fcd4d97494", + "sha1": "dbeb2d0155dad0de0ab9bbe21c062e260a61d741", + "sha256": "4f9416fa89bfd251e07da3ca0aed4d077a011d6ef7d6ed75e1d439c96d75d2b2" + }, + "dob": "1955-07-28 22:32:14", + "registered": "2010-01-10 06:52:31", + "phone": "(944)-261-2164", + "cell": "(888)-556-7285", + "picture": { + "large": "https://randomuser.me/api/portraits/men/4.jpg", + "medium": "https://randomuser.me/api/portraits/med/men/4.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/men/4.jpg" + }, + "nat": "US" + }, + { + "gender": "female", + "cart": [], + "name": { + "title": "mrs", + "first": "natalia", + "last": "ramos" + }, + "location": { + "street": "7934 avenida de salamanca", + "city": "madrid", + "state": "aragón", + "postcode": 43314 + }, + "email": "natalia.ramos@example.com", + "login": { + "username": "greenlion235", + "password": "waters", + "salt": "w10ZFgoO", + "md5": "19f6fb510c58be44b2df1816d88b739d", + "sha1": "18e545aee27156ee6be35596631353a14ee03007", + "sha256": "2b23b25939ece8ba943fe9abcb3074105867c267d122081a2bc6322f935ac809" + }, + "dob": "1947-03-05 15:23:07", + "registered": "2004-07-19 02:44:19", + "phone": "903-556-986", + "cell": "696-867-013", + "picture": { + "large": "https://randomuser.me/api/portraits/women/54.jpg", + "medium": "https://randomuser.me/api/portraits/med/women/54.jpg", + "thumbnail": "https://randomuser.me/api/portraits/thumb/women/54.jpg" + }, + "nat": "ES" + } +] diff --git a/package-lock.json b/package-lock.json index ab397dea..3b402909 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "license": "ISC", "dependencies": { "body-parser": "^1.18.2", + "dotenv": "^16.4.7", "express": "^4.18.2", "finalhandler": "latest", "http": "latest", "jsonwebtoken": "^9.0.2", "querystring": "^0.2.0", - "rand-token": "^0.4.0", "router": "^1.3.2", "swagger-ui-express": "^5.0.0", "yamljs": "^0.3.0" @@ -560,6 +560,18 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1600,14 +1612,6 @@ "node": ">=0.4.x" } }, - "node_modules/rand-token": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/rand-token/-/rand-token-0.4.0.tgz", - "integrity": "sha512-FLNNsir2R+XY8LKsZ+8u/w0qZ4sGit7cpNdznsI77cAVob6UlVPueDKRyjJ3W1Q6FJLgAVH98JvlqqpSaL7NEQ==", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2543,6 +2547,11 @@ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, + "dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==" + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3318,11 +3327,6 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==" }, - "rand-token": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/rand-token/-/rand-token-0.4.0.tgz", - "integrity": "sha512-FLNNsir2R+XY8LKsZ+8u/w0qZ4sGit7cpNdznsI77cAVob6UlVPueDKRyjJ3W1Q6FJLgAVH98JvlqqpSaL7NEQ==" - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/package.json b/package.json index afe392cf..b8df4d82 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,12 @@ "license": "ISC", "dependencies": { "body-parser": "^1.18.2", + "dotenv": "^16.4.7", "express": "^4.18.2", "finalhandler": "latest", "http": "latest", "jsonwebtoken": "^9.0.2", "querystring": "^0.2.0", - "rand-token": "^0.4.0", "router": "^1.3.2", "swagger-ui-express": "^5.0.0", "yamljs": "^0.3.0" diff --git a/swagger.yaml b/swagger.yaml index 1c2eb22f..31499863 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -1,11 +1,206 @@ -swagger: '2.0' +swagger: "2.0" info: - version: '1.0.0' - title: 'E-Commerce API' - description: 'API for managing brands, products, and user cart' -host: 'localhost:3000' + version: "1.0.0" + title: "E-Commerce API" + description: "API for managing brands, products, and user cart" +host: "localhost:3000" schemes: - - 'http' -basePath: '/api' + - "http" +basePath: "/api" produces: - - 'application/json' + - "application/json" +tags: + - name: "brands and products" + description: "Everything about products" + - name: "cart" + description: "Everything to do with the cart" + - name: "authentication" + description: "Everything to do with authentication" +paths: + /brands: + get: + tags: + - "brands and products" + summary: "Get all brands" + description: "Returns a list of available brands, or an error message." + produces: + - "application/json" + responses: + 200: + description: "Successful response with a list of brands" + 404: + description: "No brands found" + /brands/{id}/products: + get: + tags: + - "brands and products" + parameters: + - in: "path" + name: "id" + required: true + type: "integer" + description: "The brand ID" + summary: "Get all products of a certain brands" + description: "Returns a list of products for a specific brands" + produces: + - "application/json" + responses: + 200: + description: "Successful response with a list of products" + 400: + description: "No products found" + /products: + get: + tags: + - "brands and products" + summary: "Get all products" + description: "Returns a list of all available products" + produces: + - "application/json" + responses: + 200: + description: "Successful response with a list of all products" + 400: + description: "No products found" + /me/cart: + get: + tags: + - "cart" + summary: "Get the cart" + description: "Returns the cart if a user is logged in" + produces: + - "application/json" + security: + - BearerAuth: [] + responses: + 200: + description: "Successful response with a list of products inside the cart" + 401: + description: "User must be logged in" + post: + tags: + - "cart" + summary: "Add item to cart" + description: "Returns the cart with the new item inside it" + produces: + - "application/json" + security: + - BearerAuth: [] + parameters: + - in: "body" + name: "product" + description: "product to be added to cart" + schema: + type: "object" + required: + - "id" + - "name" + - "description" + - "price" + properties: + id: + type: "string" + name: + type: "string" + description: + type: "string" + price: + type: "integer" + categoryId: + type: "string" + imageUrls: + type: "array" + items: + type: "string" + responses: + 200: + description: "Successful response with new item added to cart" + 401: + description: "You must be logged in to add items to the cart" + /me/cart/{productId}: + post: + tags: + - "cart" + summary: "change quantity of an item in the cart. Adds item if it does not exist" + description: "Returns the cart and items with the new quantity." + produces: + - "application/json" + security: + - BearerAuth: [] + parameters: + - in: "body" + name: "new quantity" + description: "New quantity for item" + schema: + type: "object" + required: + - "quantity" + properties: + quantity: + type: "integer" + - in: "path" + name: "productId" + type: "integer" + required: true + description: "Product that needs quantity changed" + responses: + 200: + description: "Successful response with item quantity updated" + 401: + description: "You must be logged in to access the cart" + delete: + tags: + - "cart" + summary: "remove an item from the cart" + description: "Returns the new cart without to deleted item" + produces: + - "application/json" + security: + - BearerAuth: [] + parameters: + - in: "path" + name: "productId" + type: "integer" + required: true + description: "Item to be removed" + responses: + 200: + description: "Successful response with item removed" + 401: + description: "You must be logged in to access the cart" + /login: + post: + tags: + - "authentication" + summary: "Logs the user in" + description: "Returns the logged in user object with their authentication token" + consumes: + - "application/json" + parameters: + - in: "body" + name: "user" + description: "The user to be logged in" + schema: + type: "object" + required: + - "username" + - "password" + properties: + username: + type: "string" + password: + type: "string" + produces: + - "object" + responses: + 200: + description: "Successful authentication, user logged in" + 400: + description: "Incorrectly formatted response" + 401: + description: "Invalid username or password" +securityDefinitions: + BearerAuth: + type: apiKey + name: Authorization + in: header diff --git a/test/server.test.js b/test/server.test.js index 7ff14c8f..14421633 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -1,14 +1,195 @@ -const chai = require('chai'); -const chaiHttp = require('chai-http'); -const server = require('../app/server'); // Adjust the path as needed +const chai = require("chai"); +const jwt = require("jsonwebtoken"); +const chaiHttp = require("chai-http"); +const server = require("../app/server"); +require("dotenv").config(); +const JWT_KEY = process.env.SECRET_KEY || "84345512"; +const token = jwt.sign({ cart: [] }, JWT_KEY, { expiresIn: "30m" }); const should = chai.should(); +const expect = chai.expect; chai.use(chaiHttp); -// TODO: Write tests for the server +describe("Brands", () => { + describe("/GET brands", () => { + it("should GET an array of brands", (done) => { + chai + .request(server) + .get("/api/brands") + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + done(); + }); + }); + }); + describe("/GET products", () => { + it("should GET all products", (done) => { + chai + .request(server) + .get("/api/products") + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + done(); + }); + }); + }); + describe("/GET brands/:id/products", () => { + it("should GET all products of specified brand", (done) => { + chai + .request(server) + .get("/api/brands/1/products") + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + done(); + }); + }); + }); +}); -describe('Brands', () => {}); +describe("Login", () => { + describe("/POST login", () => { + it("should login successfully with correct credentials", (done) => { + const loginCreds = { + username: "yellowleopard753", + password: "jonjon", + }; -describe('Login', () => {}); + chai + .request(server) + .post("/api/login") + .send(loginCreds) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.a("string"); + done(); + }); + }); + it("should fail if credentials are incorrect", (done) => { + const loginCreds = { + username: "incorrect", + password: "wrongPass", + }; -describe('Cart', () => {}); + chai + .request(server) + .post("/api/login") + .send(loginCreds) + .end((err, res) => { + res.should.have.status(401); + res.body.should.have.property("error"); + done(); + }); + }); + it("should fail if a username is not entered", (done) => { + const loginCreds = { + password: "jonjon", + }; + + chai + .request(server) + .post("/api/login") + .send(loginCreds) + .end((err, res) => { + res.should.have.status(400); + res.body.should.have.property("error"); + done(); + }); + }); + it("should fail if a password is not entered", (done) => { + const loginCreds = { + username: "yellowleopard753", + }; + + chai + .request(server) + .post("/api/login") + .send(loginCreds) + .end((err, res) => { + res.should.have.status(400); + res.body.should.have.property("error"); + done(); + }); + }); + }); +}); + +describe("Cart", () => { + describe("/GET me/cart", () => { + it("should return a cart array", (done) => { + chai + .request(server) + .get("/api/me/cart") + .set("Authorization", `Bearer ${token}`) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + done(); + }); + }); + }); + describe("/POST me/cart", () => { + it("should add an item to the cart", (done) => { + const item = { + id: "1", + categoryId: "1", + name: "Superglasses", + description: "The best glasses in the world", + price: 150, + imageUrls: [ + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + "https://image.shutterstock.com/z/stock-photo-yellow-sunglasses-white-backgound-600820286.jpg", + ], + }; + chai + .request(server) + .post("/api/me/cart") + .set("Authorization", `Bearer ${token}`) + .send(item) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + done(); + }); + }); + }); + describe("/POST me/cart/:productId", () => { + it("should change the quantity of an item in the cart or add the item to the cart with the specified quantity if it does not exist", (done) => { + const productId = "1"; + const newQuantity = 5; + + chai + .request(server) + .post(`/api/me/cart/${productId}`) + .set("Authorization", `Bearer ${token}`) + .send({ quantity: newQuantity }) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("object"); + res.body.should.have.property("id", productId); + res.body.should.have.property("quantity", newQuantity); + done(); + }); + }); + }); + describe("/DELETE me/cart/:productId", () => { + it("should remove the specified item from the cart or return the cart if the item does not exist", (done) => { + const productId = "1"; + + chai + .request(server) + .delete(`/api/me/cart/${productId}`) + .set("Authorization", `Bearer ${token}`) + .end((err, res) => { + res.should.have.status(200); + res.body.should.be.an("array"); + expect(res.body.find((item) => item.id === productId)).to.be + .undefined; + done(); + }); + }); + }); +});