├── .gitignore ├── Procfile ├── README.md ├── access.log ├── emails ├── confirm_email.ejs └── reset_password.ejs ├── env_requirement ├── jest.config.js ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── app.js ├── constants │ └── constant.js ├── controllers │ ├── auth │ │ ├── auth.js │ │ └── auth_utils.js │ ├── category │ │ └── category.js │ ├── item │ │ └── item.js │ └── utils.js ├── db │ └── mongoose_connect.js ├── index.js ├── middlewares │ ├── accept_language.js │ ├── async.js │ ├── check_permissions.js │ ├── cors_header.js │ ├── is_auth.js │ ├── is_intl.js │ ├── log_errors.js │ ├── middlewares.js │ ├── param_validator.js │ ├── requests_limiter.js │ └── uploader │ │ ├── image_uploader.js │ │ └── multer_storage_imgur.js ├── models │ ├── category.js │ ├── image.js │ ├── item.js │ └── user.js ├── routes │ ├── auth.js │ ├── category.js │ ├── helper │ │ ├── 404.js │ │ └── error_handler.js │ ├── item.js │ └── routes.js ├── services │ └── emails │ │ ├── email_sender.js │ │ └── emails.js └── utils │ ├── error_thrower.js │ ├── image_util.js │ ├── is_mongo_id.js │ ├── json_success_fail.js │ ├── jwt_promise.js │ └── param_validation.js └── test ├── fixtures └── keys_bg.png ├── integration_test ├── 404.test.js ├── auth │ ├── login.test.js │ ├── profile.test.js │ ├── refreshFcmToken.test.js │ ├── register.test.js │ ├── resetPassword.test.js │ ├── updateProfile.test.js │ └── verifyEmail.test.js ├── category │ ├── addCategory.test.js │ ├── addCategoryUploadImage.test.js │ ├── addItemToCategory.test.js │ └── getCategories.test.js └── item │ ├── addItem.test.js │ └── getItems.test.js └── uitils └── tokenUtils.js /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | 3 | #jest test coverage 4 | coverage/ 5 | # Logs 6 | logs 7 | npm-debug.log* 8 | 9 | # Optional npm cache directory 10 | .npm 11 | 12 | # Dependency directories 13 | node_modules/ 14 | /node_modules 15 | /jspm_packages 16 | /bower_components 17 | 18 | 19 | # Optional eslint cache 20 | .eslintcache 21 | 22 | 23 | #Build generated 24 | dist/ 25 | build/ 26 | public/bundle.js 27 | public/bundle.js.map 28 | 29 | # Serverless generated files 30 | .serverless/ 31 | 32 | ### VisualStudioCode ### 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | 39 | ### Vim ### 40 | *.sw[a-p] 41 | 42 | ### WebStorm/IntelliJ ### 43 | /.idea 44 | .idea/ 45 | modules.xml 46 | *.ipr 47 | 48 | 49 | ### System Files ### 50 | *.DS_Store 51 | 52 | # Windows thumbnail cache files 53 | Thumbs.db 54 | ehthumbs.db 55 | ehthumbs_vista.db 56 | 57 | # Folder config file 58 | Desktop.ini 59 | 60 | # Recycle Bin used on file shares 61 | $RECYCLE.BIN/ 62 | 63 | # Thumbnails 64 | ._* 65 | 66 | # Files that might appear in the root of a volume 67 | .DocumentRevisions-V100 68 | .fseventsd 69 | .Spotlight-V100 70 | .TemporaryItems 71 | .Trashes 72 | .VolumeIcon.icns 73 | .com.apple.timemachine.donotpresent 74 | 75 | #istanbul reports 76 | .nyc_output/ -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node src/index.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # e-commerce-backend-nodejs (under development) 2 | ### After I end the MVP, I will write a good doc so anyone will have the ability to use at or contributor to it, after that, I will make a mobile app to use this backend 3 | -------------------------------------------------------------------------------- /access.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binSaed/e-commerce-backend-nodejs/c54c503b26c418501c7059cbb74a5922a2df0125/access.log -------------------------------------------------------------------------------- /emails/confirm_email.ejs: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | E-Commerce 9 | 38 | 39 | 40 |
41 | logo 43 |

44 | Thanks for signing up,
<%= name %>! 45 |

46 | target=_blank 47 | style="text-decoration:none;color:#efefef;border-style:solid;border-color:#26c0c6;border-width:8px 25px 8px 25px;display:inline-block;background:#26c0c6;border-radius:20px;text-align:center;margin-bottom:35px">Confirm 48 | Email 49 |

50 | Code: <%= code %> 51 |

52 | 53 |
54 | 64 | 65 | -------------------------------------------------------------------------------- /emails/reset_password.ejs: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | E-Commerce 9 | 38 | 39 | 40 |
41 | logo 43 |

44 | You requested to reset your password 45 |

46 | target=_blank 47 | style="text-decoration:none;color:#efefef;border-style:solid;border-color:#26c0c6;border-width:8px 25px 8px 25px;display:inline-block;background:#26c0c6;border-radius:20px;text-align:center;margin-bottom:35px">Reset 48 | Password 49 |

50 | Code: <%= code %> 51 |

