├── README.md ├── utils ├── userRoles.js ├── httpStatusText.js ├── generateJWT.js └── appError.js ├── middleware ├── asyncWrapper.js ├── allowedTo.js ├── validationSchema.js └── verfiyToken.js ├── data └── courses.js ├── models ├── course.model.js └── user.model.js ├── package.json ├── routes ├── courses.route.js └── users.route.js ├── index.js ├── controllers ├── courses.controller.js └── users.controller.js ├── .gitignore └── db.drawio /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-courses-project 2 | a simple courses project as youtube series [Leaning purpose ONLY] 3 | -------------------------------------------------------------------------------- /utils/userRoles.js: -------------------------------------------------------------------------------- 1 | const userRoles = { 2 | ADMIN: "ADMIN", 3 | USER: "USER", 4 | MANGER: "MANGER", 5 | } 6 | 7 | module.exports = userRoles; -------------------------------------------------------------------------------- /utils/httpStatusText.js: -------------------------------------------------------------------------------- 1 | const SUCCESS = "success"; 2 | const FAIL = "fail"; 3 | const ERROR = "error"; 4 | 5 | module.exports = { 6 | SUCCESS, 7 | FAIL, 8 | ERROR 9 | } 10 | -------------------------------------------------------------------------------- /middleware/asyncWrapper.js: -------------------------------------------------------------------------------- 1 | module.exports = (asyncFn) => { 2 | return (req, res, next) => { 3 | asyncFn(req, res, next).catch((err) => { 4 | next(err); 5 | }); 6 | } 7 | } -------------------------------------------------------------------------------- /utils/generateJWT.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | module.exports = async (payload) => { 4 | 5 | const token = await jwt.sign( 6 | payload, 7 | process.env.JWT_SECRET_KEY, 8 | {expiresIn: '1m'} 9 | ); 10 | 11 | return token; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /data/courses.js: -------------------------------------------------------------------------------- 1 | 2 | let courses = [ 3 | { 4 | id: 1, 5 | title: "js course", 6 | price: 1000 7 | }, 8 | { 9 | id: 2, 10 | title: "React course", 11 | price: 800 12 | } 13 | ] 14 | 15 | 16 | module.exports = { 17 | courses 18 | } 19 | -------------------------------------------------------------------------------- /middleware/allowedTo.js: -------------------------------------------------------------------------------- 1 | const appError = require("../utils/appError"); 2 | 3 | module.exports = (...roles) => { 4 | return (req, res, next) => { 5 | if(!roles.includes(req.currentUser.role)) { 6 | return next(appError.create('this role is not authorized', 401)) 7 | } 8 | next(); 9 | } 10 | } -------------------------------------------------------------------------------- /models/course.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const courseSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | required: true 7 | }, 8 | price: { 9 | type: Number, 10 | required: true 11 | } 12 | }) 13 | 14 | module.exports = mongoose.model('Course', courseSchema); -------------------------------------------------------------------------------- /utils/appError.js: -------------------------------------------------------------------------------- 1 | class AppError extends Error { 2 | constructor(){ 3 | super(); 4 | } 5 | 6 | create(message, statusCode, statusText) { 7 | this.message = message; 8 | this.statusCode = statusCode; 9 | this.statusText = statusText; 10 | return this; 11 | } 12 | } 13 | 14 | module.exports = new AppError(); -------------------------------------------------------------------------------- /middleware/validationSchema.js: -------------------------------------------------------------------------------- 1 | const { body } = require("express-validator") 2 | 3 | const validationSchema = () => { 4 | return [ 5 | body('title') 6 | .notEmpty() 7 | .withMessage("title is required") 8 | .isLength({min: 2}) 9 | .withMessage("title at least is 2 digits"), 10 | body('price') 11 | .notEmpty() 12 | .withMessage("price is required") 13 | 14 | ] 15 | } 16 | 17 | module.exports = { 18 | validationSchema 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session-5", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "run:dev": "nodemon index.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcryptjs": "^2.4.3", 14 | "cors": "^2.8.5", 15 | "dotenv": "^16.3.1", 16 | "express": "^4.18.2", 17 | "express-validator": "^7.0.1", 18 | "jsonwebtoken": "^9.0.2", 19 | "mongoose": "^7.5.0", 20 | "multer": "^1.4.5-lts.1", 21 | "validator": "^13.11.0" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^3.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /middleware/verfiyToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const httpStatusText = require('../utils/httpStatusText'); 3 | const appError = require('../utils/appError'); 4 | 5 | const verifyToken = (req, res, next) => { 6 | const authHeader = req.headers['Authorization'] || req.headers['authorization']; 7 | if(!authHeader) { 8 | const error = appError.create('token is required', 401, httpStatusText.ERROR) 9 | return next(error); 10 | } 11 | 12 | const token = authHeader.split(' ')[1]; 13 | try { 14 | 15 | const currentUser = jwt.verify(token, process.env.JWT_SECRET_KEY); 16 | req.currentUser = currentUser; 17 | next(); 18 | 19 | } catch (err) { 20 | const error = appError.create('invalid token', 401, httpStatusText.ERROR) 21 | return next(error); 22 | } 23 | 24 | } 25 | 26 | module.exports = verifyToken; -------------------------------------------------------------------------------- /routes/courses.route.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | 4 | const router = express.Router(); 5 | 6 | const courseController = require('../controllers/courses.controller'); 7 | 8 | const { validationSchema } = require('../middleware/validationSchema'); 9 | const verifyToken = require('../middleware/verfiyToken'); 10 | const userRoles = require('../utils/userRoles'); 11 | const allowedTo = require('../middleware/allowedTo'); 12 | 13 | 14 | router.route('/') 15 | .get(courseController.getAllCourses) 16 | .post(verifyToken, allowedTo(userRoles.MANGER), validationSchema(), courseController.addCourse); 17 | 18 | 19 | router.route('/:courseId') 20 | .get(courseController.getCourse) 21 | .patch(courseController.updateCourse) 22 | .delete(verifyToken, allowedTo(userRoles.ADMIN, userRoles.MANGER), courseController.deleteCourse); 23 | 24 | 25 | module.exports = router; -------------------------------------------------------------------------------- /models/user.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const validator = require('validator'); 3 | const userRoles = require('../utils/userRoles'); 4 | 5 | const userSchema = new mongoose.Schema({ 6 | firstName: { 7 | type: String, 8 | required: true 9 | }, 10 | lastName: { 11 | type: String, 12 | required: true 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | validate: [validator.isEmail , 'filed must be a valid email address'] 19 | }, 20 | password: { 21 | type: String, 22 | required: true 23 | }, 24 | token: { 25 | type: String 26 | }, 27 | role: { 28 | type: String, // ["USER", "ADMIN", "MANGER"] 29 | enum: [userRoles.USER, userRoles.ADMIN, userRoles.MANGER], 30 | default: userRoles.USER 31 | }, 32 | avatar: { 33 | type: String, 34 | default: 'uploads/profile.png' 35 | } 36 | 37 | }) 38 | 39 | module.exports = mongoose.model('User', userSchema); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | require('dotenv').config() 3 | const express = require('express'); 4 | const path = require('path'); 5 | 6 | const cors = require('cors'); 7 | 8 | 9 | const app = express(); 10 | 11 | app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); 12 | 13 | 14 | const mongoose = require('mongoose'); 15 | 16 | const httpStatusText = require('./utils/httpStatusText'); 17 | 18 | 19 | const url = process.env.MONGO_URL; 20 | 21 | mongoose.connect(url).then(() => { 22 | console.log('mongodb server started') 23 | }) 24 | 25 | app.use(cors()) 26 | app.use(express.json()); 27 | 28 | const coursesRouter = require('./routes/courses.route'); 29 | const usersRouter = require('./routes/users.route'); 30 | 31 | 32 | app.use('/api/courses', coursesRouter) // /api/courses 33 | 34 | app.use('/api/users', usersRouter) // /api/users 35 | 36 | // global middleware for not found router 37 | app.all('*', (req, res, next)=> { 38 | return res.status(404).json({ status: httpStatusText.ERROR, message: 'this resource is not available'}) 39 | }) 40 | 41 | // global error handler 42 | app.use((error, req, res, next) => { 43 | res.status(error.statusCode || 500).json({status: error.statusText || httpStatusText.ERROR, message: error.message, code: error.statusCode || 500, data: null}); 44 | }) 45 | 46 | 47 | 48 | 49 | 50 | 51 | app.listen(process.env.PORT || 4000, () => { 52 | console.log('listening on port: 4000'); 53 | }); 54 | -------------------------------------------------------------------------------- /routes/users.route.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | 4 | const router = express.Router(); 5 | 6 | const multer = require('multer'); 7 | 8 | const diskStorage = multer.diskStorage({ 9 | destination: function (req, file, cb) { 10 | cb(null, 'uploads'); 11 | }, 12 | filename: function(req, file, cb) { 13 | const ext = file.mimetype.split('/')[1]; 14 | const fileName = `user-${Date.now()}.${ext}`; 15 | cb(null, fileName); 16 | } 17 | }) 18 | 19 | const fileFilter = (req, file, cb) => { 20 | const imageType = file.mimetype.split('/')[0]; 21 | 22 | if(imageType === 'image') { 23 | return cb(null, true) 24 | } else { 25 | return cb(appError.create('file must be an image', 400), false) 26 | } 27 | } 28 | 29 | const upload = multer({ 30 | storage: diskStorage, 31 | fileFilter 32 | }) 33 | 34 | 35 | const usersController = require('../controllers/users.controller') 36 | const verifyToken = require('../middleware/verfiyToken'); 37 | const appError = require('../utils/appError'); 38 | // get all users 39 | 40 | // register 41 | 42 | // login 43 | 44 | router.route('/') 45 | .get(verifyToken, usersController.getAllUsers) 46 | 47 | router.route('/register') 48 | .post(upload.single('avatar'), usersController.register) 49 | 50 | router.route('/login') 51 | .post(usersController.login) 52 | 53 | module.exports = router; -------------------------------------------------------------------------------- /controllers/courses.controller.js: -------------------------------------------------------------------------------- 1 | 2 | const {validationResult} = require('express-validator'); 3 | const Course = require('../models/course.model'); 4 | const httpStatusText = require('../utils/httpStatusText'); 5 | const asyncWrapper = require('../middleware/asyncWrapper'); 6 | const appError = require('../utils/appError'); 7 | 8 | 9 | const getAllCourses = asyncWrapper(async (req,res) => { 10 | const query = req.query; 11 | 12 | const limit = query.limit || 10; 13 | const page = query.page || 1; 14 | const skip = (page - 1) * limit; 15 | 16 | // get all courses) from DB using Course Model 17 | const courses = await Course.find({}, {"__v": false}).limit(limit).skip(skip); 18 | 19 | res.json({ status: httpStatusText.SUCCESS, data: {courses}}); 20 | }) 21 | 22 | const getCourse = asyncWrapper( 23 | async (req, res, next) => { 24 | 25 | const course = await Course.findById(req.params.courseId); 26 | if(!course) { 27 | const error = appError.create('course not found', 404, httpStatusText.FAIL) 28 | return next(error); 29 | } 30 | return res.json({ status: httpStatusText.SUCCESS, data: {course}}); 31 | 32 | } 33 | ) 34 | 35 | const addCourse = asyncWrapper(async (req, res, next) => { 36 | const errors = validationResult(req); 37 | if(!errors.isEmpty()) { 38 | const error = appError.create(errors.array(), 400, httpStatusText.FAIL) 39 | return next(error); 40 | } 41 | 42 | const newCourse = new Course(req.body); 43 | 44 | await newCourse.save(); 45 | 46 | res.status(201).json({status: httpStatusText.SUCCESS, data: {course: newCourse}}) 47 | }) 48 | 49 | const updateCourse = asyncWrapper(async (req, res) => { 50 | const courseId = req.params.courseId; 51 | const updatedCourse = await Course.updateOne({_id: courseId}, {$set: {...req.body}}); 52 | return res.status(200).json({status: httpStatusText.SUCCESS, data: {course: updatedCourse}}) 53 | 54 | 55 | }) 56 | 57 | const deleteCourse = asyncWrapper(async (req, res) => { 58 | await Course.deleteOne({_id: req.params.courseId}); 59 | res.status(200).json({status: httpStatusText.SUCCESS, data: null}); 60 | }) 61 | 62 | module.exports = { 63 | getAllCourses, 64 | getCourse, 65 | addCourse, 66 | updateCourse, 67 | deleteCourse 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # uploads folder 133 | /uploads -------------------------------------------------------------------------------- /controllers/users.controller.js: -------------------------------------------------------------------------------- 1 | const asyncWrapper = require("../middleware/asyncWrapper"); 2 | const User = require('../models/user.model'); 3 | const httpStatusText = require('../utils/httpStatusText'); 4 | const appError = require('../utils/appError'); 5 | const bcrypt = require('bcryptjs'); 6 | const jwt = require('jsonwebtoken'); 7 | const generateJWT = require("../utils/generateJWT"); 8 | 9 | const getAllUsers = asyncWrapper(async (req,res) => { 10 | 11 | const query = req.query; 12 | 13 | const limit = query.limit || 10; 14 | const page = query.page || 1; 15 | const skip = (page - 1) * limit; 16 | 17 | // get all courses) from DB using Course Model 18 | const users = await User.find({}, {"__v": false, 'password': false}).limit(limit).skip(skip); 19 | 20 | res.json({ status: httpStatusText.SUCCESS, data: {users}}); 21 | }) 22 | 23 | 24 | const register = asyncWrapper(async (req, res, next) => { 25 | const { firstName, lastName, email, password, role } = req.body; 26 | 27 | const oldUser = await User.findOne({ email: email}); 28 | 29 | if(oldUser) { 30 | const error = appError.create('user already exists', 400, httpStatusText.FAIL) 31 | return next(error); 32 | } 33 | 34 | // password hashing 35 | const hashedPassword = await bcrypt.hash(password, 10); 36 | 37 | 38 | const newUser = new User({ 39 | firstName, 40 | lastName, 41 | email, 42 | password: hashedPassword, 43 | role, 44 | avatar: req.file.filename 45 | }) 46 | 47 | // generate JWT token 48 | const token = await generateJWT({email: newUser.email, id: newUser._id, role: newUser.role}); 49 | newUser.token = token; 50 | 51 | 52 | await newUser.save(); 53 | 54 | 55 | 56 | res.status(201).json({status: httpStatusText.SUCCESS, data: {user: newUser}}) 57 | 58 | 59 | }) 60 | 61 | 62 | const login = asyncWrapper(async (req, res, next) => { 63 | const {email, password} = req.body; 64 | 65 | if(!email && !password) { 66 | const error = appError.create('email and password are required', 400, httpStatusText.FAIL) 67 | return next(error); 68 | } 69 | 70 | const user = await User.findOne({email: email}); 71 | 72 | if(!user) { 73 | const error = appError.create('user not found', 400, httpStatusText.FAIL) 74 | return next(error); 75 | } 76 | 77 | const matchedPassword = await bcrypt.compare(password, user.password); 78 | 79 | if(user && matchedPassword) { 80 | // logged in successfully 81 | 82 | const token = await generateJWT({email: user.email, id: user._id, role: user.role}); 83 | 84 | return res.json({ status: httpStatusText.SUCCESS, data: {token}}); 85 | } else { 86 | const error = appError.create('something wrong', 500, httpStatusText.ERROR) 87 | return next(error); 88 | } 89 | 90 | }) 91 | 92 | 93 | module.exports = { 94 | getAllUsers, 95 | register, 96 | login 97 | } -------------------------------------------------------------------------------- /db.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | --------------------------------------------------------------------------------