52 | 53 |
54 | 64 | 65 | -------------------------------------------------------------------------------- /env_requirement: -------------------------------------------------------------------------------- 1 | PORT 2 | MONGO_URL 3 | PRIVATE_SERVER_KEY 4 | IMGUR_CLIENT_ID 5 | EMAIL 6 | EMAIL_PASSWORD -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node' 3 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e-commerce-backend-nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "homepage": "https://github.com/AbdOoSaed/e-commerce-backend-nodejs/blob/master/README.md", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/AbdOoSaed/e-commerce-backend-nodejs.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/AbdOoSaed/e-commerce-backend-nodejs/issues" 13 | }, 14 | "scripts": { 15 | "start:pm2": "pm2 kill & pm2 start -i max src/index.js", 16 | "stop:pm2": "pm2 kill", 17 | "start": "node src/index.js", 18 | "start:dev": "nodemon src/index.js", 19 | "test": "jest --runInBand --verbose --forceExit", 20 | "test:watch": "jest --watchAll --runInBand --verbose --forceExit", 21 | "test:coverage": "jest --runInBand --coverage --forceExit" 22 | }, 23 | "keywords": [ 24 | "JS", 25 | "NodeJs", 26 | "backend", 27 | "e-commerce" 28 | ], 29 | "author": "", 30 | "license": "ISC", 31 | "dependencies": { 32 | "@types/express-status-monitor": "^1.2.3", 33 | "accesscontrol": "^2.2.1", 34 | "bcrypt": "^5.0.1", 35 | "blurhash": "^1.1.3", 36 | "compression": "^1.7.4", 37 | "concat-stream": "^2.0.0", 38 | "ejs": "^3.1.6", 39 | "express": "^4.17.1", 40 | "express-mongo-sanitize": "^2.1.0", 41 | "express-rate-limit": "^5.2.6", 42 | "express-status-monitor": "^1.3.3", 43 | "express-validator": "^6.12.0", 44 | "helmet": "^4.6.0", 45 | "http-auth": "^4.1.5", 46 | "http-auth-connect": "^1.0.4", 47 | "http-status-codes": "^2.1.4", 48 | "imgur": "^0.3.2", 49 | "jsonwebtoken": "^8.5.1", 50 | "lodash": "^4.17.21", 51 | "mongoose": "^5.12.11", 52 | "mongoose-intl": "^3.2.0", 53 | "morgan": "^1.10.0", 54 | "multer": "^1.4.2", 55 | "nodemailer": "^6.6.0", 56 | "request-ip": "^2.1.3", 57 | "sharp": "^0.28.2", 58 | "xss-clean": "^0.1.1" 59 | }, 60 | "devDependencies": { 61 | "@types/node": "^15.6.0", 62 | "jest": "^27.0.4", 63 | "supertest": "^6.1.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binSaed/e-commerce-backend-nodejs/c54c503b26c418501c7059cbb74a5922a2df0125/public/favicon.ico -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const errorHandler = require("./routes/helper/error_handler"); 2 | const express = require("express"); 3 | const routers = require("./routes/routes"); 4 | 5 | const app = express(); 6 | require("./middlewares/middlewares")(app); 7 | 8 | app.use(routers); 9 | app.use(errorHandler); 10 | 11 | require("./db/mongoose_connect")() 12 | .then((value) => { 13 | if (process.env.NODE_ENV !== "test") console.log(value); 14 | }) 15 | .catch((reason) => console.log(reason)); 16 | //connect to DB first then run the app 17 | module.exports = app; 18 | -------------------------------------------------------------------------------- /src/constants/constant.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.freeze({ 2 | USER_ENUM: { 3 | USER: "user", 4 | MODERATOR: "moderator", 5 | ADMIN: "admin", 6 | OWNER: "owner", 7 | }, 8 | USER_TYPE: ["user", "moderator", "admin", "owner"], 9 | PERMISSIONS: { 10 | addCategory: ["moderator", "admin", "owner"], 11 | addItem: ["moderator", "admin", "owner"], 12 | addItemToCategory: ["moderator", "admin", "owner"], 13 | }, 14 | PHONE_LOCAL: ["ar-EG", "ar-SA"], 15 | LANGUAGES: ["en", "ar"], 16 | DEFAULT_LANGUAGE: "en", 17 | NAME_MIN_LENGTH: 3, 18 | NAME_MAX_LENGTH: 25, 19 | PASSWORD_MIN_LENGTH: 6, 20 | PASSWORD_MAX_LENGTH: 25, 21 | RANDOM_CODE_LENGTH: 6, 22 | MAX_IMAGES_IN_ITEM: 4, 23 | }); 24 | -------------------------------------------------------------------------------- /src/controllers/auth/auth.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcrypt"); 2 | const _ = require("lodash"); 3 | const User = require("../../models/user"); 4 | const Image = require("../../models/image"); 5 | const errorThrower = require("../../utils/error_thrower"); 6 | const emails = require("../../services/emails/emails"); 7 | const CONSTANT = require("../../constants/constant"); 8 | const { 9 | findByEmailOrThrowIfUserExist, 10 | findByEmailOrThrowIfUserNotExist, 11 | findByIdOrThrowIfUserNotExist, 12 | creatCredentialsTO, 13 | verifyTokenTo, 14 | getImageById, 15 | } = require("./auth_utils"); 16 | const { deleteImageById } = require("../../utils/image_util.js"); 17 | 18 | exports.login = async (req, res, next) => { 19 | try { 20 | const { email, password, fcmToken } = req.body; 21 | const user = await findByEmailOrThrowIfUserNotExist({ email: email }); 22 | const isEqual = await bcrypt.compare(password, user.password); 23 | if (!isEqual) { 24 | return errorThrower("Email or password incorrect!", 422); 25 | } 26 | if (fcmToken) { 27 | user.fcmTokens.push(fcmToken); 28 | user.save(); 29 | } 30 | const token = await user.generateJWT(); 31 | 32 | return res.jsonSuccess({ 33 | user: { 34 | ..._.pick(user, [ 35 | "_id", 36 | "name", 37 | "email", 38 | "phone", 39 | "emailVerified", 40 | "userType", 41 | ]), 42 | image: await getImageById({ id: user.image }), 43 | }, 44 | token, 45 | }); 46 | } catch (e) { 47 | next(e); 48 | } 49 | }; 50 | exports.register = async (req, res, next) => { 51 | try { 52 | const { name, email, phone, password, fcmToken } = req.body; 53 | await findByEmailOrThrowIfUserExist({ email: email }); 54 | const fcmTokens = fcmToken ? [fcmToken] : []; 55 | const user = new User({ name, email, phone, password, fcmTokens }); 56 | await user.save(); 57 | 58 | const jwtToken = await user.generateJWT(); 59 | const { token, code } = await creatCredentialsTO({ 60 | to: "verifyEmail", 61 | email: user.email, 62 | userID: user._id, 63 | }); 64 | res.jsonSuccess({ 65 | user: { 66 | ..._.pick(user, ["_id", "name", "email", "phone", "emailVerified"]), 67 | }, 68 | token: jwtToken, 69 | confirmEmailToken: token, 70 | }); 71 | await emails.sendConfirmEmail({ 72 | to: email, 73 | name, 74 | ConfirmEmailToken: token, 75 | code, 76 | }); 77 | } catch (e) { 78 | next(e); 79 | } 80 | }; 81 | exports.profile = async (req, res, next) => { 82 | try { 83 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId }); 84 | return res.jsonSuccess({ 85 | user: { 86 | ..._.pick(user, [ 87 | "_id", 88 | "name", 89 | "email", 90 | "phone", 91 | "emailVerified", 92 | "userType", 93 | ]), 94 | image: await getImageById({ id: user.image }), 95 | }, 96 | }); 97 | } catch (e) { 98 | next(e); 99 | } 100 | }; 101 | exports.refreshFcmToken = async (req, res, next) => { 102 | try { 103 | const { fcmToken } = req.body; 104 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId }); 105 | 106 | user.fcmTokens.push(fcmToken); 107 | user.save(); 108 | 109 | return res.jsonSuccess(); 110 | } catch (e) { 111 | next(e); 112 | } 113 | }; 114 | exports.reSendConfirmEmail = async (req, res, next) => { 115 | try { 116 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId }); 117 | 118 | const { token, code } = await creatCredentialsTO({ 119 | to: "verifyEmail", 120 | email: user.email, 121 | userID: user._id, 122 | }); 123 | res.jsonSuccess({ 124 | confirmEmailToken: token, 125 | }); 126 | await emails.sendConfirmEmail({ 127 | to: user.email, 128 | name: user.name, 129 | confirmEmailToken: token, 130 | code, 131 | }); 132 | } catch (e) { 133 | next(e); 134 | } 135 | }; 136 | exports.verifyEmail = async (req, res, next) => { 137 | try { 138 | const { code, confirmEmailToken } = req.query; 139 | 140 | const { userID, email } = await verifyTokenTo({ 141 | to: "verifyEmail", 142 | code: code, 143 | token: confirmEmailToken, 144 | }); 145 | const user = await findByEmailOrThrowIfUserNotExist({ email: email }); 146 | if (!user.emailVerified) { 147 | user.emailVerified = true; 148 | await user.save(); 149 | } 150 | return res.jsonSuccess({ 151 | user: _.pick(user, ["_id", "email", "emailVerified"]), 152 | }); 153 | } catch (e) { 154 | next(e); 155 | } 156 | }; 157 | exports.resetPassword = async (req, res, next) => { 158 | try { 159 | const { email } = req.body; 160 | const user = await findByEmailOrThrowIfUserNotExist({ email: email }); 161 | const { token, code } = await creatCredentialsTO({ 162 | to: "resetPassword", 163 | email: user.email, 164 | userID: user._id, 165 | }); 166 | res.jsonSuccess({ 167 | message: `Email sent to ${user.email}`, 168 | resetPasswordToken: token, 169 | }); 170 | await emails.sendResetPassword({ 171 | to: email, 172 | resetPasswordToken: token, 173 | code, 174 | }); 175 | } catch (e) { 176 | next(e); 177 | } 178 | }; 179 | exports.verifyResetPassword = async (req, res, next) => { 180 | try { 181 | const { code, resetPasswordToken } = req.query; 182 | 183 | const { userID, email } = await verifyTokenTo({ 184 | to: "resetPassword", 185 | code: code, 186 | token: resetPasswordToken, 187 | }); 188 | const user = await findByEmailOrThrowIfUserNotExist({ email: email }); 189 | 190 | const tokenJWT = await user.generateJWT(); 191 | return res.jsonSuccess({ token: tokenJWT }); 192 | } catch (e) { 193 | next(e); 194 | } 195 | }; 196 | exports.updateProfile = async (req, res, next) => { 197 | try { 198 | let image; 199 | const allowedUpdates = ["name", "email", "phone"]; 200 | const updatesReq = Object.keys(req.body); // ["email", "password"]; 201 | const updates = updatesReq.filter((update) => 202 | allowedUpdates.includes(update) 203 | ); 204 | 205 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId }); 206 | 207 | const file = req.file; 208 | if (file) { 209 | image = new Image({ 210 | ..._.pick(file, ["deletehash", "link", "imageHash"]), 211 | }); 212 | await image.save(); 213 | req.body.image = image._id; 214 | if (image) { 215 | deleteImageById({ id: user.image }); 216 | updates.push("image"); 217 | } 218 | } 219 | 220 | updates.forEach((update) => (user[update] = req.body[update])); 221 | 222 | await user.save(); 223 | if (!image) { 224 | image = await getImageById({ id: user.image }); 225 | } 226 | 227 | return res.jsonSuccess({ 228 | user: { 229 | ..._.pick(user, ["_id", "name", "email", "phone", "emailVerified"]), 230 | image: image, 231 | }, 232 | }); 233 | } catch (e) { 234 | next(e); 235 | } 236 | }; 237 | -------------------------------------------------------------------------------- /src/controllers/auth/auth_utils.js: -------------------------------------------------------------------------------- 1 | const User = require("../../models/user"); 2 | const Image = require("../../models/image"); 3 | const { jwtSign, jwtVerify } = require("../../utils/jwt_promise"); 4 | const errorThrower = require("../../utils/error_thrower"); 5 | 6 | exports.findByEmailOrThrowIfUserExist = async ({ email }) => { 7 | const user = await User.findOne({ email: email }); 8 | if (user) { 9 | return errorThrower("Email address already exist!", 422); 10 | } 11 | return user; 12 | }; 13 | exports.findByEmailOrThrowIfUserNotExist = async ({ email }) => { 14 | const user = await User.findOne({ email: email }); 15 | if (!user) { 16 | return errorThrower("Email address not exist!", 422); 17 | } 18 | return user; 19 | }; 20 | exports.findByIdOrThrowIfUserNotExist = async ({ id }) => { 21 | const user = await User.findById(id); 22 | if (!user) { 23 | return errorThrower("User not found!", 400); 24 | } 25 | return user; 26 | }; 27 | exports.creatCredentialsTO = async ({ to, userID, email }) => { 28 | const code = random6Digits(); 29 | const token = await jwtSign({ 30 | payload: { userID, email }, 31 | additionalSecret: `${code}${to}`, 32 | }); 33 | 34 | return { token, code }; 35 | }; 36 | exports.verifyTokenTo = async ({ to, code, token }) => { 37 | const { userID, email } = await jwtVerify(token, `${code}${to}`); 38 | return { userID, email }; 39 | }; 40 | exports.getImageById = async ({ id }) => { 41 | if (id) { 42 | const image = await Image.findById(id).select("-_id -deletehash"); 43 | if (image) { 44 | return image; 45 | } 46 | } 47 | return null; 48 | }; 49 | 50 | const random6Digits = () => Math.floor(100000 + Math.random() * 900000); 51 | -------------------------------------------------------------------------------- /src/controllers/category/category.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const Category = require("../../models/category"); 3 | const Image = require("../../models/image"); 4 | const { deleteImageById } = require("../../utils/image_util"); 5 | const { addItemToCategory } = require("../utils"); 6 | const errorThrower = require("../../utils/error_thrower"); 7 | 8 | exports.getCategories = async (req, res, next) => { 9 | try { 10 | const categories = await Category.find() 11 | .populate({ 12 | path: "image", 13 | select: "-_id -deletehash -__v", 14 | }) 15 | .select("-__v -items"); 16 | 17 | return res.jsonSuccess({ categories: categories }); 18 | } catch (e) { 19 | next(e); 20 | } 21 | }; 22 | exports.addCategory = async (req, res, next) => { 23 | let imageId; 24 | try { 25 | const { name } = req.body; 26 | const file = req.file; 27 | if (file) { 28 | const image = new Image({ 29 | ..._.pick(file, ["deletehash", "link", "imageHash"]), 30 | }); 31 | await image.save(); 32 | imageId = image._id; 33 | } 34 | const category = new Category({ name, image: imageId }); 35 | await category.save(); 36 | 37 | return res.jsonSuccess(); 38 | } catch (e) { 39 | deleteImageById({ id: imageId }); 40 | next(e); 41 | } 42 | }; 43 | 44 | exports.addItemToCategory = async (req, res, next) => { 45 | try { 46 | const { categoryId } = req.params; 47 | const { itemId } = req.body; 48 | const isAdded = await addItemToCategory({ categoryId, itemID: itemId }); 49 | if (isAdded) return res.jsonSuccess(); 50 | return errorThrower("Category not found", 404); 51 | } catch (e) { 52 | next(e); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/controllers/item/item.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const Category = require("../../models/category"); 3 | const Item = require("../../models/item"); 4 | const Image = require("../../models/image"); 5 | const { deleteImageById } = require("../../utils/image_util"); 6 | const errorThrower = require("../../utils/error_thrower"); 7 | const { addItemToCategory } = require("../utils"); 8 | 9 | exports.addItem = async (req, res, next) => { 10 | try { 11 | const files = req.files; 12 | let images = []; 13 | if (files && files.length > 0) { 14 | images = files.map( 15 | (image) => 16 | new Image({ ..._.pick(image, ["deletehash", "link", "imageHash"]) }) 17 | ); 18 | images.forEach((image) => image.save()); //save images to DB 19 | } 20 | 21 | const { 22 | title, 23 | disc, 24 | unitName, 25 | price, 26 | discount, 27 | maxQuantityInOrder, 28 | categoryId, 29 | } = req.body; 30 | 31 | const imagesIDs = images.map((image) => image._id); 32 | const item = new Item({ 33 | title, 34 | disc, 35 | units: [ 36 | { 37 | name: unitName, 38 | price, 39 | discount, 40 | maxQuantityInOrder, 41 | images: imagesIDs, 42 | }, 43 | ], 44 | }); 45 | 46 | await addItemToCategory({ categoryId: categoryId, itemID: item._id }); 47 | await item.save(); 48 | 49 | return res.jsonSuccess(); 50 | } catch (e) { 51 | next(e); 52 | } 53 | }; 54 | exports.getAllItems = async (req, res, next) => { 55 | try { 56 | const categoryId = req.params.id; 57 | const categories = await Category.findById(categoryId) 58 | .populate({ 59 | path: "items", 60 | populate: { 61 | path: "units.images", 62 | model: "Image", 63 | select: "-_id -deletehash -__v", 64 | }, 65 | }) 66 | .select("items"); 67 | if (!categories) { 68 | return errorThrower("Category not found", 404); 69 | } 70 | return res.jsonSuccess({ items: categories?.items ?? [] }); 71 | } catch (e) { 72 | next(e); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/controllers/utils.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/user"); 2 | const Item = require("../models/item"); 3 | const Category = require("../models/category"); 4 | const Image = require("../models/image"); 5 | const { jwtSign, jwtVerify } = require("../utils/jwt_promise"); 6 | const errorThrower = require("../utils/error_thrower"); 7 | 8 | exports.addItemToCategory = async ({ categoryId, itemID }) => { 9 | if (!categoryId || !itemID) return false; 10 | 11 | const category = await Category.findById(categoryId); 12 | if (!category) return false; 13 | 14 | // I am not sure we need validation for itemId 15 | // to be sure the item exists or not, for now 16 | // I insert itemId without validation 17 | //if u read this comment let me know ur opinion, Good Day 18 | 19 | category.items.push(itemID); 20 | category.save(); 21 | return true; 22 | }; 23 | -------------------------------------------------------------------------------- /src/db/mongoose_connect.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | module.exports = () => { 4 | return new Promise((resolutionFunc, rejectionFunc) => { 5 | const mongooseOptions = { 6 | useNewUrlParser: true, 7 | useCreateIndex: true, 8 | autoIndex: true, 9 | keepAlive: true, 10 | poolSize: 50, //50 people can connect at the same time 11 | bufferMaxEntries: 0, 12 | connectTimeoutMS: 10000, 13 | socketTimeoutMS: 45000, 14 | family: 4, // Use IPv4, skip trying IPv6 15 | useFindAndModify: false, 16 | useUnifiedTopology: true, 17 | }; 18 | mongoose 19 | .connect(process.env.MONGO_URL, mongooseOptions) 20 | .then((_) => 21 | resolutionFunc("mongo connected to " + process.env.MONGO_URL) 22 | ) 23 | .catch((error) => rejectionFunc("mongo not connected", error)); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const app = require("./app"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | 5 | const port = process.env.PORT || 3000; 6 | 7 | app.listen(port, () => console.log("start listen on port:" + port)); 8 | 9 | process.on("uncaughtException", (err) => { 10 | console.error(err.stack); // either logs on console or send to other server via api call. 11 | fs.appendFileSync(path.join(__dirname, "./../", "access.log"), err.stack); 12 | // process.exit(1); 13 | }); 14 | -------------------------------------------------------------------------------- /src/middlewares/accept_language.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const CONSTANT = require("../constants/constant"); 3 | 4 | const acceptLanguage = (req, res, next) => { 5 | const userLanguage = req.get("accept-language"); 6 | const acceptedLanguage = CONSTANT.LANGUAGES.includes(userLanguage); 7 | 8 | req.userLanguage = acceptedLanguage 9 | ? userLanguage 10 | : CONSTANT.DEFAULT_LANGUAGE; 11 | 12 | mongoose.setDefaultLanguage(req.userLanguage); 13 | 14 | return next(); 15 | }; 16 | module.exports = acceptLanguage; 17 | -------------------------------------------------------------------------------- /src/middlewares/async.js: -------------------------------------------------------------------------------- 1 | const asyncWrapper = (fn) => { 2 | return async (req, res, next) => { 3 | try { 4 | await fn(req, res, next); 5 | } catch (error) { 6 | next(error); 7 | } 8 | }; 9 | }; 10 | 11 | module.exports = asyncWrapper; -------------------------------------------------------------------------------- /src/middlewares/check_permissions.js: -------------------------------------------------------------------------------- 1 | const isAuth = require("../middlewares/is_auth"); 2 | const errorThrower = require("../utils/error_thrower"); 3 | const constant = require("../constants/constant"); 4 | const { jwtVerify } = require("../utils/jwt_promise"); 5 | module.exports = (action) => { 6 | return async (req, res, next) => { 7 | try { 8 | const authHeader = req.get("Authorization"); 9 | 10 | if (!authHeader) { 11 | errorThrower("Not authenticated", 401); 12 | } 13 | const token = authHeader.replace("Bearer ", ""); 14 | const decodeToken = await jwtVerify(token); 15 | 16 | const userType = decodeToken.userType; 17 | 18 | if (constant.PERMISSIONS[action].includes(userType)) { 19 | next(); 20 | } else { 21 | return errorThrower( 22 | "Token valid, but you don't have the right permission:)", 23 | 403 24 | ); 25 | } 26 | } catch (error) { 27 | next(error); 28 | } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/middlewares/cors_header.js: -------------------------------------------------------------------------------- 1 | const corsHeader = (req, res, next) => { 2 | //to accept connect to server from other domain (CORS) 3 | res.setHeader("Access-Control-Allow-Origin", "*"); //accept all domain 4 | res.setHeader("Access-Control-Allow-Methods", "GET, POST"); 5 | res.setHeader("Access-Control-Allow-Headers", "*"); //accept all headers 6 | 7 | if (req.method.toString().toLowerCase() === "options") { 8 | //fix issue with modern browser 9 | //first browser options request to check server status 10 | return res.sendStatus(200); 11 | } 12 | 13 | return next(); 14 | }; 15 | module.exports = corsHeader; 16 | -------------------------------------------------------------------------------- /src/middlewares/is_auth.js: -------------------------------------------------------------------------------- 1 | const { jwtVerify } = require("../utils/jwt_promise"); 2 | const errorThrower = require("../utils/error_thrower"); 3 | 4 | module.exports = async (req, res, next) => { 5 | const authHeader = req.get("Authorization"); 6 | 7 | if (!authHeader) { 8 | errorThrower("Not authenticated", 401); 9 | } 10 | const token = authHeader.replace("Bearer ", ""); 11 | 12 | try { 13 | const decodeToken = await jwtVerify(token); 14 | 15 | req.userId = decodeToken._id; 16 | req.userEmail = decodeToken.email; 17 | req.userType = decodeToken.userType; 18 | next(); 19 | } catch (e) { 20 | next(e); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/middlewares/is_intl.js: -------------------------------------------------------------------------------- 1 | const CONSTANT = require("../constants/constant"); 2 | const errorThrower = require("../utils/error_thrower"); 3 | module.exports = (field) => { 4 | return async (req, res, next) => { 5 | try { 6 | //in some time when i use form-data 7 | //fields come as String for this i need to parse it 8 | 9 | let input = {}; 10 | if (typeof req.body[field] === "object") { 11 | input = req.body[field]; 12 | } 13 | if (typeof req.body[field] === "string") { 14 | input = JSON.parse(req.body[field]); 15 | } 16 | // input = { "en": "test", "ar": "تجربة" } 17 | 18 | req.body[field] = input; 19 | for (let i = 0; i < CONSTANT.LANGUAGES.length; i++) { 20 | if (!input[CONSTANT.LANGUAGES[i]]) { 21 | return errorThrower( 22 | `one of ${field} languages not found=>'${CONSTANT.LANGUAGES[i]}' you must send [${CONSTANT.LANGUAGES}]`, 23 | 422 24 | ); 25 | } 26 | } 27 | next(); 28 | } catch (error) { 29 | next(error); 30 | } 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/middlewares/log_errors.js: -------------------------------------------------------------------------------- 1 | const logErrors = (err, req, res, next) => { 2 | console.error("Logger => " + err.stack); 3 | return next(err); 4 | }; 5 | module.exports = logErrors; 6 | -------------------------------------------------------------------------------- /src/middlewares/middlewares.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const auth = require("http-auth"); 3 | const authConnect = require("http-auth-connect"); 4 | const statusMonitor = require("express-status-monitor")({ path: "" }); 5 | const path = require("path"); 6 | const helmet = require("helmet"); 7 | const morgan = require("morgan"); 8 | const fs = require("fs"); 9 | const corsHeader = require("../middlewares/cors_header"); 10 | const acceptLanguage = require("../middlewares/accept_language"); 11 | const mongoSanitize = require("express-mongo-sanitize"); 12 | const xss = require("xss-clean"); 13 | const compression = require("compression"); 14 | const limiter = require("../middlewares/requests_limiter"); 15 | const requestIp = require("request-ip"); 16 | 17 | module.exports = (app) => { 18 | app.set("trust proxy", 1); 19 | app.use(statusMonitor.middleware); 20 | app.get( 21 | "/admin/statusMonitor", 22 | authConnect( 23 | auth.basic({}, (user, pass, callback) => 24 | //TODO make real auth 25 | callback(user === "user" && pass === "pass") 26 | ) 27 | ), 28 | statusMonitor.pageRoute 29 | ); 30 | app.use(limiter); 31 | app.use(compression()); 32 | app.use(mongoSanitize()); 33 | app.use(xss()); 34 | app.use(helmet()); 35 | app.use(requestIp.mw()); //req.clientIp 36 | app.use(express.urlencoded({ extended: true })); 37 | app.use(express.json({ limit: "10kb" })); 38 | app.use(express.static(path.join(__dirname, "../../", "public"))); 39 | app.use(corsHeader); 40 | app.use(acceptLanguage); 41 | require("../utils/json_success_fail")(app); 42 | if (process.env.NODE_ENV !== "test") { 43 | //not log error in test environment 44 | const accessLogStream = fs.createWriteStream( 45 | path.join(__dirname, "./../../", "access.log") 46 | ); 47 | 48 | app.use( 49 | morgan( 50 | ":remote-addr __ :method __ HTTP/:http-version __ :url __ :status __ :res[content-length] __ :req[header] __ :response-time ms __ :date[iso]", 51 | { stream: accessLogStream } 52 | ) 53 | ); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/middlewares/param_validator.js: -------------------------------------------------------------------------------- 1 | const { body, param, query, check } = require("express-validator"); 2 | const isMongoId = require("../utils/is_mongo_id"); 3 | const CONSTANT = require("../constants/constant"); 4 | const bodyPrice = body("price") 5 | .isInt({ gt: 0 }) 6 | .withMessage("must be a more than 0."); 7 | const bodyEmail = body("email") 8 | .trim() 9 | .isEmail() 10 | .normalizeEmail() 11 | .withMessage("must be a valid email."); 12 | 13 | const bodyPassword = body("password") 14 | .trim() 15 | .isLength({ 16 | min: CONSTANT.PASSWORD_MIN_LENGTH, 17 | max: CONSTANT.PASSWORD_MAX_LENGTH, 18 | }) 19 | .withMessage( 20 | `length must be between ${CONSTANT.PASSWORD_MIN_LENGTH},${CONSTANT.PASSWORD_MAX_LENGTH}` 21 | ); 22 | const bodyFcmToken = body("fcmToken") 23 | .isString() 24 | .isLength({ min: 8 }) 25 | .withMessage("must be a valid."); 26 | const bodyName = body("name") 27 | .trim() 28 | .isLength({ 29 | min: CONSTANT.NAME_MIN_LENGTH, 30 | max: CONSTANT.NAME_MAX_LENGTH, 31 | }) 32 | .withMessage( 33 | `length must be between ${CONSTANT.NAME_MIN_LENGTH},${CONSTANT.NAME_MAX_LENGTH}` 34 | ); 35 | const bodyPhone = body("phone") 36 | .trim() 37 | .isMobilePhone(CONSTANT.PHONE_LOCAL) 38 | .withMessage(`must be ${CONSTANT.PHONE_LOCAL}`); 39 | const bodyUserType = body("userType") 40 | .trim() 41 | .notEmpty() 42 | .custom((user) => CONSTANT.USER_TYPE.includes(user)); 43 | const queryRandomCode = query("code") 44 | .isLength({ 45 | max: CONSTANT.RANDOM_CODE_LENGTH, 46 | min: CONSTANT.RANDOM_CODE_LENGTH, 47 | }) 48 | .withMessage(`length must be ${CONSTANT.RANDOM_CODE_LENGTH} digits`) 49 | .trim() 50 | .isInt(); 51 | 52 | const queryToken = query("token").isJWT().withMessage(`is not valid`); 53 | const queryConfirmEmailToken = query("confirmEmailToken") 54 | .isJWT() 55 | .withMessage(`is not valid`); 56 | const queryResetPasswordToken = query("resetPasswordToken") 57 | .isJWT() 58 | .withMessage(`is not valid`); 59 | const paramId = param("id").trim().custom(isMongoId); 60 | const paramMongoId = (fieldName) => param(fieldName).trim().custom(isMongoId); 61 | const bodyMongoId = (fieldName) => body(fieldName).trim().custom(isMongoId); 62 | const queryMongoId = (fieldName) => query(fieldName).trim().custom(isMongoId); 63 | 64 | const paramValidator = { 65 | bodyName, 66 | bodyFcmToken, 67 | bodyEmail, 68 | bodyPrice, 69 | bodyPassword, 70 | bodyPhone, 71 | bodyUserType, 72 | queryRandomCode, 73 | queryToken, 74 | queryConfirmEmailToken, 75 | paramId, 76 | paramMongoId, 77 | queryResetPasswordToken, 78 | bodyMongoId, 79 | queryMongoId, 80 | }; 81 | module.exports = paramValidator; 82 | -------------------------------------------------------------------------------- /src/middlewares/requests_limiter.js: -------------------------------------------------------------------------------- 1 | const rateLimit = require("express-rate-limit"); 2 | 3 | const limiter = rateLimit({ 4 | // each user only make 100 req in 15 MIN 5 | windowMs: 2 * 60 * 1000, // 2 minutes 6 | max: 20, // limit each IP to 100 requests per windowMs 7 | message: { 8 | status: "error", 9 | message: "Too many requests, please try again later after 2 minutes.", 10 | }, 11 | }); 12 | module.exports = limiter; -------------------------------------------------------------------------------- /src/middlewares/uploader/image_uploader.js: -------------------------------------------------------------------------------- 1 | const CONSTANT = require("../../constants/constant"); 2 | const _multer = require("multer"); 3 | const acceptedMimetype = ["image/png", "image/jpeg", "image/jpg"]; 4 | 5 | const multerStorageImgur = require("./multer_storage_imgur"); 6 | 7 | const _fileFilter = (req, file, callback) => { 8 | callback(null, acceptedMimetype.includes(file.mimetype)); 9 | }; 10 | 11 | const upload = _multer({ 12 | storage: multerStorageImgur({ clientId: process.env.IMGUR_CLIENT_ID }), 13 | fileFilter: _fileFilter, 14 | }); 15 | 16 | exports.single = upload.single("image"); 17 | exports.array = upload.array("image", CONSTANT.MAX_IMAGES_IN_ITEM); 18 | -------------------------------------------------------------------------------- /src/middlewares/uploader/multer_storage_imgur.js: -------------------------------------------------------------------------------- 1 | const imgur = require("imgur"); 2 | const concat = require("concat-stream"); 3 | const sharp = require("sharp"); 4 | const { encodeImageToBlurHash } = require("../../utils/image_util"); 5 | 6 | function setupImgurStorage(opts = {}) { 7 | if (!opts.clientId) throw new Error("Missing client id"); 8 | imgur.setClientId(opts.clientId); 9 | 10 | async function _handleFile(req, file, cb) { 11 | if (!file.mimetype || !file.mimetype.match(/image/gi)) { 12 | return cb(new Error("File is not of image type")); 13 | } 14 | const resize = sharp() 15 | .resize({ width: 500 }) 16 | .jpeg({ quality: 60, palette: true }); 17 | file.stream.pipe(resize).pipe( 18 | concat((data) => { 19 | imgur 20 | ._imgurRequest("upload", data, {}) 21 | .then((json) => { 22 | const jsonLink = json.link; 23 | if (!(json && jsonLink)) { 24 | return cb(new Error("File is not uploaded")); 25 | } 26 | 27 | const factor = json.width / json.height; 28 | 29 | encodeImageToBlurHash(data, factor) 30 | .then((imageHash) => { 31 | cb(null, { ...json, imageHash: imageHash ?? "" }); 32 | }) 33 | .catch(cb); 34 | }) 35 | .catch(cb); 36 | }) 37 | ); 38 | } 39 | 40 | function _removeFile() {} 41 | 42 | return { _handleFile, _removeFile }; 43 | } 44 | 45 | module.exports = function (opts) { 46 | return setupImgurStorage(opts); 47 | }; 48 | -------------------------------------------------------------------------------- /src/models/category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const { Schema, Types, model } = require("mongoose"); 3 | const _ = require("lodash"); 4 | const CONSTANT = require("../constants/constant"); 5 | 6 | const mongooseIntl = require("mongoose-intl"); 7 | 8 | // r we need to know who creates the category? it depends. HaHaHa 9 | // I need to stop over-engineering and end this project ASAP 10 | const categorySchema = new Schema( 11 | { 12 | name: { 13 | type: String, 14 | intl: true, 15 | required: true, 16 | requiredAll: true, 17 | trim: true, 18 | unique: true, 19 | }, 20 | image: { 21 | type: Types.ObjectId, 22 | ref: "Image", 23 | default: null, 24 | required: false, 25 | }, 26 | items: { 27 | type: [ 28 | { 29 | type: Types.ObjectId, 30 | ref: "Item", 31 | }, 32 | ], 33 | default: [], 34 | required: false, 35 | }, 36 | }, 37 | { 38 | timestamps: false, 39 | toJSON: { 40 | virtuals: true, 41 | }, 42 | } 43 | ); 44 | categorySchema.plugin(mongooseIntl, { 45 | languages: CONSTANT.LANGUAGES, 46 | defaultLanguage: CONSTANT.DEFAULT_LANGUAGE, 47 | }); 48 | categorySchema.pre("save", async function (next) { 49 | const category = this; 50 | // i used uniqBy instead of uniq 51 | // because ObjectId("foo") == ObjectId("foo"); => false 52 | //the solve for this problem is to convert to string 53 | category.items = _.uniqBy(category.items, (id) => id.toString()); 54 | next(); 55 | }); 56 | module.exports = mongoose.model("Category", categorySchema); 57 | -------------------------------------------------------------------------------- /src/models/image.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const imageSchema = new Schema( 4 | { 5 | link: { 6 | type: String, 7 | required: true, 8 | index: true, 9 | }, 10 | deletehash: { 11 | type: String, 12 | required: true, 13 | }, 14 | imageHash: { 15 | type: String, 16 | default: "", 17 | }, 18 | }, 19 | { 20 | timestamps: false, 21 | } 22 | ); 23 | module.exports = mongoose.model("Image", imageSchema); 24 | -------------------------------------------------------------------------------- /src/models/item.js: -------------------------------------------------------------------------------- 1 | const CONSTANT = require("../constants/constant"); 2 | const { Schema, Types, model } = require("mongoose"); 3 | const mongooseIntl = require("mongoose-intl"); 4 | 5 | //add a new field called deletedAt to make a soft delete 6 | 7 | const itemSchema = new Schema( 8 | { 9 | title: { 10 | type: String, 11 | intl: true, 12 | requiredAll: true, 13 | }, 14 | disc: { 15 | type: String, 16 | intl: true, 17 | requiredAll: true, 18 | }, 19 | units: [ 20 | //https://www.fakahany.com/ar/item/view/228 21 | { 22 | _id: { 23 | type: Types.ObjectId, 24 | required: true, 25 | auto: true, 26 | }, 27 | name: { 28 | type: String, 29 | intl: true, 30 | requiredAll: true, 31 | }, 32 | images: { 33 | type: [ 34 | { 35 | type: Types.ObjectId, 36 | ref: "Image", 37 | }, 38 | ], 39 | default: [], 40 | required: false, 41 | }, 42 | price: { 43 | type: Number, 44 | required: true, 45 | }, 46 | discount: { 47 | type: Number, 48 | required: false, 49 | default: 0, 50 | }, 51 | minQuantityInOrder: { 52 | type: Number, 53 | default: 1, 54 | // min Quantity user can Order 55 | }, 56 | maxQuantityInOrder: { 57 | type: Number, 58 | default: null, 59 | // max Quantity user can add to Order 60 | }, 61 | }, 62 | ], 63 | }, 64 | { 65 | timestamps: false, 66 | toJSON: { 67 | virtuals: true, 68 | }, 69 | } 70 | ); 71 | itemSchema.plugin(mongooseIntl, { 72 | languages: CONSTANT.LANGUAGES, 73 | defaultLanguage: CONSTANT.DEFAULT_LANGUAGE, 74 | }); 75 | module.exports = model("Item", itemSchema); 76 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | const { Schema, Types, model } = require("mongoose"); 2 | const { jwtSign } = require("../utils/jwt_promise"); 3 | const bcrypt = require("bcrypt"); 4 | const CONSTANT = require("../constants/constant"); 5 | const _ = require("lodash"); 6 | const { deleteImageById } = require("../utils/image_util"); 7 | 8 | const UserSchema = new Schema( 9 | { 10 | name: { 11 | type: String, 12 | required: [true, "must provide name"], 13 | maxlength: [20, "name can not be more than 20 characters"], 14 | trim: true, 15 | }, 16 | email: { 17 | type: String, 18 | required: true, 19 | unique: true, 20 | index: true, 21 | trim: true, 22 | lowercase: true, 23 | match: [ 24 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 25 | "Please provide a valid email", 26 | ], 27 | }, 28 | emailVerified: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | phone: { 33 | type: String, 34 | required: true, 35 | trim: true, 36 | }, 37 | password: { 38 | type: String, 39 | required: true, 40 | trim: true, 41 | }, 42 | 43 | userType: { 44 | type: String, 45 | enum: { 46 | values: CONSTANT.USER_TYPE, 47 | message: `wrong user type try ${CONSTANT.USER_ENUM.USER}`, 48 | }, 49 | default: CONSTANT.USER_ENUM.USER, 50 | }, 51 | image: { 52 | type: Types.ObjectId, 53 | ref: "Image", 54 | }, 55 | fcmTokens: { 56 | //TODO should be [{fcmToken, deviceId}] 57 | type: [String], 58 | default: [], 59 | required: false, 60 | }, 61 | }, 62 | { 63 | timestamps: { 64 | createdAt: true, 65 | updatedAt: false, 66 | }, 67 | } 68 | ); 69 | UserSchema.methods.generateJWT = function () { 70 | // add user ip in token 71 | return jwtSign({ 72 | payload: { 73 | _id: this._id.toString(), 74 | email: this.email, 75 | userType: this.userType, 76 | }, 77 | }); 78 | }; 79 | 80 | UserSchema.pre("save", async function (next) { 81 | const user = this; 82 | if (user.isModified("password")) { 83 | // Hash the plain text password before saving 84 | user.password = await bcrypt.hash(user.password, 6); 85 | } 86 | if (user.isModified("email")) { 87 | // set emailVerified if user changes his email 88 | user.emailVerified = false; 89 | } 90 | user.fcmTokens = _.union(user.fcmTokens); 91 | next(); 92 | }); 93 | // Delete user Image when user is removed 94 | UserSchema.pre("remove", async function (next) { 95 | const user = this; 96 | deleteImageById({ id: user.image }); 97 | next(); 98 | }); 99 | module.exports = model("User", UserSchema); 100 | -------------------------------------------------------------------------------- /src/routes/auth.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | const router = Router(); 3 | const { 4 | register, 5 | login, 6 | resetPassword, 7 | verifyResetPassword, 8 | verifyEmail, 9 | profile, 10 | updateProfile, 11 | reSendConfirmEmail, 12 | refreshFcmToken, 13 | } = require("../controllers/auth/auth"); 14 | const uploader = require("../middlewares/uploader/image_uploader"); 15 | const isAuth = require("../middlewares/is_auth"); 16 | const paramValidator = require("../middlewares/param_validator"); 17 | const paramValidation = require("../utils/param_validation"); 18 | 19 | router.post( 20 | "/register", 21 | [ 22 | paramValidator.bodyName, 23 | paramValidator.bodyEmail, 24 | paramValidator.bodyPhone, 25 | paramValidator.bodyPassword, 26 | ], 27 | paramValidation, 28 | register 29 | ); 30 | router.post( 31 | "/login", 32 | [paramValidator.bodyEmail, paramValidator.bodyPassword], 33 | paramValidation, 34 | login 35 | ); 36 | 37 | router.put("/updateProfile", isAuth, uploader.single, updateProfile); 38 | router.post("/profile", isAuth, profile); 39 | router.post("/reSendConfirmEmail", isAuth, reSendConfirmEmail); 40 | router.get( 41 | "/verifyEmail", 42 | [paramValidator.queryRandomCode, paramValidator.queryConfirmEmailToken], 43 | paramValidation, 44 | verifyEmail 45 | ); 46 | router.post( 47 | "/resetPassword", 48 | [paramValidator.bodyEmail], 49 | paramValidation, 50 | resetPassword 51 | ); 52 | router.get( 53 | "/verifyResetPassword", 54 | [paramValidator.queryRandomCode, paramValidator.queryResetPasswordToken], 55 | paramValidation, 56 | verifyResetPassword 57 | ); 58 | router.post( 59 | "/refreshFcmToken", 60 | isAuth, 61 | [paramValidator.bodyFcmToken], 62 | paramValidation, 63 | refreshFcmToken 64 | ); 65 | 66 | module.exports = router; 67 | -------------------------------------------------------------------------------- /src/routes/category.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | const checkPermissions = require("../middlewares/check_permissions"); 3 | const isIntl = require("../middlewares/is_intl"); 4 | const imageUploader = require("../middlewares/uploader/image_uploader"); 5 | const { 6 | addCategory, 7 | addItemToCategory, 8 | getCategories, 9 | } = require("../controllers/category/category"); 10 | const paramValidator = require("../middlewares/param_validator"); 11 | const paramValidation = require("../utils/param_validation"); 12 | 13 | const router = Router(); 14 | 15 | //create category 16 | router.post( 17 | "/add", 18 | checkPermissions("addCategory"), 19 | imageUploader.single, 20 | isIntl("name"), 21 | paramValidation, 22 | addCategory 23 | ); 24 | //add an item to an existing category 25 | router.post( 26 | "/:categoryId/addItem", 27 | checkPermissions("addItemToCategory"), 28 | paramValidator.paramMongoId("categoryId"), 29 | paramValidator.bodyMongoId("itemId"), 30 | paramValidation, 31 | addItemToCategory 32 | ); 33 | router.get("/getAll", getCategories); 34 | 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /src/routes/helper/404.js: -------------------------------------------------------------------------------- 1 | const notFound = (__, res) => { 2 | return res.jsonFail( 3 | { 4 | message: "api not found", 5 | }, 6 | 404 7 | ); 8 | }; 9 | module.exports = notFound; 10 | -------------------------------------------------------------------------------- /src/routes/helper/error_handler.js: -------------------------------------------------------------------------------- 1 | const errorHandler = (err, __, res, _) => { 2 | const {statusCode, message} = err; 3 | return res.status(statusCode || 500).jsonFail({message}); 4 | }; 5 | module.exports = errorHandler; 6 | -------------------------------------------------------------------------------- /src/routes/item.js: -------------------------------------------------------------------------------- 1 | const { Router } = require("express"); 2 | const checkPermissions = require("../middlewares/check_permissions"); 3 | const isIntl = require("../middlewares/is_intl"); 4 | const paramValidator = require("../middlewares/param_validator"); 5 | const paramValidation = require("../utils/param_validation"); 6 | const imageUploader = require("../middlewares/uploader/image_uploader"); 7 | const { addItem, getAllItems } = require("../controllers/item/item"); 8 | 9 | const router = Router(); 10 | 11 | router.post( 12 | "/add", 13 | checkPermissions("addItem"), 14 | imageUploader.array, 15 | isIntl("title"), 16 | isIntl("disc"), 17 | isIntl("unitName"), 18 | paramValidator.bodyPrice, 19 | paramValidation, 20 | addItem 21 | ); 22 | 23 | router.get("/getAll/:id", paramValidator.paramId, paramValidation, getAllItems); 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /src/routes/routes.js: -------------------------------------------------------------------------------- 1 | const authRouter = require("./auth"); 2 | const categoryRouter = require("./category"); 3 | const itemRouter = require("./item"); 4 | const errorHandler = require("./helper/error_handler"); 5 | const notFound = require("./helper/404"); 6 | const logErrors = require("../middlewares/log_errors"); 7 | const {Router} = require("express"); 8 | const routers = Router(); 9 | 10 | routers.use("/api/auth", authRouter); 11 | routers.use("/api/category", categoryRouter); 12 | routers.use("/api/items", itemRouter); 13 | 14 | if (process.env.NODE_ENV !== "test") { 15 | //not log error in test environment 16 | routers.use(logErrors); 17 | } 18 | routers.use(errorHandler); 19 | 20 | routers.use(notFound); 21 | 22 | module.exports = routers; 23 | -------------------------------------------------------------------------------- /src/services/emails/email_sender.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | module.exports = ({ to, subject, html }) => { 4 | return new Promise(async (resolutionFunc, rejectionFunc) => { 5 | const transport = nodemailer.createTransport({ 6 | service: "gmail", 7 | auth: { 8 | user: process.env.EMAIL, 9 | pass: process.env.EMAIL_PASSWORD, 10 | }, 11 | }); 12 | 13 | const mailOptions = { 14 | from: `Tkamul SA <${process.env.EMAIL}>`, 15 | to, 16 | sender: "Tkamul SA", 17 | subject, 18 | html, 19 | }; 20 | 21 | transport.sendMail(mailOptions, (err, info) => { 22 | if (err) { 23 | rejectionFunc(err); 24 | } 25 | resolutionFunc(info); 26 | }); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/services/emails/emails.js: -------------------------------------------------------------------------------- 1 | const ejs = require("ejs"); 2 | const path = require("path"); 3 | 4 | const emailSender = require("./email_sender"); 5 | 6 | exports.sendConfirmEmail = async ({ to, name, confirmEmailToken, code }) => { 7 | const hostname = ""; //req.hostname 8 | const subject = "Confirm your email"; 9 | const confirmEmailLink = `${hostname}/api/auth/verifyEmail?token=${confirmEmailToken}&code=${code}`; 10 | const renderedFile = await ejs.renderFile( 11 | path.join(__dirname, "../../../", "emails/confirm_email.ejs"), 12 | { name, confirmEmailLink, code } 13 | ); 14 | await emailSender({ to, subject, html: renderedFile }); 15 | }; 16 | exports.sendResetPassword = async ({ to, resetPasswordToken, code }) => { 17 | const hostname = ""; //req.hostname 18 | const subject = "Reset password"; 19 | const resetPasswordLink = `${hostname}/api/auth/verifyResetPassword?token=${resetPasswordToken}&code=${code}`; 20 | const renderedFile = await ejs.renderFile( 21 | path.join(__dirname, "../../../", "emails/reset_password.ejs"), 22 | { resetPasswordLink, code } 23 | ); 24 | await emailSender({ to, subject, html: renderedFile }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/error_thrower.js: -------------------------------------------------------------------------------- 1 | const errorThrower = (err, statusCode) => { 2 | if (err) { 3 | const error = new Error(err.toString()); 4 | error.statusCode = statusCode; 5 | throw error; 6 | } 7 | }; 8 | module.exports = errorThrower; 9 | -------------------------------------------------------------------------------- /src/utils/image_util.js: -------------------------------------------------------------------------------- 1 | const Imgur = require("imgur"); 2 | const sharp = require("sharp"); 3 | const blurHash = require("blurhash"); 4 | const Image = require("../models/image"); 5 | 6 | // factor=width/height 7 | // factor=500/600=0.83 8 | exports.encodeImageToBlurHash = (data, factor = 1) => 9 | new Promise((resolve, reject) => { 10 | sharp(data) 11 | .raw() 12 | .ensureAlpha() 13 | .resize(Math.round(44 * factor), 44, { fit: "inside" }) 14 | .toBuffer((err, buffer, { width, height }) => { 15 | if (err) return reject(err); 16 | resolve( 17 | blurHash.encode(new Uint8ClampedArray(buffer), width, height, 4, 4) 18 | ); 19 | }); 20 | }); 21 | exports.deleteImageById = async ({ id }) => { 22 | if (id) { 23 | const image = await Image.findByIdAndDelete(id); 24 | if (image) { 25 | Imgur.deleteImage(image.deletehash); 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/is_mongo_id.js: -------------------------------------------------------------------------------- 1 | const ObjectId = require("mongoose").Types.ObjectId; 2 | const isMongoID = (id) => 3 | ObjectId.isValid(id) && new ObjectId(id).toString() === id; 4 | module.exports = isMongoID; 5 | -------------------------------------------------------------------------------- /src/utils/json_success_fail.js: -------------------------------------------------------------------------------- 1 | module.exports = (app) => { 2 | app.response.jsonSuccess = function (obj, statusCode) { 3 | return this.status(statusCode ?? 200).json({ 4 | status: true, 5 | ...obj, 6 | }); 7 | }; 8 | app.response.jsonFail = function (obj, statusCode) { 9 | return this.status(statusCode ?? 400).json({ 10 | status: false, 11 | ...obj, 12 | }); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/jwt_promise.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | exports.jwtSign = ({ 3 | payload = {}, 4 | additionalSecret = "", 5 | expiresIn = "30 days", 6 | }) => { 7 | return new Promise((resolve, reject) => { 8 | jwt.sign( 9 | payload, 10 | `${process.env.PRIVATE_SERVER_KEY}${additionalSecret}`, 11 | { expiresIn: expiresIn }, 12 | (err, token) => { 13 | if (err) { 14 | return reject(err); 15 | } 16 | resolve(token); 17 | } 18 | ); 19 | }); 20 | }; 21 | exports.jwtVerify = (token, additionalSecret = "") => { 22 | return new Promise((resolve, reject) => { 23 | jwt.verify( 24 | token, 25 | `${process.env.PRIVATE_SERVER_KEY}${additionalSecret}`, 26 | {}, 27 | (err, decodeToken) => { 28 | if (err || !decodeToken) { 29 | const error = new Error("Not authenticated"); 30 | error.statusCode = 401; 31 | return reject(error); 32 | } 33 | resolve(decodeToken); 34 | } 35 | ); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/param_validation.js: -------------------------------------------------------------------------------- 1 | const { validationResult } = require("express-validator"); 2 | const errorThrower = require("../utils/error_thrower"); 3 | 4 | module.exports = (req, res, next) => { 5 | const errors = validationResult(req) 6 | .array({ onlyFirstError: true }) 7 | .map((error) => `param ${error.param} ${error.msg}`) 8 | .join(" && "); 9 | if (errors.length > 0) { 10 | errorThrower(errors, 422); 11 | } 12 | next(); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/keys_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binSaed/e-commerce-backend-nodejs/c54c503b26c418501c7059cbb74a5922a2df0125/test/fixtures/keys_bg.png -------------------------------------------------------------------------------- /test/integration_test/404.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../src/app"); 3 | 4 | const server = request(app); 5 | 6 | describe("404", () => { 7 | test("should get 404 api not found", async (done) => { 8 | const response = await server 9 | .get("/randomRoute") 10 | .expect("Content-Type", /json/) 11 | .expect(404); 12 | 13 | const body = response.body; 14 | expect(body).toMatchObject({ status: false }); 15 | done(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/integration_test/auth/login.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const User = require("../../../src/models/user"); 4 | 5 | const server = request(app); 6 | 7 | let userData = { 8 | email: "me@abdosaed.ml", 9 | password: "abdo1234", 10 | name: "abdelrahman", 11 | phone: "01151034858", 12 | }; 13 | beforeEach(() => { 14 | userData = { 15 | email: "me@abdosaed.ml", 16 | password: "abdo1234", 17 | name: "abdelrahman", 18 | phone: "01151034858", 19 | }; 20 | }); 21 | beforeAll(async () => { 22 | await User.deleteMany(); 23 | const user = await new User(userData).save(); 24 | }); 25 | describe("login", () => { 26 | test("login should be fail when body not send", async () => { 27 | const response = await server 28 | .post("/api/auth/login") 29 | .expect("Content-Type", /json/) 30 | .expect(422); 31 | const { body } = response; 32 | expect(body.status).toBe(false); 33 | }); 34 | test("login should be fail when body with not valid pram", async () => { 35 | userData.email = "abdo.ml"; 36 | userData.password = "1234"; 37 | const response = await server 38 | .post("/api/auth/login") 39 | .send(userData) 40 | .expect("Content-Type", /json/) 41 | .expect(422); 42 | 43 | const { body } = response; 44 | 45 | expect(body.status).toBe(false); 46 | 47 | expect(body.message).toContain("email"); 48 | expect(body.message).toContain("param password"); 49 | }); 50 | test("login should be fail when email not found", async () => { 51 | userData.email = "wrongEmail"; 52 | const response = await server 53 | .post("/api/auth/login") 54 | .send(userData) 55 | .expect("Content-Type", /json/) 56 | .expect(422); 57 | 58 | const { body } = response; 59 | 60 | expect(body.status).toBe(false); 61 | }); 62 | test("login should be fail when wrong password", async () => { 63 | userData.password = "wrongPassword"; 64 | const response = await server 65 | .post("/api/auth/login") 66 | .send(userData) 67 | .expect("Content-Type", /json/) 68 | .expect(422); 69 | 70 | const { body } = response; 71 | 72 | expect(body.status).toBe(false); 73 | }); 74 | test("should login", async () => { 75 | const response = await server 76 | .post("/api/auth/login") 77 | .send(userData) 78 | .expect("Content-Type", /json/) 79 | .expect(200); 80 | 81 | const { body } = response; 82 | 83 | expect(body.status).toBe(true); 84 | 85 | expect(userData.name).toBe(body.user.name); 86 | expect(userData.email).toBe(body.user.email); 87 | expect(body.token.toString().split(".").length).toBe(3); 88 | }); 89 | 90 | test("should save fcmToken when login", async () => { 91 | const user = { ...userData, fcmToken: "token" }; 92 | 93 | const response = await server 94 | .post("/api/auth/login") 95 | .send(user) 96 | .expect("Content-Type", /json/) 97 | .expect(200); 98 | 99 | const { body } = response; 100 | 101 | expect(body.status).toBe(true); 102 | 103 | const userInDB = await User.findById(body.user._id); 104 | 105 | expect(body.status).toBe(true); 106 | 107 | expect(userInDB.fcmTokens.length).toBe(1); 108 | expect(userInDB.fcmTokens[0]).toEqual(user.fcmToken); 109 | }); 110 | test("should save fcmToken when login without duplicate", async () => { 111 | const user = { ...userData, fcmToken: "token" }; 112 | await server.post("/api/auth/login").send(user); 113 | const response = await server 114 | .post("/api/auth/login") 115 | .send(user) 116 | .expect("Content-Type", /json/) 117 | .expect(200); 118 | 119 | const { body } = response; 120 | 121 | expect(body.status).toBe(true); 122 | 123 | const userInDB = await User.findById(body.user._id); 124 | 125 | expect(body.status).toBe(true); 126 | 127 | expect(userInDB.fcmTokens.length).toBe(1); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/integration_test/auth/profile.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const User = require("../../../src/models/user"); 4 | 5 | const server = request(app); 6 | 7 | let userData = { 8 | email: "me@abdosaed.ml", 9 | password: "abdo1234", 10 | name: "abdelrahman", 11 | phone: "01151034858", 12 | }; 13 | let token; 14 | const wrongToken = () => token.replace("a", "b"); //to be wrong token 15 | beforeEach(() => { 16 | userData = { 17 | email: "me@abdosaed.ml", 18 | password: "abdo1234", 19 | name: "abdelrahman", 20 | phone: "01151034858", 21 | }; 22 | }); 23 | beforeAll(async () => { 24 | await User.deleteMany(); 25 | const user = await new User(userData).save(); 26 | token = await user.generateJWT(); 27 | }); 28 | 29 | describe("profile", () => { 30 | test("should return user profile", async () => { 31 | const response = await server 32 | .post("/api/auth/profile") 33 | .set("Authorization", `Bearer ${token}`) 34 | .expect("Content-Type", /json/) 35 | .expect(200); 36 | 37 | const body = response.body; 38 | 39 | expect(body.status).toBe(true); 40 | 41 | expect(userData.name).toBe(body.user.name); 42 | expect(userData.email).toBe(body.user.email); 43 | }); 44 | test("should fail not valid token", async (done) => { 45 | const response = await server 46 | .post("/api/auth/profile") 47 | .set("Authorization", `Bearer ${wrongToken()}`) 48 | .expect("Content-Type", /json/) 49 | .expect(401); 50 | done(); 51 | 52 | const { body } = response; 53 | 54 | expect(body.status).toBe(false); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/integration_test/auth/refreshFcmToken.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const User = require("../../../src/models/user"); 4 | 5 | const server = request(app); 6 | 7 | let userData = { 8 | email: "me@abdosaed.ml", 9 | password: "abdo1234", 10 | name: "abdelrahman", 11 | phone: "01151034858", 12 | }; 13 | let token; 14 | const wrongToken = () => token.replace("a", "b"); //to be wrong token 15 | beforeAll(async () => { 16 | await User.deleteMany(); 17 | const user = await new User(userData).save(); 18 | token = await user.generateJWT(); 19 | }); 20 | 21 | describe("refreshFcmToken", () => { 22 | test("should fail not valid token", async () => { 23 | const response = await server 24 | .post("/api/auth/refreshFcmToken") 25 | .set("Authorization", `Bearer ${wrongToken()}`) 26 | .expect("Content-Type", /json/) 27 | .expect(401); 28 | 29 | const { body } = response; 30 | 31 | expect(body.status).toBe(false); 32 | }); 33 | 34 | test("should success valid fcmToken", async () => { 35 | const response = await server 36 | .post("/api/auth/refreshFcmToken") 37 | .send({ fcmToken: "fcmToken1234" }) 38 | .set("Authorization", `Bearer ${token}`) 39 | .expect("Content-Type", /json/) 40 | .expect(200); 41 | 42 | const { body } = response; 43 | 44 | expect(body.status).toBe(true); 45 | }); 46 | 47 | test("should fail not valid fcmToken", async () => { 48 | //valid fcmToken Length up to 8 49 | const response = await server 50 | .post("/api/auth/refreshFcmToken") 51 | .set("Authorization", `Bearer ${token}`) 52 | .expect("Content-Type", /json/) 53 | .expect(422); 54 | 55 | const { body } = response; 56 | 57 | expect(body.status).toBe(false); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/integration_test/auth/register.test.js: -------------------------------------------------------------------------------- 1 | const app = require("../../../src/app"); 2 | const User = require("../../../src/models/user"); 3 | const request = require("supertest"); 4 | 5 | const server = request(app); 6 | 7 | let userData = { 8 | email: "me@abdosaed.ml", 9 | password: "abdo1234", 10 | name: "abdelrahman", 11 | phone: "01151034858", 12 | }; 13 | beforeEach(() => { 14 | userData = { 15 | email: "me@abdosaed.ml", 16 | password: "abdo1234", 17 | name: "abdelrahman", 18 | phone: "01151034858", 19 | }; 20 | }); 21 | beforeAll(async () => { 22 | await User.deleteMany(); 23 | }); 24 | afterEach(async () => { 25 | await User.deleteMany(); 26 | }); 27 | 28 | describe("register", () => { 29 | test("should fail register when body not send", async () => { 30 | const response = await server 31 | .post("/api/auth/register") 32 | .expect("Content-Type", /json/) 33 | 34 | .expect(422); 35 | 36 | const { body } = response; 37 | 38 | expect(body.status).toBe(false); 39 | 40 | const emailNotValid = body.message 41 | .toString() 42 | .toLowerCase() 43 | .includes("email"); 44 | expect(emailNotValid).toBe(true); 45 | }); 46 | test("should fail register when body with not valid pram", async () => { 47 | userData.email = "abdo.ml"; 48 | userData.password = "1234"; 49 | userData.name = "al"; 50 | const response = await server 51 | .post("/api/auth/register") 52 | .send(userData) 53 | .expect("Content-Type", /json/) 54 | 55 | .expect(422); 56 | 57 | const { body } = response; 58 | 59 | expect(body.status).toBe(false); 60 | 61 | const emailNotValid = body.message 62 | .toString() 63 | .toLowerCase() 64 | .includes("email"); 65 | const nameNotValid = body.message 66 | .toString() 67 | .toLowerCase() 68 | .includes("param name"); 69 | const passwordNotValid = body.message 70 | .toString() 71 | .toLowerCase() 72 | .includes("param password"); 73 | expect(emailNotValid).toBe(true); 74 | expect(nameNotValid).toBe(true); 75 | expect(passwordNotValid).toBe(true); 76 | }); 77 | test("should register new user", async () => { 78 | const response = await server 79 | .post("/api/auth/register") 80 | .send(userData) 81 | .expect("Content-Type", /json/) 82 | .expect(200); 83 | 84 | const body = response.body; 85 | 86 | expect(body.status).toBe(true); 87 | 88 | expect(userData.name).toBe(body.user.name); 89 | expect(userData.email).toBe(body.user.email); 90 | expect(body.token.toString().split(".").length).toBe(3); 91 | }); 92 | test("should fail register when email used before", async () => { 93 | await server.post("/api/auth/register").send(userData); 94 | const response = await server 95 | .post("/api/auth/register") 96 | .send(userData) 97 | .expect("Content-Type", /json/) 98 | 99 | .expect(422); 100 | 101 | const { body } = response; 102 | 103 | expect(body.status).toBe(false); 104 | 105 | const emailNotValid = body.message 106 | .toString() 107 | .toLowerCase() 108 | .includes("email"); 109 | expect(emailNotValid).toBe(true); 110 | }); 111 | test("should not save fcmToken if it empty", async () => { 112 | const user = { ...userData }; 113 | const response = await server 114 | .post("/api/auth/register") 115 | .send(user) 116 | .expect("Content-Type", /json/) 117 | .expect(200); 118 | const { body } = response; 119 | const userInDB = await User.findById(body.user._id); 120 | 121 | expect(body.status).toBe(true); 122 | 123 | expect(userInDB.fcmTokens.length).toBe(0); 124 | }); 125 | test("should save fcmToken if it not empty", async () => { 126 | const user = { ...userData, fcmToken: "token" }; 127 | const response = await server 128 | .post("/api/auth/register") 129 | .send(user) 130 | .expect("Content-Type", /json/) 131 | .expect(200); 132 | const { body } = response; 133 | const userInDB = await User.findById(body.user._id); 134 | 135 | expect(body.status).toBe(true); 136 | 137 | expect(userInDB.fcmTokens.length).toBe(1); 138 | expect(userInDB.fcmTokens[0]).toEqual(user.fcmToken); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /test/integration_test/auth/resetPassword.test.js: -------------------------------------------------------------------------------- 1 | const app = require("../../../src/app"); 2 | const User = require("../../../src/models/user"); 3 | const request = require("supertest"); 4 | const emails = require("../../../src/services/emails/emails"); 5 | 6 | const server = request(app); 7 | 8 | let userData = { 9 | email: "me@abdosaed.ml", 10 | password: "abdo1234", 11 | name: "abdelrahman", 12 | phone: "01151034858", 13 | }; 14 | let token; 15 | const wrongToken = () => token.replace("a", "b"); //to be wrong token 16 | beforeEach(() => { 17 | userData = { 18 | email: "me@abdosaed.ml", 19 | password: "abdo1234", 20 | name: "abdelrahman", 21 | phone: "01151034858", 22 | }; 23 | }); 24 | beforeAll(async () => { 25 | await User.deleteMany(); 26 | const user = await new User(userData).save(); 27 | token = await user.generateJWT(); 28 | }); 29 | 30 | describe("resetPassword", () => { 31 | test("should send resetPassword to email", async () => { 32 | emails.sendResetPassword = jest.fn(); 33 | const response = await server 34 | .post("/api/auth/resetPassword") 35 | .send(userData) 36 | .expect("Content-Type", /json/) 37 | .expect(200); 38 | 39 | const { body } = response; 40 | 41 | expect(body.status).toBe(true); 42 | 43 | expect(emails.sendResetPassword).toHaveBeenCalled(); 44 | }); 45 | test("should fail and not send resetPassword if email not found", async () => { 46 | userData.email = "notfound@gmail.com"; 47 | emails.sendResetPassword = jest.fn(); 48 | const response = await server 49 | .post("/api/auth/resetPassword") 50 | .send(userData) 51 | .expect("Content-Type", /json/) 52 | .expect(422); 53 | 54 | const { body } = response; 55 | 56 | expect(body.status).toBe(false); 57 | 58 | expect(emails.sendResetPassword).toHaveBeenCalledTimes(0); 59 | }); 60 | }); 61 | describe("verifyResetPassword", () => { 62 | test("should get user token if resetPasswordToken is valid", async () => { 63 | emails.sendResetPassword = jest.fn(); 64 | await server.post("/api/auth/resetPassword").send(userData); 65 | const { resetPasswordToken, code } = 66 | emails.sendResetPassword.mock.calls[0][0]; 67 | 68 | const response = await server 69 | .get( 70 | `/api/auth/verifyResetPassword?resetPasswordToken=${resetPasswordToken}&code=${code}` 71 | ) 72 | .expect("Content-Type", /json/) 73 | .expect(200); 74 | const { body } = response; 75 | expect(emails.sendResetPassword).toBeCalled(); 76 | expect(body.status).toBe(true); 77 | expect(body.token).not.toBeNull(); 78 | }); 79 | test("should fail with wrong email", async () => {}); 80 | test("should fail with wrong token and code", async () => { 81 | emails.sendResetPassword = jest.fn(); 82 | await server.post("/api/auth/resetPassword").send(userData); 83 | const { resetPasswordToken, code } = 84 | emails.sendResetPassword.mock.calls[0][0]; 85 | 86 | const response = await server 87 | .get( 88 | `/api/auth/verifyResetPassword?resetPasswordToken=${resetPasswordToken.replace( 89 | "a", 90 | "b" 91 | )}&code=${code}` 92 | ) 93 | .expect("Content-Type", /json/) 94 | .expect(401); 95 | const { body } = response; 96 | 97 | expect(body.status).toBe(false); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/integration_test/auth/updateProfile.test.js: -------------------------------------------------------------------------------- 1 | const app = require("../../../src/app"); 2 | const User = require("../../../src/models/user"); 3 | const request = require("supertest"); 4 | 5 | const server = request(app); 6 | 7 | let userData = { 8 | email: "me@abdosaed.ml", 9 | password: "abdo1234", 10 | name: "abdelrahman", 11 | phone: "01151034858", 12 | }; 13 | let token; 14 | const wrongToken = () => token.replace("a", "b"); //to be wrong token 15 | beforeEach(() => { 16 | userData = { 17 | email: "me@abdosaed.ml", 18 | password: "abdo1234", 19 | name: "abdelrahman", 20 | phone: "01151034858", 21 | }; 22 | }); 23 | beforeAll(async () => { 24 | await User.deleteMany(); 25 | const user = await new User(userData).save(); 26 | token = await user.generateJWT(); 27 | }); 28 | 29 | describe("updateProfile", () => { 30 | test("should update the user with valid data", async () => { 31 | const newName = "newname"; 32 | const newEmail = "newemail"; 33 | const newPhone = "newphone"; 34 | const response = await server 35 | .put("/api/auth/updateProfile") 36 | .set("Authorization", `Bearer ${token}`) 37 | .field("name", newName) 38 | .field("email", newEmail) 39 | .field("phone", newPhone) 40 | .expect("Content-Type", /json/) 41 | .expect(200); 42 | 43 | const { body } = response; 44 | expect(body.status).toBe(true); 45 | 46 | const { user } = body; 47 | expect(user.email).toEqual(newEmail); 48 | expect(user.name).toEqual(newName); 49 | expect(user.phone).toEqual(newPhone); 50 | }); 51 | test("should be fail update the user with wrong token", async () => { 52 | const response = await server 53 | .put("/api/auth/updateProfile") 54 | .set("Authorization", `Bearer ${wrongToken()}`) 55 | .expect("Content-Type", /json/) 56 | .expect(401); 57 | 58 | const { body } = response; 59 | expect(body.status).toBe(false); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/integration_test/auth/verifyEmail.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const nodemailer = require("nodemailer"); 3 | const app = require("../../../src/app"); 4 | const User = require("../../../src/models/user"); 5 | const emails = require("../../../src/services/emails/emails"); 6 | 7 | const server = request(app); 8 | 9 | let userData = { 10 | email: "me@abdosaed.ml", 11 | password: "abdo1234", 12 | name: "abdelrahman", 13 | phone: "01151034858", 14 | }; 15 | let token; 16 | const wrongToken = () => token.replace("a", "b"); //to be wrong token 17 | 18 | beforeEach(() => { 19 | userData = { 20 | email: "me@abdosaed.ml", 21 | password: "abdo1234", 22 | name: "abdelrahman", 23 | phone: "01151034858", 24 | }; 25 | }); 26 | beforeAll(async () => { 27 | await User.deleteMany(); 28 | const user = await new User(userData).save(); 29 | token = await user.generateJWT(); 30 | }); 31 | 32 | describe("reSendConfirmEmail", () => { 33 | test("should send email", async () => { 34 | const sendMail = jest.fn().mockResolvedValue("done"); 35 | nodemailer.createTransport = jest.fn().mockReturnValue({ sendMail }); 36 | const response = await server 37 | .post("/api/auth/reSendConfirmEmail") 38 | .set("Authorization", `Bearer ${token}`) 39 | .expect("Content-Type", /json/) 40 | .expect(200); 41 | 42 | const { body } = response; 43 | 44 | expect(body.status).toBe(true); 45 | expect(nodemailer.createTransport).toHaveBeenCalled(); 46 | }); 47 | test("should fail to send email not valid token", async () => { 48 | nodemailer.createTransport = jest.fn(); 49 | const response = await server 50 | .post("/api/auth/reSendConfirmEmail") 51 | .set("Authorization", `Bearer ${wrongToken}`) 52 | .expect("Content-Type", /json/) 53 | .expect(401); 54 | 55 | const { body } = response; 56 | 57 | expect(body.status).toBe(false); 58 | expect(nodemailer.createTransport).toBeCalledTimes(0); 59 | }); 60 | }); 61 | describe("verifyEmail", () => { 62 | test("should set emailVerified to true", async () => { 63 | emails.sendConfirmEmail = jest.fn(); 64 | await server 65 | .post("/api/auth/reSendConfirmEmail") 66 | .set("Authorization", `Bearer ${token}`); 67 | const { confirmEmailToken, code } = 68 | emails.sendConfirmEmail.mock.calls[0][0]; 69 | 70 | const response = await server 71 | .get( 72 | `/api/auth/verifyEmail?confirmEmailToken=${confirmEmailToken}&code=${code}` 73 | ) 74 | .expect("Content-Type", /json/) 75 | .expect(200); 76 | const { body } = response; 77 | 78 | expect(body.status).toBe(true); 79 | expect(body.user.emailVerified).toBe(true); 80 | }); 81 | test("should fail with wrong token", async () => { 82 | emails.sendConfirmEmail = jest.fn(); 83 | await server 84 | .post("/api/auth/reSendConfirmEmail") 85 | .set("Authorization", `Bearer ${wrongToken()}`) 86 | .expect(401); 87 | 88 | expect(emails.sendConfirmEmail).toBeCalledTimes(0); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/integration_test/category/addCategory.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const Category = require("../../../src/models/category"); 4 | const Item = require("../../../src/models/item"); 5 | const TokenUtils = require("../../uitils/tokenUtils"); 6 | 7 | const server = request(app); 8 | const category = { 9 | name: { en: "categoryName", ar: "categoryName" }, 10 | }; 11 | const categoryWithoutArIntel = { name: { en: "categoryName" } }; 12 | const categoryWithoutEnIntel = { name: { ar: "categoryName" } }; 13 | 14 | beforeEach(async () => { 15 | await Category.deleteMany(); 16 | await Item.deleteMany(); 17 | }); 18 | 19 | beforeAll(async () => {}); 20 | afterAll(async () => { 21 | jest.resetAllMocks(); 22 | }); 23 | 24 | describe("addCategory", () => { 25 | test("should create category when user has access", async () => { 26 | const response = await server 27 | .post(`/api/category/add`) 28 | .send(category) 29 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 30 | .expect("Content-Type", /json/) 31 | .expect(200); 32 | const body = response.body; 33 | 34 | expect(body.status).toBeTruthy(); 35 | 36 | const categories = await Category.find(); 37 | 38 | expect( 39 | JSON.stringify(category.name).includes(categories[0].name) 40 | ).toBeTruthy(); 41 | }); 42 | 43 | test("should not create category when token expired (Unauthorized)", async () => { 44 | const response = await server 45 | .post(`/api/category/add`) 46 | .send(category) 47 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`) 48 | .expect("Content-Type", /json/) 49 | .expect(401); 50 | const body = response.body; 51 | 52 | expect(body.status).toBeFalsy(); 53 | expect(body).toHaveProperty("message"); 54 | 55 | const categories = await Category.find(); 56 | expect(categories.length).toBe(0); 57 | }); 58 | test("should not create category when user hasn't access (Forbidden)", async () => { 59 | const response = await server 60 | .post(`/api/category/add`) 61 | .send(category) 62 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`) 63 | .expect("Content-Type", /json/) 64 | .expect(403); 65 | const body = response.body; 66 | 67 | expect(body.status).toBeFalsy(); 68 | expect(body).toHaveProperty("message"); 69 | 70 | const categories = await Category.find(); 71 | expect(categories.length).toBe(0); 72 | }); 73 | test("should not create category when user not send data", async () => { 74 | const response = await server 75 | .post(`/api/category/add`) 76 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 77 | .expect("Content-Type", /json/) 78 | .expect(422); 79 | const body = response.body; 80 | 81 | expect(body.status).toBeFalsy(); 82 | expect(body).toHaveProperty("message"); 83 | 84 | const categories = await Category.find(); 85 | expect(categories.length).toBe(0); 86 | }); 87 | test("should not create category when data not valid", async () => { 88 | const response1 = await server 89 | .post(`/api/category/add`) 90 | .send(categoryWithoutArIntel) 91 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 92 | .expect("Content-Type", /json/) 93 | .expect(422); 94 | const body1 = response1.body; 95 | 96 | expect(body1.status).toBeFalsy(); 97 | expect(body1).toHaveProperty("message"); 98 | 99 | const categories1 = await Category.find(); 100 | expect(categories1.length).toBe(0); 101 | 102 | const response2 = await server 103 | .post(`/api/category/add`) 104 | .send(categoryWithoutEnIntel) 105 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 106 | .expect("Content-Type", /json/) 107 | .expect(422); 108 | const body2 = response2.body; 109 | 110 | expect(body2.status).toBeFalsy(); 111 | expect(body2).toHaveProperty("message"); 112 | 113 | const categories2 = await Category.find(); 114 | expect(categories2.length).toBe(0); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/integration_test/category/addCategoryUploadImage.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const Category = require("../../../src/models/category"); 4 | const Item = require("../../../src/models/item"); 5 | const TokenUtils = require("../../uitils/tokenUtils"); 6 | 7 | jest.mock("multer", () => () => { 8 | return { 9 | array: jest.fn(() => (req, res, next) => next()), 10 | single: jest.fn(() => { 11 | return (req, res, next) => { 12 | req.body = { 13 | name: { en: "categoryName", ar: "categoryName" }, 14 | }; 15 | req.file = { 16 | imageHash: "sample.name", 17 | link: "sample.type", 18 | deletehash: "sample.url", 19 | }; 20 | return next(); 21 | }; 22 | }), 23 | }; 24 | }); 25 | 26 | const server = request(app); 27 | const category = { 28 | name: { en: "categoryName", ar: "categoryName" }, 29 | }; 30 | 31 | beforeEach(async () => { 32 | await Category.deleteMany(); 33 | await Item.deleteMany(); 34 | }); 35 | 36 | beforeAll(async () => {}); 37 | afterAll(async () => { 38 | jest.resetAllMocks(); 39 | }); 40 | 41 | describe("addCategoryUploadImage", () => { 42 | test("should create category with image if user upload image", async () => { 43 | const response = await server 44 | .post(`/api/category/add`) 45 | .field("name", JSON.stringify(category.name)) 46 | .attach("image", "test/fixtures/keys_bg.png") 47 | .set("Connection", "keep-alive") 48 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 49 | .expect("Content-Type", /json/) 50 | .expect(200); 51 | 52 | const body = response.body; 53 | 54 | expect(body.status).toBeTruthy(); 55 | }); 56 | 57 | test("should not create category when token expired (Unauthorized)", async () => { 58 | const response = await server 59 | .post(`/api/category/add`) 60 | .field("name", JSON.stringify(category.name)) 61 | .attach("image", "test/fixtures/keys_bg.png") 62 | .set("Connection", "keep-alive") 63 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`) 64 | .expect("Content-Type", /json/) 65 | .expect(401); 66 | const body = response.body; 67 | 68 | expect(body.status).toBeFalsy(); 69 | expect(body).toHaveProperty("message"); 70 | 71 | const categories = await Category.find(); 72 | expect(categories.length).toBe(0); 73 | }); 74 | test("should not create category when user hasn't access (Forbidden)", async () => { 75 | const response = await server 76 | .post(`/api/category/add`) 77 | .field("name", JSON.stringify(category.name)) 78 | .attach("image", "test/fixtures/keys_bg.png") 79 | .set("Connection", "keep-alive") 80 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`) 81 | .expect("Content-Type", /json/) 82 | .expect(403); 83 | const body = response.body; 84 | 85 | expect(body.status).toBeFalsy(); 86 | expect(body).toHaveProperty("message"); 87 | 88 | const categories = await Category.find(); 89 | expect(categories.length).toBe(0); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/integration_test/category/addItemToCategory.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const Category = require("../../../src/models/category"); 4 | const Item = require("../../../src/models/item"); 5 | const TokenUtils = require("../../uitils/tokenUtils"); 6 | 7 | const server = request(app); 8 | 9 | beforeEach(async () => { 10 | await Category.deleteMany(); 11 | await Item.deleteMany(); 12 | }); 13 | afterAll(async () => jest.resetAllMocks()); 14 | 15 | describe("addItemToCategory", () => { 16 | test("should not add item to category when token expired (Unauthorized)", async () => { 17 | const category = new Category({ 18 | name: { en: "categoryName", ar: "categoryName" }, 19 | }); 20 | await category.save(); 21 | 22 | const item = new Item({ 23 | title: { en: "itemTitle", ar: "itemTitle" }, 24 | disc: { en: "itemDisc", ar: "itemDisc" }, 25 | }); 26 | await item.save(); 27 | 28 | const response = await server 29 | .post(`/api/category/${category._id}/addItem`) 30 | .send({ itemId: item._id }) 31 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`) 32 | .expect("Content-Type", /json/) 33 | .expect(401); 34 | 35 | const body = response.body; 36 | expect(body.status).toBe(false); 37 | expect(body).toHaveProperty("message"); 38 | 39 | const categoryAfterInsert = await Category.findById(category._id); 40 | 41 | expect(categoryAfterInsert.items.includes(item._id)).toBe(false); 42 | }); 43 | test("should not add item to category when user hasn't access (Forbidden)", async () => { 44 | const category = new Category({ 45 | name: { en: "categoryName", ar: "categoryName" }, 46 | }); 47 | await category.save(); 48 | 49 | const item = new Item({ 50 | title: { en: "itemTitle", ar: "itemTitle" }, 51 | disc: { en: "itemDisc", ar: "itemDisc" }, 52 | }); 53 | await item.save(); 54 | 55 | const response = await server 56 | .post(`/api/category/${category._id}/addItem`) 57 | .send({ itemId: item._id }) 58 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`) 59 | .expect("Content-Type", /json/) 60 | .expect(403); 61 | 62 | const body = response.body; 63 | expect(body.status).toBe(false); 64 | expect(body).toHaveProperty("message"); 65 | 66 | const categoryAfterInsert = await Category.findById(category._id); 67 | 68 | expect(categoryAfterInsert.items.includes(item._id)).toBe(false); 69 | }); 70 | test("should add item to category when user has access", async () => { 71 | const category = new Category({ 72 | name: { en: "categoryName", ar: "categoryName" }, 73 | }); 74 | await category.save(); 75 | 76 | const item = new Item({ 77 | title: { en: "itemTitle", ar: "itemTitle" }, 78 | disc: { en: "itemDisc", ar: "itemDisc" }, 79 | }); 80 | await item.save(); 81 | 82 | const response = await server 83 | .post(`/api/category/${category._id}/addItem`) 84 | .send({ itemId: item._id }) 85 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 86 | .expect("Content-Type", /json/) 87 | .expect(200); 88 | const body = response.body; 89 | 90 | expect(body.status).toBe(true); 91 | 92 | const categoryAfterInsert = await Category.findById(category._id); 93 | 94 | expect(categoryAfterInsert.items.includes(item._id)).toBe(true); 95 | }); 96 | test("should return 404 with message if wrong categoryId", async () => { 97 | const item = new Item({ 98 | title: { en: "itemTitle", ar: "itemTitle" }, 99 | disc: { en: "itemDisc", ar: "itemDisc" }, 100 | }); 101 | await item.save(); 102 | 103 | const wrongCategoryId = "60a9deeb06496a08843fa4a2"; 104 | 105 | const response = await server 106 | .post(`/api/category/${wrongCategoryId}/addItem`) 107 | .send({ itemId: item._id }) 108 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 109 | .expect("Content-Type", /json/) 110 | .expect(404); 111 | 112 | const body = response.body; 113 | 114 | expect(body.status).toBe(false); 115 | expect(body).toHaveProperty("message"); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/integration_test/category/getCategories.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const Category = require("../../../src/models/category"); 4 | 5 | const server = request(app); 6 | 7 | beforeEach(() => {}); 8 | beforeAll(async () => { 9 | await Category.deleteMany(); 10 | }); 11 | 12 | describe("getCategories", () => { 13 | test("should return empty list if no Categories", async () => { 14 | const response = await server 15 | .get("/api/category/getAll") 16 | .expect("Content-Type", /json/) 17 | .expect(200); 18 | 19 | const body = response.body; 20 | 21 | expect(body.status).toBe(true); 22 | expect(body.categories).toBeInstanceOf(Array); 23 | expect(body.categories.length).toBe(0); 24 | }); 25 | test("should return list of Categories if it has", async () => { 26 | const category = new Category({ 27 | name: { en: "categoryName", ar: "categoryName" }, 28 | }); 29 | await category.save(); 30 | 31 | const response = await server 32 | .get("/api/category/getAll") 33 | .expect("Content-Type", /json/) 34 | .expect(200); 35 | 36 | const { body } = response; 37 | 38 | expect(body.status).toBe(true); 39 | expect(body.categories).toBeInstanceOf(Array); 40 | expect(body.categories.length).toBe(1); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/integration_test/item/addItem.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const Category = require("../../../src/models/category"); 4 | const Item = require("../../../src/models/item"); 5 | const TokenUtils = require("../../uitils/tokenUtils"); 6 | 7 | const server = request(app); 8 | const item = { 9 | title: { en: "sdfsdf", ar: " vfddfdf" }, 10 | disc: { en: "sdfsdf", ar: " vfddfdf" }, 11 | unitName: { en: "sdfsdf", ar: " vfddfdf" }, 12 | price: 1, 13 | categoryId: "60a94a17bddc6f1838b0e1d1", 14 | discount: "4", 15 | maxQuantityInOrder: "5", 16 | }; 17 | const itemWithoutTitleArIntel = () => { 18 | const newItem = { ...item }; 19 | newItem.title = { en: "sdfsdf" }; 20 | return newItem; 21 | }; 22 | const itemWithoutTitleEnIntel = () => { 23 | const newItem = { ...item }; 24 | newItem.title = { ar: "sdfsdf" }; 25 | return newItem; 26 | }; 27 | const itemWithoutDiscArIntel = () => { 28 | const newItem = { ...item }; 29 | newItem.disc = { en: "sdfsdf" }; 30 | return newItem; 31 | }; 32 | const itemWithoutDiscEnIntel = () => { 33 | const newItem = { ...item }; 34 | newItem.disc = { ar: "sdfsdf" }; 35 | return newItem; 36 | }; 37 | const itemWithoutUnitNameArIntel = () => { 38 | const newItem = { ...item }; 39 | newItem.unitName = { en: "sdfsdf" }; 40 | return newItem; 41 | }; 42 | const itemWithoutUnitNameEnIntel = () => { 43 | const newItem = { ...item }; 44 | newItem.unitName = { ar: "sdfsdf" }; 45 | return newItem; 46 | }; 47 | const itemWithoutPrice = () => { 48 | const newItem = { ...item }; 49 | delete newItem.price; 50 | return newItem; 51 | }; 52 | 53 | const itemWithNotValidPrice = () => { 54 | const newItem = { ...item }; 55 | newItem.price = "ss"; 56 | return newItem; 57 | }; 58 | const itemWithPriceUnderOne = () => { 59 | const newItem = { ...item }; 60 | newItem.price = 0; 61 | return newItem; 62 | }; 63 | 64 | beforeEach(async () => { 65 | await Category.deleteMany(); 66 | await Item.deleteMany(); 67 | }); 68 | 69 | beforeAll(async () => { 70 | jest.resetAllMocks(); 71 | }); 72 | afterAll(async () => {}); 73 | 74 | describe("addCategory", () => { 75 | test("should create item when user has access", async () => { 76 | const response = await server 77 | .post(`/api/items/add`) 78 | .send(item) 79 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 80 | .expect("Content-Type", /json/) 81 | .expect(200); 82 | const body = response.body; 83 | 84 | expect(body.status).toBeTruthy(); 85 | 86 | const items = await Item.find(); 87 | 88 | expect(JSON.stringify(item.title).includes(items[0].title)).toBeTruthy(); 89 | expect(JSON.stringify(item.disc).includes(items[0].disc)).toBeTruthy(); 90 | expect( 91 | JSON.stringify(item.unitName).includes(items[0].units[0].name) 92 | ).toBeTruthy(); 93 | expect(item.price).toEqual(items[0].units[0].price); 94 | }); 95 | test("should create item and add to category when user has access", async () => { 96 | const category = new Category({ 97 | name: { en: "categoryName", ar: "categoryName" }, 98 | }); 99 | await category.save(); 100 | const categoryId = category._id; 101 | 102 | const response = await server 103 | .post(`/api/items/add`) 104 | .send({ ...item, categoryId }) 105 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 106 | .expect("Content-Type", /json/) 107 | .expect(200); 108 | const body = response.body; 109 | 110 | expect(body.status).toBeTruthy(); 111 | 112 | const items = await Item.find(); 113 | const itemDB = items[0]; 114 | expect(JSON.stringify(item.title).includes(itemDB.title)).toBeTruthy(); 115 | expect(JSON.stringify(item.disc).includes(itemDB.disc)).toBeTruthy(); 116 | expect( 117 | JSON.stringify(item.unitName).includes(itemDB.units[0].name) 118 | ).toBeTruthy(); 119 | expect(item.price).toEqual(itemDB.units[0].price); 120 | 121 | const category2 = await Category.findById(categoryId); 122 | expect(category2.items[0]).toEqual(itemDB._id); 123 | }); 124 | test("should not create item when token expired (Unauthorized)", async () => { 125 | const response = await server 126 | .post(`/api/items/add`) 127 | .send(item) 128 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`) 129 | .expect("Content-Type", /json/) 130 | .expect(401); 131 | const body = response.body; 132 | 133 | expect(body.status).toBeFalsy(); 134 | expect(body).toHaveProperty("message"); 135 | 136 | const items = await Item.find(); 137 | expect(items).toHaveLength(0); 138 | }); 139 | test("should not create item when user hasn't access (Forbidden)", async () => { 140 | const response = await server 141 | .post(`/api/items/add`) 142 | .send(item) 143 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`) 144 | .expect("Content-Type", /json/) 145 | .expect(403); 146 | const body = response.body; 147 | 148 | expect(body.status).toBeFalsy(); 149 | expect(body).toHaveProperty("message"); 150 | 151 | const items = await Item.find(); 152 | expect(items.length).toBe(0); 153 | }); 154 | test("should not create item when user not send data", async () => { 155 | const response = await server 156 | .post(`/api/items/add`) 157 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 158 | .expect("Content-Type", /json/) 159 | .expect(422); 160 | const body = response.body; 161 | 162 | expect(body.status).toBeFalsy(); 163 | expect(body).toHaveProperty("message"); 164 | 165 | const items = await Item.find(); 166 | expect(items.length).toBe(0); 167 | }); 168 | test("should not create item when data not valid", async () => { 169 | const response1 = await server 170 | .post(`/api/items/add`) 171 | .send(itemWithoutTitleArIntel()) 172 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 173 | .expect("Content-Type", /json/) 174 | .expect(422); 175 | const body1 = response1.body; 176 | 177 | expect(body1.status).toBeFalsy(); 178 | expect(body1).toHaveProperty("message"); 179 | const items1 = await Item.find(); 180 | expect(items1).toHaveLength(0); 181 | 182 | const response2 = await server 183 | .post(`/api/items/add`) 184 | .send(itemWithoutTitleEnIntel()) 185 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 186 | .expect("Content-Type", /json/) 187 | .expect(422); 188 | const body2 = response2.body; 189 | 190 | expect(body2.status).toBeFalsy(); 191 | expect(body2).toHaveProperty("message"); 192 | const items2 = await Item.find(); 193 | expect(items2).toHaveLength(0); 194 | 195 | const response3 = await server 196 | .post(`/api/items/add`) 197 | .send(itemWithoutDiscArIntel()) 198 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 199 | .expect("Content-Type", /json/) 200 | .expect(422); 201 | const body3 = response3.body; 202 | 203 | expect(body3.status).toBeFalsy(); 204 | expect(body3).toHaveProperty("message"); 205 | const items3 = await Item.find(); 206 | expect(items3).toHaveLength(0); 207 | 208 | const response4 = await server 209 | .post(`/api/items/add`) 210 | .send(itemWithoutDiscEnIntel()) 211 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 212 | .expect("Content-Type", /json/) 213 | .expect(422); 214 | const body4 = response4.body; 215 | 216 | expect(body4.status).toBeFalsy(); 217 | expect(body4).toHaveProperty("message"); 218 | const items4 = await Item.find(); 219 | expect(items4).toHaveLength(0); 220 | 221 | const response5 = await server 222 | .post(`/api/items/add`) 223 | .send(itemWithoutUnitNameArIntel()) 224 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 225 | .expect("Content-Type", /json/) 226 | .expect(422); 227 | const body5 = response5.body; 228 | 229 | expect(body5.status).toBeFalsy(); 230 | expect(body5).toHaveProperty("message"); 231 | const items5 = await Item.find(); 232 | expect(items5).toHaveLength(0); 233 | 234 | const response6 = await server 235 | .post(`/api/items/add`) 236 | .send(itemWithoutUnitNameArIntel()) 237 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 238 | .expect("Content-Type", /json/) 239 | .expect(422); 240 | const body6 = response6.body; 241 | 242 | expect(body6.status).toBeFalsy(); 243 | expect(body6).toHaveProperty("message"); 244 | const items6 = await Item.find(); 245 | expect(items6).toHaveLength(0); 246 | 247 | const response7 = await server 248 | .post(`/api/items/add`) 249 | .send(itemWithoutUnitNameEnIntel()) 250 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 251 | .expect("Content-Type", /json/) 252 | .expect(422); 253 | const body7 = response7.body; 254 | 255 | expect(body7.status).toBeFalsy(); 256 | expect(body7).toHaveProperty("message"); 257 | const items7 = await Item.find(); 258 | expect(items7).toHaveLength(0); 259 | 260 | const response8 = await server 261 | .post(`/api/items/add`) 262 | .send(itemWithoutPrice()) 263 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 264 | .expect("Content-Type", /json/) 265 | .expect(422); 266 | const body8 = response8.body; 267 | 268 | expect(body8.status).toBeFalsy(); 269 | expect(body8).toHaveProperty("message"); 270 | const items8 = await Item.find(); 271 | expect(items8).toHaveLength(0); 272 | 273 | const response9 = await server 274 | .post(`/api/items/add`) 275 | .send(itemWithNotValidPrice()) 276 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 277 | .expect("Content-Type", /json/) 278 | .expect(422); 279 | const body9 = response9.body; 280 | 281 | expect(body9.status).toBeFalsy(); 282 | expect(body9).toHaveProperty("message"); 283 | const items9 = await Item.find(); 284 | expect(items9).toHaveLength(0); 285 | 286 | const response10 = await server 287 | .post(`/api/items/add`) 288 | .send(itemWithPriceUnderOne()) 289 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`) 290 | .expect("Content-Type", /json/) 291 | .expect(422); 292 | const body10 = response10.body; 293 | 294 | expect(body10.status).toBeFalsy(); 295 | expect(body10).toHaveProperty("message"); 296 | const items10 = await Item.find(); 297 | expect(items10).toHaveLength(0); 298 | }); 299 | }); 300 | -------------------------------------------------------------------------------- /test/integration_test/item/getItems.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../../../src/app"); 3 | const Category = require("../../../src/models/category"); 4 | const Item = require("../../../src/models/item"); 5 | 6 | const server = request(app); 7 | 8 | beforeEach(async () => { 9 | await Category.deleteMany(); 10 | await Item.deleteMany(); 11 | }); 12 | beforeAll(async () => { 13 | jest.resetAllMocks(); 14 | }); 15 | 16 | describe("getItems", () => { 17 | test("should return 422 with message if wrongMongoId", async () => { 18 | const wrongMongoId = "60a9fbad6c449219ggc9ec37"; 19 | const response = await server 20 | .get(`/api/items/getAll/${wrongMongoId}`) 21 | .expect("Content-Type", /json/) 22 | .expect(422); 23 | 24 | const body = response.body; 25 | 26 | expect(body.status).toBeFalsy(); 27 | expect(body).toHaveProperty("message"); 28 | }); 29 | test("should return 404 with message if notFoundCategoryID", async () => { 30 | const notFoundCategoryID = "60283da17a9e931be015420d"; 31 | const response = await server 32 | .get(`/api/items/getAll/${notFoundCategoryID}`) 33 | .expect("Content-Type", /json/) 34 | .expect(404); 35 | 36 | const body = response.body; 37 | 38 | expect(body.status).toBeFalsy(); 39 | expect(body).toHaveProperty("message"); 40 | }); 41 | test("should return list of items if category has", async () => { 42 | const category = new Category({ 43 | name: { en: "categoryName", ar: "categoryName" }, 44 | }); 45 | 46 | const item = new Item({ 47 | title: { en: "aa", ar: " bb" }, 48 | disc: { en: "aa", ar: " bb" }, 49 | units: [ 50 | { 51 | name: { en: "aa", ar: " bb" }, 52 | price: 1, 53 | }, 54 | ], 55 | }); 56 | 57 | const categoryID = category._id; 58 | const itemID = item._id; 59 | category.items.push(itemID); 60 | await item.save(); 61 | await category.save(); 62 | const response = await server 63 | .get(`/api/items/getAll/${categoryID}`) 64 | .expect("Content-Type", /json/) 65 | .expect(200); 66 | 67 | const { body } = response; 68 | 69 | expect(body.status).toBe(true); 70 | expect(body.items).toBeInstanceOf(Array); 71 | expect(body.items.length).toBe(1); 72 | }); 73 | test("should return empty list if no items", async () => { 74 | const category = new Category({ 75 | name: { en: "categoryName", ar: "categoryName" }, 76 | }); 77 | const categoryID = category._id; 78 | await category.save(); 79 | 80 | const response = await server 81 | .get(`/api/items/getAll/${categoryID}`) 82 | .expect("Content-Type", /json/) 83 | .expect(200); 84 | 85 | const { body } = response; 86 | 87 | expect(body.status).toBe(true); 88 | expect(body.items).toBeInstanceOf(Array); 89 | expect(body.items.length).toBe(0); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /test/uitils/tokenUtils.js: -------------------------------------------------------------------------------- 1 | const { jwtSign } = require("../../src/utils/jwt_promise"); 2 | 3 | let _expiredUserToken; 4 | let _expiredOwnerToken; 5 | let _userToken; 6 | let _wrongUserToken; 7 | let _ownerUserToken; 8 | 9 | exports.expiredUserToken = async () => { 10 | if (_expiredUserToken) return _expiredUserToken; 11 | 12 | _expiredUserToken = await jwtSign({ 13 | payload: { 14 | _id: "id", 15 | email: "me@abdosaed.ml", 16 | userType: "user", 17 | }, 18 | expiresIn: "0", 19 | }); 20 | return _expiredUserToken; 21 | }; 22 | exports.expiredOwnerToken = async () => { 23 | if (_expiredOwnerToken) return _expiredOwnerToken; 24 | 25 | _expiredOwnerToken = await jwtSign({ 26 | payload: { 27 | _id: "id", 28 | email: "me@abdosaed.ml", 29 | userType: "owner", 30 | }, 31 | expiresIn: "0", 32 | }); 33 | return _expiredOwnerToken; 34 | }; 35 | exports.userToken = async () => { 36 | if (_userToken) return _userToken; 37 | 38 | _userToken = await jwtSign({ 39 | payload: { 40 | _id: "id", 41 | email: "me@abdosaed.ml", 42 | userType: "user", 43 | }, 44 | }); 45 | return _userToken; 46 | }; 47 | exports.wrongUserToken = async () => { 48 | if (_wrongUserToken) return _wrongUserToken; 49 | 50 | const token = await jwtSign({ 51 | payload: { 52 | _id: "id", 53 | email: "me@abdosaed.ml", 54 | userType: "user", 55 | }, 56 | }); 57 | _wrongUserToken = token.replace("a", "b"); //to be wrong token 58 | return _wrongUserToken; 59 | }; 60 | exports.ownerUserToken = async () => { 61 | if (_ownerUserToken) return _ownerUserToken; 62 | 63 | _ownerUserToken = await jwtSign({ 64 | payload: { 65 | _id: "id", 66 | email: "me@abdosaed.ml", 67 | userType: "owner", 68 | }, 69 | }); 70 | 71 | return _ownerUserToken; 72 | }; 73 | --------------------------------------------------------------------------------