├── .prettierignore ├── .gitignore ├── img ├── pic-02-01.png ├── pic-06-12.png ├── pic-08-05-01.png └── pic-08-05-02.png ├── .prettierrc ├── .github └── dependabot.yml ├── app └── api │ ├── middleware │ ├── async.js │ ├── logger.js │ ├── error.js │ ├── auth.js │ └── advancedResults.js │ ├── public │ └── uploads │ │ └── photo_5d725a1b7b292f5f8ceff788.jpg │ ├── utils │ ├── errorResponse.js │ ├── geocoder.js │ └── sendEmail.js │ ├── config │ ├── db.js │ └── config.env │ ├── routes │ ├── users.js │ ├── auth.js │ ├── reviews.js │ ├── courses.js │ └── bootcamps.js │ ├── package.json │ ├── controllers │ ├── users.js │ ├── reviews.js │ ├── courses.js │ ├── auth.js │ └── bootcamps.js │ ├── seeder.js │ ├── models │ ├── Review.js │ ├── Course.js │ ├── User.js │ └── Bootcamp.js │ ├── server.js │ └── _data │ ├── bootcamps.json │ ├── users.json │ ├── courses.json │ └── reviews.json ├── Readme.md └── Development.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # *.test.js 3 | # *.spec.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.~ 2 | *.log 3 | *.swp 4 | node_modules/ 5 | logs/ 6 | package-lock.json -------------------------------------------------------------------------------- /img/pic-02-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/Node.js-API-Masterclass-With-Express-MongoDB/HEAD/img/pic-02-01.png -------------------------------------------------------------------------------- /img/pic-06-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/Node.js-API-Masterclass-With-Express-MongoDB/HEAD/img/pic-06-12.png -------------------------------------------------------------------------------- /img/pic-08-05-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/Node.js-API-Masterclass-With-Express-MongoDB/HEAD/img/pic-08-05-01.png -------------------------------------------------------------------------------- /img/pic-08-05-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/Node.js-API-Masterclass-With-Express-MongoDB/HEAD/img/pic-08-05-02.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "printWidth": 80, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/app/api/' 5 | schedule: 6 | interval: 'monthly' 7 | -------------------------------------------------------------------------------- /app/api/middleware/async.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (fn) => (req, res, next) => 2 | Promise.resolve(fn(req, res, next)).catch(next); 3 | 4 | module.exports = asyncHandler; 5 | -------------------------------------------------------------------------------- /app/api/public/uploads/photo_5d725a1b7b292f5f8ceff788.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmakaka/Node.js-API-Masterclass-With-Express-MongoDB/HEAD/app/api/public/uploads/photo_5d725a1b7b292f5f8ceff788.jpg -------------------------------------------------------------------------------- /app/api/utils/errorResponse.js: -------------------------------------------------------------------------------- 1 | class ErrorResponse extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | } 6 | } 7 | 8 | module.exports = ErrorResponse; 9 | -------------------------------------------------------------------------------- /app/api/middleware/logger.js: -------------------------------------------------------------------------------- 1 | // @desc Logs request to console 2 | const logger = (req, res, next) => { 3 | console.log( 4 | `${req.method} ${req.protocol}://${req.get('host')}${req.originalUrl}` 5 | ); 6 | next(); 7 | }; 8 | 9 | module.exports = logger; 10 | -------------------------------------------------------------------------------- /app/api/utils/geocoder.js: -------------------------------------------------------------------------------- 1 | const NodeGeocoder = require('node-geocoder'); 2 | 3 | const dotenv = require('dotenv'); 4 | dotenv.config({ path: './config/config.env' }); 5 | 6 | const options = { 7 | provider: process.env.GEOCODER_PROVIDER, 8 | httpAdapter: 'https', 9 | apiKey: process.env.GEOCODER_API_KEY, 10 | formatter: null, 11 | }; 12 | 13 | const geocoder = NodeGeocoder(options); 14 | 15 | module.exports = geocoder; 16 | -------------------------------------------------------------------------------- /app/api/config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const connectDB = async () => { 4 | const conn = await mongoose.connect(process.env.MONGO_URI, { 5 | useNewUrlParser: true, 6 | useCreateIndex: true, 7 | useFindAndModify: false, 8 | useUnifiedTopology: true, 9 | }); 10 | 11 | console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline.bold); 12 | }; 13 | 14 | module.exports = connectDB; 15 | -------------------------------------------------------------------------------- /app/api/config/config.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=5000 3 | 4 | MONGO_URI=mongodb+srv://user11:password11@traversy-node-js-api-masterclass-9n706.mongodb.net/traversy-node-js-api-masterclass-db?retryWrites=true&w=majority 5 | 6 | GEOCODER_PROVIDER=mapquest 7 | GEOCODER_API_KEY=ocNtLGR548JhBOiHQsYcLq7OhdrkTiDG 8 | 9 | FILE_UPLOAD_PATH=./public/uploads 10 | MAX_FILE_UPLOAD=100000000 11 | 12 | JWT_SECRET=MySuperSecret 13 | JWT_EXPIRE=30d 14 | JWT_COOKIE_EXPIRE=30 15 | 16 | SMTP_HOST=smtp.mailtrap.io 17 | SMTP_PORT=2525 18 | SMTP_EMAIL=3a605bd583d634 19 | SMTP_PASSWORD=3b6a9161861719 20 | FROM_EMAIL=noreply@devcamper.io 21 | FROM_NAME=DevCamper -------------------------------------------------------------------------------- /app/api/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | getUsers, 4 | getUser, 5 | createUser, 6 | updateUser, 7 | deleteUser, 8 | } = require('../controllers/users'); 9 | 10 | const User = require('../models/User'); 11 | 12 | const router = express.Router({ mergeParams: true }); 13 | 14 | const advancedResults = require('../middleware/advancedResults'); 15 | const { protect, authorize } = require('../middleware/auth'); 16 | 17 | router.use(protect); 18 | router.use(authorize('admin')); 19 | 20 | router.route('/').get(advancedResults(User), getUsers).post(createUser); 21 | 22 | router.route('/:id').get(getUser).put(updateUser).delete(deleteUser); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /app/api/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | register, 4 | login, 5 | logout, 6 | getMe, 7 | forgotPassword, 8 | resetpassword, 9 | updateDetails, 10 | updatePassword, 11 | } = require('../controllers/auth'); 12 | 13 | const router = express.Router(); 14 | 15 | const { protect } = require('../middleware/auth'); 16 | 17 | router.post('/register', register); 18 | router.post('/login', login); 19 | router.get('/logout', logout); 20 | router.get('/me', protect, getMe); 21 | router.put('/updatedetails', protect, updateDetails); 22 | router.put('/updatepassword', protect, updatePassword); 23 | router.post('/forgotpassword', forgotPassword); 24 | router.put('/resetpassword/:resettoken', resetpassword); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /app/api/utils/sendEmail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | 3 | const sendEmail = async (options) => { 4 | const transporter = nodemailer.createTransport({ 5 | host: process.env.SMTP_HOST, 6 | port: process.env.SMTP_PORT, 7 | auth: { 8 | user: process.env.SMTP_EMAIL, 9 | pass: process.env.SMTP_PASSWORD, 10 | }, 11 | }); 12 | 13 | // send mail with defined transport object 14 | const message = { 15 | from: `${process.env.FROM_NAME} <${process.env.FROM_EMAIL}>`, 16 | to: options.email, 17 | subject: options.subject, 18 | text: options.message, 19 | }; 20 | 21 | const info = await transporter.sendMail(message); 22 | 23 | console.log('Message sent: %s', info.messageId); 24 | }; 25 | 26 | module.exports = sendEmail; 27 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # [Brad Traversy] Node.js API Masterclass With Express & MongoDB [ENG, 2019] 2 | 3 | **Original src:** 4 | https://github.com/bradtraversy/devcamper-api 5 | 6 |
7 | 8 | ### [Development step by step](./Development.md) 9 | 10 | --- 11 | 12 |
13 | 14 | ## How to Run 15 | 16 |
17 | 18 | ### Update Config 19 | 20 | **api/config/config.env** 21 | 22 | Need update MONGO_URI at least 23 | 24 |
25 | 26 | ### Install packages 27 | 28 | $ npm install 29 | 30 |
31 | 32 | ### Database Seeder For Bootcamps 33 | 34 | // ImportData 35 | $ node seeder -i 36 | 37 | // DestroyData 38 | $ node seeder -d 39 | 40 |
41 | 42 | ### Run Application in development mode 43 | 44 | $ npm run dev 45 | 46 |
47 | 48 | --- 49 | 50 |
51 | 52 | **Marley** 53 | 54 | Any questions on eng: https://jsdev.org/chat/ 55 | Любые вопросы на русском: https://jsdev.ru/chat/ 56 | -------------------------------------------------------------------------------- /app/api/routes/reviews.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | getReviews, 4 | getReview, 5 | addReview, 6 | updateReview, 7 | deleteReview, 8 | } = require('../controllers/reviews'); 9 | 10 | const Review = require('../models/Review'); 11 | 12 | const router = express.Router({ mergeParams: true }); 13 | 14 | const advancedResults = require('../middleware/advancedResults'); 15 | const { protect, authorize } = require('../middleware/auth'); 16 | 17 | router 18 | .route('/') 19 | .get( 20 | advancedResults(Review, { 21 | path: 'bootcamp', 22 | select: 'name description', 23 | }), 24 | getReviews 25 | ) 26 | .post(protect, authorize('user', 'admin'), addReview); 27 | 28 | router 29 | .route('/:id') 30 | .get(getReview) 31 | .put(protect, authorize('user', 'admin'), updateReview) 32 | .delete(protect, authorize('user', 'admin'), deleteReview); 33 | 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /app/api/routes/courses.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | getCourses, 4 | getCourse, 5 | addCourse, 6 | updateCourse, 7 | deleteCourse, 8 | } = require('../controllers/courses'); 9 | 10 | const Course = require('../models/Course'); 11 | 12 | const router = express.Router({ mergeParams: true }); 13 | 14 | const advancedResults = require('../middleware/advancedResults'); 15 | const { protect, authorize } = require('../middleware/auth'); 16 | 17 | router 18 | .route('/') 19 | .get( 20 | advancedResults(Course, { 21 | path: 'bootcamp', 22 | select: 'name description', 23 | }), 24 | getCourses 25 | ) 26 | .post(protect, authorize('publisher', 'admin'), addCourse); 27 | 28 | router 29 | .route('/:id') 30 | .get(getCourse) 31 | .put(protect, authorize('publisher', 'admin'), updateCourse) 32 | .delete(protect, authorize('publisher', 'admin'), deleteCourse); 33 | 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /app/api/middleware/error.js: -------------------------------------------------------------------------------- 1 | const ErrorResponse = require('../utils/errorResponse'); 2 | 3 | const errorHandler = (err, req, res, next) => { 4 | let error = { ...err }; 5 | 6 | error.message = err.message; 7 | 8 | console.log(err); 9 | 10 | if (err.name === 'CastError') { 11 | const message = `Resource not found`; 12 | error = new ErrorResponse(message, 404); 13 | } 14 | 15 | // Mongoose duplicate key 16 | if (err.code === 11000) { 17 | const message = 'Duplicate field value entered'; 18 | error = new ErrorResponse(message, 400); 19 | } 20 | 21 | // Mongoose validatoin error 22 | if (err.name === 'ValidationError') { 23 | const message = Object.values(err.errors).map((val) => val.message); 24 | error = new ErrorResponse(message, 400); 25 | } 26 | 27 | res.status(error.statusCode || 500).json({ 28 | success: false, 29 | error: error.message || 'Server Error', 30 | }); 31 | }; 32 | 33 | module.exports = errorHandler; 34 | -------------------------------------------------------------------------------- /app/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node server", 8 | "dev": "nodemon server" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.3", 15 | "colors": "^1.4.0", 16 | "cookie-parser": "^1.4.5", 17 | "cors": "^2.8.5", 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "express-fileupload": "^1.2.0", 21 | "express-mongo-sanitize": "^2.0.0", 22 | "express-rate-limit": "^5.1.3", 23 | "helmet": "^4.1.1", 24 | "hpp": "^0.2.3", 25 | "jsonwebtoken": "^8.5.1", 26 | "mongoose": "^5.10.11", 27 | "morgan": "^1.10.0", 28 | "node-geocoder": "^3.27.0", 29 | "nodemailer": "^6.4.14", 30 | "slugify": "^1.4.5", 31 | "xss-clean": "^0.1.1" 32 | }, 33 | "devDependencies": { 34 | "nodemon": "^2.0.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/routes/bootcamps.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { 3 | getBootcamps, 4 | getBootcamp, 5 | createBootcamp, 6 | updateBootcamp, 7 | deleteBootcamp, 8 | getBootcampsInRadius, 9 | bootcampPhotoUpload, 10 | } = require('../controllers/bootcamps'); 11 | 12 | const Bootcamp = require('../models/Bootcamp'); 13 | 14 | // Include other resource routers 15 | const courseRotuer = require('./courses'); 16 | const reviewRotuer = require('./reviews'); 17 | 18 | const router = express.Router(); 19 | 20 | const advancedResults = require('../middleware/advancedResults'); 21 | const { protect, authorize } = require('../middleware/auth'); 22 | 23 | // Re-route into other rosourse rotuers 24 | router.use('/:bootcampId/courses', courseRotuer); 25 | router.use('/:bootcampId/reviews', reviewRotuer); 26 | 27 | router.route('/radius/:zipcode/:distance').get(getBootcampsInRadius); 28 | 29 | router 30 | .route('/:id/photo') 31 | .put(protect, authorize('publisher', 'admin'), bootcampPhotoUpload); 32 | 33 | router 34 | .route('/') 35 | .get(advancedResults(Bootcamp, 'courses'), getBootcamps) 36 | .post(protect, authorize('publisher', 'admin'), createBootcamp); 37 | 38 | router 39 | .route('/:id') 40 | .get(getBootcamp) 41 | .put(protect, authorize('publisher', 'admin'), updateBootcamp) 42 | .delete(protect, authorize('publisher', 'admin'), deleteBootcamp); 43 | 44 | module.exports = router; 45 | -------------------------------------------------------------------------------- /app/api/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const asyncHandler = require('./async'); 3 | const ErrorResponse = require('../utils/errorResponse'); 4 | const User = require('../models/User'); 5 | 6 | // Protect routes 7 | exports.protect = asyncHandler(async (req, res, next) => { 8 | let token; 9 | 10 | if ( 11 | req.headers.authorization && 12 | req.headers.authorization.startsWith('Bearer') 13 | ) { 14 | // Set token from Bearer token in header 15 | token = req.headers.authorization.split(' ')[1]; 16 | } 17 | 18 | // Set token from cookie 19 | // else if (req.cookies.token) { 20 | // token = req.cookies.token; 21 | // } 22 | 23 | // Make sure token exists 24 | if (!token) { 25 | return next(new ErrorResponse('Not authorized to access this route', 401)); 26 | } 27 | 28 | try { 29 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 30 | req.user = await User.findById(decoded.id); 31 | next(); 32 | } catch (err) { 33 | return next(new ErrorResponse('Not authorized to access this route', 401)); 34 | } 35 | }); 36 | 37 | // Grant access to specific roles 38 | exports.authorize = (...roles) => { 39 | return (req, res, next) => { 40 | if (!roles.includes(req.user.role)) { 41 | return next( 42 | new ErrorResponse( 43 | `User role ${req.user.role} is not authorized to access this route`, 44 | 403 45 | ) 46 | ); 47 | } 48 | next(); 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /app/api/controllers/users.js: -------------------------------------------------------------------------------- 1 | const ErrorResponse = require('../utils/errorResponse'); 2 | const asyncHandler = require('../middleware/async'); 3 | const User = require('../models/User'); 4 | 5 | // @desc Get all users 6 | // @route GET /api/v1/auth/users 7 | // @access Private/Admin 8 | exports.getUsers = asyncHandler(async (req, res, next) => { 9 | res.status(200).json(res.advancedResults); 10 | }); 11 | 12 | // @desc Get single user 13 | // @route GET /api/v1/auth/users/:id 14 | // @access Private/Admin 15 | exports.getUser = asyncHandler(async (req, res, next) => { 16 | const user = await User.findById(req.params.id); 17 | 18 | return res.status(200).json({ 19 | success: true, 20 | data: user, 21 | }); 22 | }); 23 | 24 | // @desc Create user 25 | // @route POST /api/v1/auth/users/ 26 | // @access Private/Admin 27 | exports.createUser = asyncHandler(async (req, res, next) => { 28 | const user = await User.create(req.body); 29 | 30 | return res.status(201).json({ 31 | success: true, 32 | data: user, 33 | }); 34 | }); 35 | 36 | // @desc Update user 37 | // @route PUT /api/v1/auth/users/:id 38 | // @access Private/Admin 39 | exports.updateUser = asyncHandler(async (req, res, next) => { 40 | const user = await User.findByIdAndUpdate(req.params.id, req.body, { 41 | new: true, 42 | runValidators: true, 43 | }); 44 | 45 | return res.status(200).json({ 46 | success: true, 47 | data: user, 48 | }); 49 | }); 50 | 51 | // @desc Delete user 52 | // @route DELETE /api/v1/auth/users/:id 53 | // @access Private/Admin 54 | exports.deleteUser = asyncHandler(async (req, res, next) => { 55 | await User.findByIdAndDelete(req.params.id); 56 | 57 | return res.status(200).json({ 58 | success: true, 59 | data: {}, 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /app/api/middleware/advancedResults.js: -------------------------------------------------------------------------------- 1 | const advancedResults = (model, populate) => async (req, res, next) => { 2 | let query; 3 | 4 | const reqQuery = { ...req.query }; 5 | 6 | const removeFields = ['select', 'sort', 'page', 'limit']; 7 | 8 | removeFields.forEach((param) => delete reqQuery[param]); 9 | 10 | let queryStr = JSON.stringify(reqQuery); 11 | 12 | queryStr = queryStr.replace( 13 | /\b(gt|gte|lt|lte|in)\b/g, 14 | (match) => `$${match}` 15 | ); 16 | 17 | query = model.find(JSON.parse(queryStr)); 18 | 19 | if (req.query.select) { 20 | const fields = req.query.select.split(',').join(' '); 21 | query = query.select(fields); 22 | } 23 | 24 | if (req.query.sort) { 25 | const sortBy = req.query.sort.split(',').join(' '); 26 | query = query.sort(sortBy); 27 | } else { 28 | query = query.sort('-createdAt'); 29 | } 30 | 31 | // Pagination 32 | const page = parseInt(req.query.page, 10) || 1; 33 | const limit = parseInt(req.query.page, 10) || 25; 34 | const startIndex = (page - 1) * limit; 35 | const endIndex = page * limit; 36 | const total = await model.countDocuments(); 37 | 38 | query = query.skip(startIndex).limit(limit); 39 | 40 | if (populate) { 41 | query = query.populate(populate); 42 | } 43 | 44 | // Execute query 45 | const results = await query; 46 | 47 | // Pagination result 48 | const pagination = {}; 49 | 50 | if (endIndex < total) { 51 | pagination.next = { 52 | page: page + 1, 53 | limit, 54 | }; 55 | } 56 | 57 | if (startIndex > 0) { 58 | pagination.prev = { 59 | page: page - 1, 60 | limit, 61 | }; 62 | } 63 | 64 | res.advancedResults = { 65 | success: true, 66 | count: results.length, 67 | pagination, 68 | data: results, 69 | }; 70 | 71 | next(); 72 | }; 73 | 74 | module.exports = advancedResults; 75 | -------------------------------------------------------------------------------- /app/api/seeder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mongoose = require('mongoose'); 3 | const colors = require('colors'); 4 | const dotenv = require('dotenv'); 5 | 6 | dotenv.config({ path: './config/config.evn' }); 7 | 8 | const Bootcamp = require('./models/Bootcamp'); 9 | const Course = require('./models/Course'); 10 | const User = require('./models/User'); 11 | const Review = require('./models/Review'); 12 | 13 | mongoose.connect(process.env.MONGO_URI, { 14 | useNewUrlParser: true, 15 | useCreateIndex: true, 16 | useFindAndModify: false, 17 | useUnifiedTopology: true, 18 | }); 19 | 20 | // Read JSON files 21 | const bootcamps = JSON.parse( 22 | fs.readFileSync(`${__dirname}/_data/bootcamps.json`, 'utf-8') 23 | ); 24 | 25 | const courses = JSON.parse( 26 | fs.readFileSync(`${__dirname}/_data/courses.json`, 'utf-8') 27 | ); 28 | 29 | const users = JSON.parse( 30 | fs.readFileSync(`${__dirname}/_data/users.json`, 'utf-8') 31 | ); 32 | 33 | const reviews = JSON.parse( 34 | fs.readFileSync(`${__dirname}/_data/reviews.json`, 'utf-8') 35 | ); 36 | 37 | // Import into DB 38 | const importData = async () => { 39 | try { 40 | await Bootcamp.create(bootcamps); 41 | await Course.create(courses); 42 | await User.create(users); 43 | await Review.create(reviews); 44 | 45 | console.log('Data Imported...'.green.inverse); 46 | process.exit(); 47 | } catch (err) { 48 | console.error(err); 49 | } 50 | }; 51 | 52 | // Delete data 53 | const deleteData = async () => { 54 | try { 55 | await Bootcamp.deleteMany(); 56 | await Course.deleteMany(); 57 | await User.deleteMany(); 58 | await Review.deleteMany(); 59 | console.log('Data Destroyed...'.red.inverse); 60 | process.exit(); 61 | } catch (err) { 62 | console.error(err); 63 | } 64 | }; 65 | 66 | if (process.argv[2] === '-i') { 67 | importData(); 68 | } else if (process.argv[2] === '-d') { 69 | deleteData(); 70 | } 71 | -------------------------------------------------------------------------------- /app/api/models/Review.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ReviewSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | trim: true, 7 | requrired: [true, 'Please add a title for the review'], 8 | maxlength: 100, 9 | }, 10 | text: { 11 | type: String, 12 | required: [true, 'Please add some text'], 13 | }, 14 | rating: { 15 | type: Number, 16 | min: 1, 17 | max: 10, 18 | required: [true, 'Please add a rating between 1 and 10'], 19 | }, 20 | createdAt: { 21 | type: Date, 22 | default: Date.now, 23 | }, 24 | bootcamp: { 25 | type: mongoose.Schema.ObjectId, 26 | ref: 'Bootcamp', 27 | required: true, 28 | }, 29 | user: { 30 | type: mongoose.Schema.ObjectId, 31 | ref: 'User', 32 | required: true, 33 | }, 34 | }); 35 | 36 | // Prevent user from submitting more than one review per bootcamp 37 | ReviewSchema.index({ bootcamp: 1, user: 1 }, { unique: true }); 38 | 39 | // Static method to get avg rating and save 40 | ReviewSchema.statics.getAverageRating = async function (bootcampId) { 41 | const obj = await this.aggregate([ 42 | { 43 | $match: { bootcamp: bootcampId }, 44 | }, 45 | { 46 | $group: { 47 | _id: '$bootcamp', 48 | averageRating: { $avg: '$rating' }, 49 | }, 50 | }, 51 | ]); 52 | 53 | try { 54 | await this.model('Bootcamp').findByIdAndUpdate(bootcampId, { 55 | averageRating: obj[0].averageRating, 56 | }); 57 | } catch (err) { 58 | console.error(err); 59 | } 60 | }; 61 | 62 | // Call getAverageRating after save 63 | ReviewSchema.post('save', function () { 64 | this.constructor.getAverageRating(this.bootcamp); 65 | }); 66 | 67 | // Call getAverageRating before remove 68 | ReviewSchema.pre('remove', function () { 69 | this.constructor.getAverageRating(this.bootcamp); 70 | }); 71 | 72 | module.exports = mongoose.model('Review', ReviewSchema); 73 | -------------------------------------------------------------------------------- /app/api/models/Course.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const CourseSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | trim: true, 7 | requrired: [true, 'Please add a course title'], 8 | }, 9 | description: { 10 | type: String, 11 | required: [true, 'Please add a description'], 12 | }, 13 | weeks: { 14 | type: String, 15 | required: [true, 'Please add number of weeks'], 16 | }, 17 | tuition: { 18 | type: Number, 19 | required: [true, 'Please add a tuition cost'], 20 | }, 21 | minimumSkill: { 22 | type: String, 23 | required: [true, 'Please add a minimum skill'], 24 | enum: ['beginner', 'intermediate', 'advanced'], 25 | }, 26 | scholarshipAvailable: { 27 | type: Boolean, 28 | dfault: false, 29 | }, 30 | createdAt: { 31 | type: Date, 32 | default: Date.now, 33 | }, 34 | bootcamp: { 35 | type: mongoose.Schema.ObjectId, 36 | ref: 'Bootcamp', 37 | required: true, 38 | }, 39 | user: { 40 | type: mongoose.Schema.ObjectId, 41 | ref: 'User', 42 | required: true, 43 | }, 44 | }); 45 | 46 | // Static method to get avg of course tuitions 47 | CourseSchema.statics.getAverageCost = async function (bootcampId) { 48 | const obj = await this.aggregate([ 49 | { 50 | $match: { bootcamp: bootcampId }, 51 | }, 52 | { 53 | $group: { 54 | _id: '$bootcamp', 55 | averageCost: { $avg: '$tuition' }, 56 | }, 57 | }, 58 | ]); 59 | 60 | try { 61 | await this.model('Bootcamp').findByIdAndUpdate(bootcampId, { 62 | averageCost: Math.ceil(obj[0].averageCost / 10) * 10, 63 | }); 64 | } catch (err) { 65 | console.error(err); 66 | } 67 | }; 68 | 69 | // Call getAverageCost after save 70 | CourseSchema.post('save', function () { 71 | this.constructor.getAverageCost(this.bootcamp); 72 | }); 73 | 74 | // Call getAverageCost before remove 75 | CourseSchema.pre('remove', function () { 76 | this.constructor.getAverageCost(this.bootcamp); 77 | }); 78 | 79 | module.exports = mongoose.model('Course', CourseSchema); 80 | -------------------------------------------------------------------------------- /app/api/models/User.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const mongoose = require('mongoose'); 3 | const bcrypt = require('bcryptjs'); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | const UserSchema = new mongoose.Schema({ 7 | name: { 8 | type: String, 9 | required: [true, 'Please add a name'], 10 | }, 11 | email: { 12 | type: String, 13 | required: [true, 'Please add an email'], 14 | unique: true, 15 | match: [ 16 | /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 17 | 'Please add a valid email', 18 | ], 19 | }, 20 | role: { 21 | type: String, 22 | enum: ['user', 'publisher'], 23 | default: 'user', 24 | }, 25 | password: { 26 | type: String, 27 | required: [true, 'Please add a password'], 28 | minlength: 6, 29 | select: false, 30 | }, 31 | resetPasswordToken: String, 32 | resetPasswordExpire: Date, 33 | createdAt: { 34 | type: Date, 35 | default: Date.now, 36 | }, 37 | }); 38 | 39 | // Encrypt passwrod using bcrypt 40 | UserSchema.pre('save', async function (next) { 41 | if (!this.isModified('password')) { 42 | next(); 43 | } 44 | 45 | const salt = await bcrypt.genSalt(10); 46 | this.password = await bcrypt.hash(this.password, salt); 47 | }); 48 | 49 | // Sign JWT and return 50 | UserSchema.methods.getSignedJwtToken = function () { 51 | return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { 52 | expiresIn: process.env.JWT_EXPIRE, 53 | }); 54 | }; 55 | 56 | // Match user entered password to hashed password in database 57 | UserSchema.methods.matchPassword = async function (enteredPassword) { 58 | return await bcrypt.compare(enteredPassword, this.password); 59 | }; 60 | 61 | // Generate and hash password token 62 | UserSchema.methods.getResetPasswordToken = function () { 63 | // Genereate token 64 | const resetToken = crypto.randomBytes(20).toString('hex'); 65 | 66 | // Hash token and set to resetPasswordToken field 67 | this.resetPasswordToken = crypto 68 | .createHash('sha256') 69 | .update(resetToken) 70 | .digest('hex'); 71 | 72 | // Set expire 73 | this.resetPasswordExpire = Date.now() + 10 * 60 * 1000; 74 | 75 | return resetToken; 76 | }; 77 | 78 | module.exports = mongoose.model('User', UserSchema); 79 | -------------------------------------------------------------------------------- /app/api/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const dotenv = require('dotenv'); 4 | const morgan = require('morgan'); 5 | const colors = require('colors'); 6 | const fileupload = require('express-fileupload'); 7 | const cookieParser = require('cookie-parser'); 8 | const mongoSanitize = require('express-mongo-sanitize'); 9 | const xss = require('xss-clean'); 10 | const helmet = require('helmet'); 11 | const rateLimit = require('express-rate-limit'); 12 | const hpp = require('hpp'); 13 | const cors = require('cors'); 14 | const errorHandler = require('./middleware/error'); 15 | const connectDB = require('./config/db'); 16 | 17 | // Route files 18 | const bootcamps = require('./routes/bootcamps'); 19 | const courses = require('./routes/courses'); 20 | const auth = require('./routes/auth'); 21 | const users = require('./routes/users'); 22 | const reviews = require('./routes/reviews'); 23 | 24 | dotenv.config({ path: './config/config.env' }); 25 | 26 | connectDB(); 27 | 28 | const app = express(); 29 | 30 | // Body parser 31 | app.use(express.json()); 32 | 33 | // Cookie parser 34 | app.use(cookieParser()); 35 | 36 | if (process.env.NODE_ENV === 'development') { 37 | app.use(morgan('dev')); 38 | } 39 | 40 | // File uploading 41 | app.use(fileupload()); 42 | 43 | // Sanitize data 44 | app.use(mongoSanitize()); 45 | 46 | // Set security headers 47 | app.use(helmet()); 48 | 49 | // Prevent XSS attacks 50 | app.use(xss()); 51 | 52 | // Rate limiting 53 | const limiter = rateLimit({ 54 | windowsMs: 10 * 60 * 1000, // 10 mins 55 | max: 1000, 56 | }); 57 | 58 | app.use(limiter); 59 | 60 | // Prevent http param pollution 61 | app.use(hpp()); 62 | 63 | // Enable CORS 64 | app.use(cors()); 65 | 66 | // Set static folder 67 | app.use(express.static(path.join(__dirname, 'public'))); 68 | 69 | // Mount routers 70 | app.use('/api/v1/bootcamps', bootcamps); 71 | app.use('/api/v1/courses', courses); 72 | app.use('/api/v1/auth', auth); 73 | app.use('/api/v1/users', users); 74 | app.use('/api/v1/reviews', reviews); 75 | 76 | app.use(errorHandler); 77 | 78 | const PORT = process.env.PORT || 5000; 79 | 80 | const server = app.listen(PORT, () => { 81 | console.log( 82 | `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow.bold 83 | ); 84 | }); 85 | 86 | // Handle unhandled promise rejections 87 | process.on('unhandledRejection', (err, promise) => { 88 | console.log(`Error: ${err.message}`.red.bold); 89 | 90 | // Close server & exit process 91 | server.close(() => process.exit(1)); 92 | }); 93 | -------------------------------------------------------------------------------- /app/api/_data/bootcamps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5d713995b721c3bb38c1f5d0", 4 | "user": "5d7a514b5d2c12c7449be045", 5 | "name": "Devworks Bootcamp", 6 | "description": "Devworks is a full stack JavaScript Bootcamp located in the heart of Boston that focuses on the technologies you need to get a high paying job as a web developer", 7 | "website": "https://devworks.com", 8 | "phone": "(111) 111-1111", 9 | "email": "enroll@devworks.com", 10 | "address": "233 Bay State Rd Boston MA 02215", 11 | "careers": ["Web Development", "UI/UX", "Business"], 12 | "housing": true, 13 | "jobAssistance": true, 14 | "jobGuarantee": false, 15 | "acceptGi": true 16 | }, 17 | { 18 | "_id": "5d713a66ec8f2b88b8f830b8", 19 | "user": "5d7a514b5d2c12c7449be046", 20 | "name": "ModernTech Bootcamp", 21 | "description": "ModernTech has one goal, and that is to make you a rockstar developer and/or designer with a six figure salary. We teach both development and UI/UX", 22 | "website": "https://moderntech.com", 23 | "phone": "(222) 222-2222", 24 | "email": "enroll@moderntech.com", 25 | "address": "220 Pawtucket St, Lowell, MA 01854", 26 | "careers": ["Web Development", "UI/UX", "Mobile Development"], 27 | "housing": false, 28 | "jobAssistance": true, 29 | "jobGuarantee": false, 30 | "acceptGi": true 31 | }, 32 | { 33 | "_id": "5d725a037b292f5f8ceff787", 34 | "user": "5c8a1d5b0190b214360dc031", 35 | "name": "Codemasters", 36 | "description": "Is coding your passion? Codemasters will give you the skills and the tools to become the best developer possible. We specialize in full stack web development and data science", 37 | "website": "https://codemasters.com", 38 | "phone": "(333) 333-3333", 39 | "email": "enroll@codemasters.com", 40 | "address": "85 South Prospect Street Burlington VT 05405", 41 | "careers": ["Web Development", "Data Science", "Business"], 42 | "housing": false, 43 | "jobAssistance": false, 44 | "jobGuarantee": false, 45 | "acceptGi": false 46 | }, 47 | { 48 | "_id": "5d725a1b7b292f5f8ceff788", 49 | "user": "5c8a1d5b0190b214360dc032", 50 | "name": "Devcentral Bootcamp", 51 | "description": "Is coding your passion? Codemasters will give you the skills and the tools to become the best developer possible. We specialize in front end and full stack web development", 52 | "website": "https://devcentral.com", 53 | "phone": "(444) 444-4444", 54 | "email": "enroll@devcentral.com", 55 | "address": "45 Upper College Rd Kingston RI 02881", 56 | "careers": [ 57 | "Mobile Development", 58 | "Web Development", 59 | "Data Science", 60 | "Business" 61 | ], 62 | "housing": false, 63 | "jobAssistance": true, 64 | "jobGuarantee": true, 65 | "acceptGi": true 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /app/api/_data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5d7a514b5d2c12c7449be042", 4 | "name": "Admin Account", 5 | "email": "admin@gmail.com", 6 | "role": "user", 7 | "password": "123456" 8 | }, 9 | { 10 | "_id": "5d7a514b5d2c12c7449be043", 11 | "name": "Publisher Account", 12 | "email": "publisher@gmail.com", 13 | "role": "publisher", 14 | "password": "123456" 15 | }, 16 | { 17 | "_id": "5d7a514b5d2c12c7449be044", 18 | "name": "User Account", 19 | "email": "user@gmail.com", 20 | "role": "user", 21 | "password": "123456" 22 | }, 23 | { 24 | "_id": "5d7a514b5d2c12c7449be045", 25 | "name": "John Doe", 26 | "email": "john@gmail.com", 27 | "role": "publisher", 28 | "password": "123456" 29 | }, 30 | { 31 | "_id": "5d7a514b5d2c12c7449be046", 32 | "name": "Kevin Smith", 33 | "email": "kevin@gmail.com", 34 | "role": "publisher", 35 | "password": "123456" 36 | }, 37 | { 38 | "_id": "5c8a1d5b0190b214360dc031", 39 | "name": "Mary Williams", 40 | "email": "mary@gmail.com", 41 | "role": "publisher", 42 | "password": "123456" 43 | }, 44 | { 45 | "_id": "5c8a1d5b0190b214360dc032", 46 | "name": "Sasha Ryan", 47 | "email": "sasha@gmail.com", 48 | "role": "publisher", 49 | "password": "123456" 50 | }, 51 | { 52 | "_id": "5c8a1d5b0190b214360dc033", 53 | "name": "Greg Harris", 54 | "email": "greg@gmail.com", 55 | "role": "user", 56 | "password": "123456" 57 | }, 58 | { 59 | "_id": "5c8a1d5b0190b214360dc034", 60 | "name": "Derek Glover", 61 | "email": "derek@gmail.com", 62 | "role": "user", 63 | "password": "123456" 64 | }, 65 | { 66 | "_id": "5c8a1d5b0190b214360dc035", 67 | "name": "Stephanie Hanson", 68 | "email": "steph@gmail.com", 69 | "role": "user", 70 | "password": "123456" 71 | }, 72 | { 73 | "_id": "5c8a1d5b0190b214360dc036", 74 | "name": "Jerry Wiliams", 75 | "email": "jerry@gmail.com", 76 | "role": "user", 77 | "password": "123456" 78 | }, 79 | { 80 | "_id": "5c8a1d5b0190b214360dc037", 81 | "name": "Maggie Johnson", 82 | "email": "maggie@gmail.com", 83 | "role": "user", 84 | "password": "123456" 85 | }, 86 | { 87 | "_id": "5c8a1d5b0190b214360dc038", 88 | "name": "Barry Dickens", 89 | "email": "barry@gmail.com", 90 | "role": "user", 91 | "password": "123456" 92 | }, 93 | { 94 | "_id": "5c8a1d5b0190b214360dc039", 95 | "name": "Ryan Bolin", 96 | "email": "ryan@gmail.com", 97 | "role": "user", 98 | "password": "123456" 99 | }, 100 | { 101 | "_id": "5c8a1d5b0190b214360dc040", 102 | "name": "Sara Kensing", 103 | "email": "sara@gmail.com", 104 | "role": "user", 105 | "password": "123456" 106 | } 107 | ] 108 | -------------------------------------------------------------------------------- /app/api/controllers/reviews.js: -------------------------------------------------------------------------------- 1 | const ErrorResponse = require('../utils/errorResponse'); 2 | const asyncHandler = require('../middleware/async'); 3 | const Review = require('../models/Review'); 4 | const Bootcamp = require('../models/Bootcamp'); 5 | 6 | // @desc Get singlereviews 7 | // @route GET /api/v1/reviews 8 | // @route GET /api/v1/bootcamps/:bootcampId/reviews 9 | // @access Public 10 | exports.getReviews = asyncHandler(async (req, res, next) => { 11 | if (req.params.bootcampId) { 12 | const reviews = await Review.find({ bootcamp: req.params.bootcampId }); 13 | 14 | return res.status(200).json({ 15 | success: true, 16 | count: reviews.length, 17 | data: reviews, 18 | }); 19 | } else { 20 | res.status(200).json(res.advancedResults); 21 | } 22 | }); 23 | 24 | // @desc Get single review 25 | // @route GET /api/v1/reviews/:id 26 | // @access Public 27 | exports.getReview = asyncHandler(async (req, res, next) => { 28 | const review = await Review.findById(req.params.id).populate({ 29 | path: 'bootcamp', 30 | select: 'name description', 31 | }); 32 | 33 | if (!review) { 34 | return next( 35 | new ErrorResponse(`No review found with the id of ${req.params.id}`, 404) 36 | ); 37 | } 38 | 39 | return res.status(200).json({ 40 | success: true, 41 | data: review, 42 | }); 43 | }); 44 | 45 | // @desc Add review 46 | // @route POST /api/v1/bootcamps/:bootcampId/reviews 47 | // @access Private 48 | exports.addReview = asyncHandler(async (req, res, next) => { 49 | req.body.bootcamp = req.params.bootcampId; 50 | req.body.user = req.user.id; 51 | 52 | const bootcamp = await Bootcamp.findById(req.params.bootcampId); 53 | 54 | if (!bootcamp) { 55 | return next( 56 | new ErrorResponse( 57 | `No bootcamp with the id of ${req.params.bootcampId}`, 58 | 404 59 | ) 60 | ); 61 | } 62 | 63 | const review = await Review.create(req.body); 64 | 65 | return res.status(201).json({ 66 | success: true, 67 | data: review, 68 | }); 69 | }); 70 | 71 | // @desc Update review 72 | // @route PUT /api/v1/reviews/:id 73 | // @access Private 74 | exports.updateReview = asyncHandler(async (req, res, next) => { 75 | let review = await Review.findById(req.params.id); 76 | 77 | if (!review) { 78 | return next( 79 | new ErrorResponse(`No review with the id of ${req.params.id}`, 404) 80 | ); 81 | } 82 | 83 | // Make sure review belongs to user or user is admin 84 | if (review.user.toString() !== req.user.id && req.user.role !== 'admin') { 85 | return next(new ErrorResponse(`Not authorized to update review`, 401)); 86 | } 87 | 88 | review = await Review.findByIdAndUpdate(req.params.id, req.body, { 89 | new: true, 90 | runValidators: true, 91 | }); 92 | 93 | return res.status(200).json({ 94 | success: true, 95 | data: review, 96 | }); 97 | }); 98 | 99 | // @desc Delete review 100 | // @route DELETE /api/v1/reviews/:id 101 | // @access Private 102 | exports.deleteReview = asyncHandler(async (req, res, next) => { 103 | const review = await Review.findById(req.params.id); 104 | 105 | if (!review) { 106 | return next( 107 | new ErrorResponse(`No review with the id of ${req.params.id}`, 404) 108 | ); 109 | } 110 | 111 | // Make sure review belongs to user or user is admin 112 | if (review.user.toString() !== req.user.id && req.user.role !== 'admin') { 113 | return next(new ErrorResponse(`Not authorized to update review`, 401)); 114 | } 115 | 116 | await review.remove(); 117 | 118 | return res.status(200).json({ 119 | success: true, 120 | data: {}, 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /app/api/controllers/courses.js: -------------------------------------------------------------------------------- 1 | const ErrorResponse = require('../utils/errorResponse'); 2 | const asyncHandler = require('../middleware/async'); 3 | const Course = require('../models/Course'); 4 | const Bootcamp = require('../models/Bootcamp'); 5 | 6 | // @desc Get courses 7 | // @route GET /api/v1/courses 8 | // @route GET /api/v1/bootcamps/:bootcampId/courses 9 | // @access Public 10 | exports.getCourses = asyncHandler(async (req, res, next) => { 11 | if (req.params.bootcampId) { 12 | const courses = await Course.find({ bootcamp: req.params.bootcampId }); 13 | 14 | return res.status(200).json({ 15 | success: true, 16 | count: courses.length, 17 | data: courses, 18 | }); 19 | } else { 20 | res.status(200).json(res.advancedResults); 21 | } 22 | }); 23 | 24 | // @desc Get single courses 25 | // @route GET /api/v1/courses/:id 26 | // @access Public 27 | exports.getCourse = asyncHandler(async (req, res, next) => { 28 | const course = await Course.findById(req.params.id).populate({ 29 | path: 'bootcamp', 30 | select: 'name description', 31 | }); 32 | if (!course) { 33 | return next( 34 | new ErrorResponse(`No course with the id of ${req.params.id}`), 35 | 404 36 | ); 37 | } 38 | return res.status(200).json({ 39 | success: true, 40 | data: course, 41 | }); 42 | }); 43 | 44 | // @desc Add course 45 | // @route POST /api/v1/bootcamps/:bootcampId/courses 46 | // @access Private 47 | exports.addCourse = asyncHandler(async (req, res, next) => { 48 | req.body.bootcamp = req.params.bootcampId; 49 | 50 | req.body.user = req.user.id; 51 | 52 | const bootcamp = await Bootcamp.findById(req.params.bootcampId); 53 | 54 | if (!bootcamp) { 55 | return next( 56 | new ErrorResponse(`No bootcamp with the id of ${req.params.bootcampId}`), 57 | 404 58 | ); 59 | } 60 | 61 | // Make sure user is bootcamp owner 62 | if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { 63 | return next( 64 | new ErrorResponse( 65 | `User ${req.user.id} is not authorized to add a course to bootcamp ${bootcmap._id}`, 66 | 401 67 | ) 68 | ); 69 | } 70 | 71 | const course = await Course.create(req.body); 72 | 73 | return res.status(200).json({ 74 | success: true, 75 | data: course, 76 | }); 77 | }); 78 | 79 | // @desc Update course 80 | // @route PUT /api/v1/courses/:id 81 | // @access Private 82 | exports.updateCourse = asyncHandler(async (req, res, next) => { 83 | let course = await Course.findById(req.params.id); 84 | 85 | if (!course) { 86 | return next( 87 | new ErrorResponse(`No course with the id of ${req.params.id}`), 88 | 404 89 | ); 90 | } 91 | 92 | // Make sure user is course owner 93 | if (course.user.toString() !== req.user.id && req.user.role !== 'admin') { 94 | return next( 95 | new ErrorResponse( 96 | `User ${req.user.id} is not authorized to update course ${course._id}`, 97 | 401 98 | ) 99 | ); 100 | } 101 | 102 | course = await Course.findByIdAndUpdate(req.params.id, req.body, { 103 | new: true, 104 | runValidators: true, 105 | }); 106 | 107 | return res.status(200).json({ 108 | success: true, 109 | data: course, 110 | }); 111 | }); 112 | 113 | // @desc Delete course 114 | // @route Delete /api/v1/courses/:id 115 | // @access Private 116 | exports.deleteCourse = asyncHandler(async (req, res, next) => { 117 | const course = await Course.findById(req.params.id); 118 | 119 | if (!course) { 120 | return next( 121 | new ErrorResponse(`No course with the id of ${req.params.id}`), 122 | 404 123 | ); 124 | } 125 | 126 | // Make sure user is course owner 127 | if (course.user.toString() !== req.user.id && req.user.role !== 'admin') { 128 | return next( 129 | new ErrorResponse( 130 | `User ${req.user.id} is not authorized to delete course ${course._id}`, 131 | 401 132 | ) 133 | ); 134 | } 135 | 136 | await course.remove(); 137 | 138 | return res.status(200).json({ 139 | success: true, 140 | data: {}, 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /app/api/_data/courses.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5d725a4a7b292f5f8ceff789", 4 | "title": "Front End Web Development", 5 | "description": "This course will provide you with all of the essentials to become a successful frontend web developer. You will learn to master HTML, CSS and front end JavaScript, along with tools like Git, VSCode and front end frameworks like Vue", 6 | "weeks": 8, 7 | "tuition": 8000, 8 | "minimumSkill": "beginner", 9 | "scholarhipsAvailable": true, 10 | "bootcamp": "5d713995b721c3bb38c1f5d0", 11 | "user": "5d7a514b5d2c12c7449be045" 12 | }, 13 | { 14 | "_id": "5d725c84c4ded7bcb480eaa0", 15 | "title": "Full Stack Web Development", 16 | "description": "In this course you will learn full stack web development, first learning all about the frontend with HTML/CSS/JS/Vue and then the backend with Node.js/Express/MongoDB", 17 | "weeks": 12, 18 | "tuition": 10000, 19 | "minimumSkill": "intermediate", 20 | "scholarhipsAvailable": true, 21 | "bootcamp": "5d713995b721c3bb38c1f5d0", 22 | "user": "5d7a514b5d2c12c7449be045" 23 | }, 24 | { 25 | "_id": "5d725cb9c4ded7bcb480eaa1", 26 | "title": "Full Stack Web Dev", 27 | "description": "In this course you will learn all about the front end with HTML, CSS and JavaScript. You will master tools like Git and Webpack and also learn C# and ASP.NET with Postgres", 28 | "weeks": 10, 29 | "tuition": 12000, 30 | "minimumSkill": "intermediate", 31 | "scholarhipsAvailable": true, 32 | "bootcamp": "5d713a66ec8f2b88b8f830b8", 33 | "user": "5d7a514b5d2c12c7449be046" 34 | }, 35 | { 36 | "_id": "5d725cd2c4ded7bcb480eaa2", 37 | "title": "UI/UX", 38 | "description": "In this course you will learn to create beautiful interfaces. It is a mix of design and development to create modern user experiences on both web and mobile", 39 | "weeks": 12, 40 | "tuition": 10000, 41 | "minimumSkill": "intermediate", 42 | "scholarhipsAvailable": true, 43 | "bootcamp": "5d713a66ec8f2b88b8f830b8", 44 | "user": "5d7a514b5d2c12c7449be046" 45 | }, 46 | { 47 | "_id": "5d725ce8c4ded7bcb480eaa3", 48 | "title": "Web Design & Development", 49 | "description": "Get started building websites and web apps with HTML/CSS/JavaScript/PHP. We teach you", 50 | "weeks": 10, 51 | "tuition": 9000, 52 | "minimumSkill": "beginner", 53 | "scholarhipsAvailable": true, 54 | "bootcamp": "5d725a037b292f5f8ceff787", 55 | "user": "5c8a1d5b0190b214360dc031" 56 | }, 57 | { 58 | "_id": "5d725cfec4ded7bcb480eaa4", 59 | "title": "Data Science Program", 60 | "description": "In this course you will learn Python for data science, machine learning and big data tools", 61 | "weeks": 10, 62 | "tuition": 12000, 63 | "minimumSkill": "intermediate", 64 | "scholarhipsAvailable": false, 65 | "bootcamp": "5d725a037b292f5f8ceff787", 66 | "user": "5c8a1d5b0190b214360dc031" 67 | }, 68 | { 69 | "_id": "5d725cfec4ded7bcb480eaa5", 70 | "title": "Web Development", 71 | "description": "This course will teach you how to build high quality web applications with technologies like React, Node.js, PHP & Laravel", 72 | "weeks": 8, 73 | "tuition": 8000, 74 | "minimumSkill": "beginner", 75 | "scholarhipsAvailable": false, 76 | "bootcamp": "5d725a1b7b292f5f8ceff788", 77 | "user": "5c8a1d5b0190b214360dc032" 78 | }, 79 | { 80 | "_id": "5d725cfec4ded7bcb480eaa6", 81 | "title": "Software QA", 82 | "description": "This course will teach you everything you need to know about quality assurance", 83 | "weeks": 6, 84 | "tuition": 5000, 85 | "minimumSkill": "intermediate", 86 | "scholarhipsAvailable": false, 87 | "bootcamp": "5d725a1b7b292f5f8ceff788", 88 | "user": "5c8a1d5b0190b214360dc032" 89 | }, 90 | { 91 | "_id": "5d725cfec4ded7bcb480eaa7", 92 | "title": "IOS Development", 93 | "description": "Get started building mobile applications for IOS using Swift and other tools", 94 | "weeks": 8, 95 | "tuition": 6000, 96 | "minimumSkill": "intermediate", 97 | "scholarhipsAvailable": false, 98 | "bootcamp": "5d725a1b7b292f5f8ceff788", 99 | "user": "5c8a1d5b0190b214360dc032" 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /app/api/models/Bootcamp.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const slugify = require('slugify'); 3 | const geocoder = require('../utils/geocoder'); 4 | 5 | const BootcampSchema = new mongoose.Schema( 6 | { 7 | name: { 8 | type: String, 9 | required: [true, 'Please add a name'], 10 | unique: true, 11 | trim: true, 12 | maxlength: [50, 'Name can not be more than 50 characters'], 13 | }, 14 | slug: String, 15 | description: { 16 | type: String, 17 | required: [true, 'Please add a description'], 18 | maxlength: [500, 'Description can not be more than 500 characters'], 19 | }, 20 | website: { 21 | type: String, 22 | match: [ 23 | /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/, 24 | 'Please use a valid URL with HTTP or HTTPS', 25 | ], 26 | }, 27 | phone: { 28 | type: String, 29 | maxlength: [20, 'Phone number can not be longer than 20 characters'], 30 | }, 31 | email: { 32 | type: String, 33 | match: [ 34 | /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 35 | 'Please add a valid email', 36 | ], 37 | }, 38 | address: { 39 | type: String, 40 | required: [true, 'Please add an address'], 41 | }, 42 | location: { 43 | // GeoJSON Point 44 | type: { 45 | type: String, 46 | enum: ['Point'], 47 | }, 48 | coordinates: { 49 | type: [Number], 50 | index: '2dsphere', 51 | }, 52 | formattedAddress: String, 53 | street: String, 54 | city: String, 55 | state: String, 56 | zipcode: String, 57 | country: String, 58 | }, 59 | careers: { 60 | // Array of strings 61 | type: [String], 62 | required: true, 63 | enum: [ 64 | 'Web Development', 65 | 'Mobile Development', 66 | 'UI/UX', 67 | 'Data Science', 68 | 'Business', 69 | 'Other', 70 | ], 71 | }, 72 | averageRating: { 73 | type: Number, 74 | min: [1, 'Rating must be at least 1'], 75 | max: [10, 'Rating must can not be more than 10'], 76 | }, 77 | averageCost: Number, 78 | photo: { 79 | type: String, 80 | default: 'no-photo.jpg', 81 | }, 82 | housing: { 83 | type: Boolean, 84 | default: false, 85 | }, 86 | jobAssistance: { 87 | type: Boolean, 88 | default: false, 89 | }, 90 | jobGuarantee: { 91 | type: Boolean, 92 | default: false, 93 | }, 94 | acceptGi: { 95 | type: Boolean, 96 | default: false, 97 | }, 98 | createdAt: { 99 | type: Date, 100 | default: Date.now, 101 | }, 102 | user: { 103 | type: mongoose.Schema.ObjectId, 104 | ref: 'User', 105 | required: true, 106 | }, 107 | }, 108 | { 109 | toJSON: { virtuals: true }, 110 | toObject: { virtuals: true }, 111 | } 112 | ); 113 | 114 | // Create bootcamp slug from the name 115 | BootcampSchema.pre('save', function (next) { 116 | this.slug = slugify(this.name, { lower: true }); 117 | next(); 118 | }); 119 | 120 | // Geocode & create location field 121 | BootcampSchema.pre('save', async function (next) { 122 | const loc = await geocoder.geocode(this.address); 123 | this.location = { 124 | type: 'Point', 125 | coordinates: [loc[0].longitude, loc[0].latitude], 126 | formattedAddress: loc[0].formattedAddress, 127 | street: loc[0].streetName, 128 | city: loc[0].city, 129 | state: loc[0].stateCode, 130 | zipcode: loc[0].zipcode, 131 | country: loc[0].countyCode, 132 | }; 133 | 134 | // Do not save address in DB 135 | this.address = undefined; 136 | 137 | next(); 138 | }); 139 | 140 | // Cascade delete course when a bootcamp is deleted 141 | BootcampSchema.pre('remove', async function (next) { 142 | console.log(`Courses being removed from bootcamp ${this._id}`); 143 | await this.model('Course').deleteMany({ 144 | bootcamp: this._id, 145 | }); 146 | next(); 147 | }); 148 | 149 | // Reverse populate with virtuals 150 | BootcampSchema.virtual('courses', { 151 | ref: 'Course', 152 | localField: '_id', 153 | foreignField: 'bootcamp', 154 | justOne: false, 155 | }); 156 | 157 | module.exports = mongoose.model('Bootcamp', BootcampSchema); 158 | -------------------------------------------------------------------------------- /app/api/_data/reviews.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5d7a514b5d2c12c7449be020", 4 | "title": "Learned a ton!", 5 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 6 | "rating": "8", 7 | "bootcamp": "5d713995b721c3bb38c1f5d0", 8 | "user": "5c8a1d5b0190b214360dc033" 9 | }, 10 | { 11 | "_id": "5d7a514b5d2c12c7449be021", 12 | "title": "Great bootcamp", 13 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 14 | "rating": "10", 15 | "bootcamp": "5d713995b721c3bb38c1f5d0", 16 | "user": "5c8a1d5b0190b214360dc034" 17 | }, 18 | { 19 | "_id": "5d7a514b5d2c12c7449be022", 20 | "title": "Got me a developer job", 21 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 22 | "rating": "7", 23 | "bootcamp": "5d713a66ec8f2b88b8f830b8", 24 | "user": "5c8a1d5b0190b214360dc035" 25 | }, 26 | { 27 | "_id": "5d7a514b5d2c12c7449be023", 28 | "title": "Not that great", 29 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 30 | "rating": "4", 31 | "bootcamp": "5d713a66ec8f2b88b8f830b8", 32 | "user": "5c8a1d5b0190b214360dc036" 33 | }, 34 | { 35 | "_id": "5d7a514b5d2c12c7449be024", 36 | "title": "Great overall experience", 37 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 38 | "rating": "7", 39 | "bootcamp": "5d725a037b292f5f8ceff787", 40 | "user": "5c8a1d5b0190b214360dc037" 41 | }, 42 | { 43 | "_id": "5d7a514b5d2c12c7449be025", 44 | "title": "Not worth the money", 45 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 46 | "rating": "5", 47 | "bootcamp": "5d725a037b292f5f8ceff787", 48 | "user": "5c8a1d5b0190b214360dc038" 49 | }, 50 | { 51 | "_id": "5d7a514b5d2c12c7449be026", 52 | "title": "Best instructors", 53 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 54 | "rating": "10", 55 | "bootcamp": "5d725a1b7b292f5f8ceff788", 56 | "user": "5c8a1d5b0190b214360dc039" 57 | }, 58 | { 59 | "_id": "5d7a514b5d2c12c7449be027", 60 | "title": "Was worth the investment", 61 | "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec viverra feugiat mauris id viverra. Duis luctus ex sed facilisis ultrices. Curabitur scelerisque bibendum ligula, quis condimentum libero fermentum in. Aenean erat erat, aliquam in purus a, rhoncus hendrerit tellus. Donec accumsan justo in felis consequat sollicitudin. Fusce luctus mattis nunc vitae maximus. Curabitur semper felis eu magna laoreet scelerisque", 62 | "rating": "7", 63 | "bootcamp": "5d725a1b7b292f5f8ceff788", 64 | "user": "5c8a1d5b0190b214360dc040" 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /app/api/controllers/auth.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const ErrorResponse = require('../utils/errorResponse'); 3 | const asyncHandler = require('../middleware/async'); 4 | const User = require('../models/User'); 5 | const sendEmail = require('../utils/sendEmail'); 6 | 7 | // @desc Get current logged in user 8 | // @route POST /api/v1/auth/me 9 | // @access Private 10 | exports.getMe = asyncHandler(async (req, res, next) => { 11 | const user = (await User.findById(req.user.id)) || null; 12 | 13 | return res.status(200).json({ 14 | success: true, 15 | data: user, 16 | }); 17 | }); 18 | 19 | // @desc Log user out / clear cookie 20 | // @route GET /api/v1/auth/logout 21 | // @access Private 22 | exports.logout = asyncHandler(async (req, res, next) => { 23 | res.cookie('token', 'node', { 24 | expires: new Date(Date.now() + 10 * 1000), 25 | httpOnly: true, 26 | }); 27 | 28 | return res.status(200).json({ 29 | success: true, 30 | data: {}, 31 | }); 32 | }); 33 | 34 | // @desc Forgot password 35 | // @route POST /api/v1/auth/forgotpassword 36 | // @access Private 37 | exports.forgotPassword = asyncHandler(async (req, res, next) => { 38 | const user = await User.findOne({ email: req.body.email }); 39 | 40 | if (!user) { 41 | return next(new ErrorResponse('There is not user with that email', 404)); 42 | } 43 | 44 | // Get reset token 45 | const resetToken = user.getResetPasswordToken(); 46 | 47 | await user.save({ validateBeforeSave: false }); 48 | 49 | // Create reset url 50 | const resetUrl = `${req.protocol}://${req.get( 51 | 'host' 52 | )}/api/v1/auth/resetpassword/${resetToken}`; 53 | 54 | const message = `You are receiving this email because you (or someone else) has requested the reset of a password. Please make a PUT request to: \n\n ${resetUrl}`; 55 | 56 | try { 57 | await sendEmail({ 58 | email: user.email, 59 | subject: 'Password reset token', 60 | message, 61 | }); 62 | 63 | return res.status(200).json({ 64 | success: true, 65 | data: `Email sent to ${user.email}`, 66 | }); 67 | } catch (err) { 68 | console.log(err); 69 | user.resetPasswordToken = undefined; 70 | user.resetPasswordExpire = undefined; 71 | 72 | await user.save({ 73 | validateBeforeSave: false, 74 | }); 75 | 76 | return next(new ErrorResponse('Email could not be send', 500)); 77 | } 78 | }); 79 | 80 | // @desc Update user details 81 | // @route PUT /api/v1/auth/updatedetails 82 | // @access Private 83 | exports.updateDetails = asyncHandler(async (req, res, next) => { 84 | const fieldsToUpdate = { 85 | name: req.body.name, 86 | email: req.body.email, 87 | }; 88 | 89 | const user = await User.findByIdAndUpdate(req.user.id, fieldsToUpdate, { 90 | new: true, 91 | runValidators: true, 92 | }); 93 | 94 | return res.status(200).json({ 95 | success: true, 96 | data: user, 97 | }); 98 | }); 99 | 100 | // @desc Update password 101 | // @route PUT /api/v1/auth/updatepassword 102 | // @access Private 103 | exports.updatePassword = asyncHandler(async (req, res, next) => { 104 | const user = await User.findById(req.user.id).select('+password'); 105 | 106 | // Check current password 107 | if (!(await user.matchPassword(req.body.currentPassword))) { 108 | return next(new ErrorResponse('Password is incorrect', 401)); 109 | } 110 | 111 | user.password = req.body.newPassword; 112 | await user.save(); 113 | 114 | sendTokenResponse(user, 200, res); 115 | }); 116 | 117 | // @desc Reset password 118 | // @route PUT /api/v1/auth/resetpassword/:resettoken 119 | // @access Public 120 | exports.resetpassword = asyncHandler(async (req, res, next) => { 121 | // Get hashed token 122 | const resetPasswordToken = crypto 123 | .createHash('sha256') 124 | .update(req.params.resettoken) 125 | .digest('hex'); 126 | 127 | const user = await User.findOne({ 128 | resetPasswordToken, 129 | resetPasswordExpire: { $gt: Date.now() }, 130 | }); 131 | 132 | if (!user) { 133 | return next(new ErrorResponse('Invalid token', 400)); 134 | } 135 | 136 | // Set new password 137 | user.password = req.body.password; 138 | user.resetPasswordToken = undefined; 139 | user.resetPasswordExpire = undefined; 140 | await user.save(); 141 | 142 | sendTokenResponse(user, 200, res); 143 | }); 144 | 145 | // @desc Register user 146 | // @route POST /api/v1/auth/register 147 | // @access Public 148 | exports.register = asyncHandler(async (req, res, next) => { 149 | const { name, email, password, role } = req.body; 150 | 151 | const user = await User.create({ 152 | name, 153 | email, 154 | password, 155 | role, 156 | }); 157 | 158 | sendTokenResponse(user, 200, res); 159 | }); 160 | 161 | // @desc Login user 162 | // @route POST /api/v1/auth/login 163 | // @access Public 164 | exports.login = asyncHandler(async (req, res, next) => { 165 | const { email, password } = req.body; 166 | 167 | // Validate email & password 168 | if (!email || !password) { 169 | return next(new ErrorResponse('Please provide an email and password', 400)); 170 | } 171 | 172 | // Check for user 173 | const user = await User.findOne({ email }).select('+password'); 174 | 175 | if (!user) { 176 | return next(new ErrorResponse('Invalid credentials', 401)); 177 | } 178 | 179 | // Check if password matches 180 | const isMatch = await user.matchPassword(password); 181 | 182 | if (!isMatch) { 183 | return next(new ErrorResponse('Invalid credentials', 401)); 184 | } 185 | 186 | sendTokenResponse(user, 200, res); 187 | }); 188 | 189 | // Get token from model, create cookie and send response 190 | const sendTokenResponse = (user, statusCode, res) => { 191 | // Create token 192 | const token = user.getSignedJwtToken(); 193 | 194 | const options = { 195 | expires: new Date( 196 | Date.now() + process.env.JWT_COOKIE_EXPIR * 24 * 60 * 60 * 1000 197 | ), 198 | httpOnly: true, 199 | }; 200 | 201 | if (process.env.NODE_ENV === 'production') { 202 | options.secure = true; 203 | } 204 | 205 | return res.status(statusCode).cookie('token', token, options).json({ 206 | success: true, 207 | token, 208 | }); 209 | }; 210 | -------------------------------------------------------------------------------- /app/api/controllers/bootcamps.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const asyncHandler = require('../middleware/async'); 3 | const ErrorResponse = require('../utils/errorResponse'); 4 | const Bootcamp = require('../models/Bootcamp'); 5 | const geocoder = require('../utils/geocoder'); 6 | 7 | // @desc Get all bootcamps 8 | // @route GET /api/v1/bootcamps 9 | // @access Public 10 | exports.getBootcamps = asyncHandler(async (req, res, next) => { 11 | return res.status(200).json(res.advancedResults); 12 | }); 13 | 14 | // @desc Get single bootcamp 15 | // @route GET /api/v1/bootcamps/:id 16 | // @access Public 17 | exports.getBootcamp = asyncHandler(async (req, res, next) => { 18 | const bootcamp = await Bootcamp.findById(req.params.id); 19 | 20 | if (!bootcamp) { 21 | return next( 22 | new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) 23 | ); 24 | } 25 | 26 | return res.status(200).json({ 27 | success: true, 28 | data: bootcamp, 29 | }); 30 | }); 31 | 32 | // @desc Create new bootcamp 33 | // @route POST /api/v1/bootcamps 34 | // @access Private 35 | exports.createBootcamp = asyncHandler(async (req, res, next) => { 36 | // Add user to req.body 37 | req.body.user = req.user.id; 38 | 39 | // Check for published bootcamp 40 | const publishedBootcamp = await Bootcamp.findOne({ user: req.user.id }); 41 | 42 | // If the user is not an admin, they can only add one bootcamp 43 | if (publishedBootcamp && req.user.role !== 'admin') { 44 | return next( 45 | new ErrorResponse( 46 | `The user with ID ${req.user.id} has already published a bootcamp`, 47 | 400 48 | ) 49 | ); 50 | } 51 | 52 | const bootcamp = await Bootcamp.create(req.body); 53 | 54 | return res.status(200).json({ 55 | success: true, 56 | data: bootcamp, 57 | }); 58 | }); 59 | 60 | // @desc Update bootcamp 61 | // @route PUT /api/v1/bootcamps/:id 62 | // @access Private 63 | exports.updateBootcamp = asyncHandler(async (req, res, next) => { 64 | let bootcamp = await Bootcamp.findById(req.params.id); 65 | 66 | if (!bootcamp) { 67 | return next( 68 | new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) 69 | ); 70 | } 71 | 72 | // Make sure user is bootcamp owner 73 | if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { 74 | return next( 75 | new ErrorResponse( 76 | `User ${req.params.id} is not authorized to update this bootcamp`, 77 | 401 78 | ) 79 | ); 80 | } 81 | 82 | bootcamp = await Bootcamp.findOneAndUpdate(req.params.id, req.body, { 83 | new: true, 84 | runValidators: true, 85 | }); 86 | 87 | return res.status(200).json({ 88 | success: true, 89 | data: bootcamp, 90 | }); 91 | }); 92 | 93 | // @desc Delete bootcamp 94 | // @route DELETE /api/v1/bootcamps/:id 95 | // @access Private 96 | exports.deleteBootcamp = asyncHandler(async (req, res, next) => { 97 | const bootcamp = await Bootcamp.findById(req.params.id); 98 | 99 | if (!bootcamp) { 100 | return next( 101 | new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) 102 | ); 103 | } 104 | 105 | // Make sure user is bootcamp owner 106 | if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { 107 | return next( 108 | new ErrorResponse( 109 | `User ${req.params.id} is not authorized to delete this bootcamp`, 110 | 401 111 | ) 112 | ); 113 | } 114 | 115 | bootcamp.remove(); 116 | 117 | return res.status(200).json({ 118 | success: true, 119 | data: {}, 120 | }); 121 | }); 122 | 123 | // @desc Get bootcamps within a radius 124 | // @route GET /api/v1/bootcamps/radius/:zipcode/:distance 125 | // @access Private 126 | exports.getBootcampsInRadius = asyncHandler(async (req, res, next) => { 127 | const { zipcode, distance } = req.params; 128 | 129 | // Get lat/lng from geocoder 130 | const loc = await geocoder.geocode(zipcode); 131 | const lat = loc[0].latitude; 132 | const lng = loc[0].longitude; 133 | 134 | // Calc radius using radians 135 | // Divide dist by radius of Earth 136 | // Earth Radius = 3,963 mi / 6,378 km 137 | const radius = distance / 3963; 138 | const bootcamps = await Bootcamp.find({ 139 | location: { $geoWithin: { $centerSphere: [[lng, lat], radius] } }, 140 | }); 141 | 142 | res.status(200).json({ 143 | success: true, 144 | count: bootcamps.length, 145 | data: bootcamps, 146 | }); 147 | }); 148 | 149 | // @desc Upload photo for bootcamp 150 | // @route PUT /api/v1/bootcamps/:id/photo 151 | // @access Private 152 | exports.bootcampPhotoUpload = asyncHandler(async (req, res, next) => { 153 | const bootcamp = await Bootcamp.findById(req.params.id); 154 | if (!bootcamp) { 155 | return next( 156 | new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) 157 | ); 158 | } 159 | 160 | // Make sure user is bootcamp owner 161 | if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { 162 | return next( 163 | new ErrorResponse( 164 | `User ${req.params.id} is not authorized to update this bootcamp`, 165 | 401 166 | ) 167 | ); 168 | } 169 | 170 | if (!req.files) { 171 | return next(new ErrorResponse(`Please upload a file`, 404)); 172 | } 173 | 174 | const file = req.files.file; 175 | 176 | // Make sure the image is a photo 177 | if (!file.mimetype.startsWith('image')) { 178 | return next(new ErrorResponse(`Please upload an image file`, 404)); 179 | } 180 | 181 | // Check filesize 182 | if (file.size > process.env.MAX_FILE_UPLOAD) { 183 | return next( 184 | new ErrorResponse( 185 | `Please upload an image less than ${process.env.MAX_FILE_UPLOAD}`, 186 | 404 187 | ) 188 | ); 189 | } 190 | 191 | // Create custom filename 192 | file.name = `photo_${bootcamp._id}${path.parse(file.name).ext}`; 193 | 194 | file.mv(`${process.env.FILE_UPLOAD_PATH}/${file.name}`, async (err) => { 195 | if (err) { 196 | console.error(err); 197 | return next(new ErrorResponse(`Problem with file upload`, 500)); 198 | } 199 | 200 | await Bootcamp.findByIdAndUpdate(req.param.id, { photo: file.name }); 201 | 202 | return res.status(200).json({ 203 | success: true, 204 | data: file.name, 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /Development.md: -------------------------------------------------------------------------------- 1 | # [Brad Traversy] Node.js API Masterclass With Express & MongoDB [ENG, 2019] 2 | 3 |
4 | 5 | ## 2. HTTP Intro - Headers, Body, Status Codes, etc 6 | 7 |
8 | 9 | ## 3. Starting Our DevCamper Project 10 | 11 |
12 | 13 | ### 2. Basic Express Server, dotenv & Git 14 | 15 | $ cd api 16 | $ npm init -y 17 | 18 | $ npm install --save express dotenv 19 | $ npm install --save-dev nodemon 20 | 21 | $ npm run dev 22 | 23 |
24 | 25 | ### 3. Creating Routes & Responses In Express 26 | 27 |
28 | 29 | ![Application](/img/pic-02-01.png?raw=true) 30 | 31 |
32 | 33 | ### 4. Using The Express Router 34 | 35 |
36 | 37 | ### 5. Creating Controller Methods 38 | 39 |
40 | 41 | ### 6. Intro To Middleware 42 | 43 | $ npm install --save-dev morgan 44 | 45 |
46 | 47 | ### 7. Postman Environment & Collections 48 | 49 |
50 | 51 | ## 4. Getting Started With MongoDB & Bootcamps Resource 52 | 53 |
54 | 55 | ### 1. MongoDB Atlas & Compass Setup 56 | 57 | We made an account on mongodb.com 58 | 59 |
60 | 61 | ### 2. Connecting To The Database With Mongoose 62 | 63 | $ npm install --save mongoose 64 | 65 |
66 | 67 | MongoDB Connected: traversy-node-js-api-masterclass-shard-00-02-9n706.mongodb.net 68 | 69 |
70 | 71 | ### 3. Colors In The Console 72 | 73 | $ npm install --save colors 74 | 75 |
76 | 77 | ### 4. Creating Our First Model 78 | 79 |
80 | 81 | ### 5. Create Bootcamp - POST 82 | 83 | $ curl -d '{ 84 | "user": "5d7a514b5d2c12c7449be045", 85 | "name": "Devworks Bootcamp", 86 | "description": "Devworks is a full stack JavaScript Bootcamp located in the heart of Boston that focuses on the technologies you need to get a high paying job as a web developer", 87 | "website": "https://devworks.com", 88 | "phone": "(111) 111-1111", 89 | "email": "enroll@devworks.com", 90 | "address": "233 Bay State Rd Boston MA 02215", 91 | "careers": ["Web Development", "UI/UX", "Business"], 92 | "housing": true, 93 | "jobAssistance": true, 94 | "jobGuarantee": false, 95 | "acceptGi": true 96 | }' \ 97 | -H "Content-Type: application/json" \ 98 | -X POST localhost:5000/api/v1/bootcamps \ 99 | | python -m json.tool 100 | 101 |
102 | 103 | $ curl -d '{ 104 | "user": "5d7a514b5d2c12c7449be046", 105 | "name": "ModernTech Bootcamp", 106 | "description": "ModernTech has one goal, and that is to make you a rockstar developer and/or designer with a six figure salary. We teach both development and UI/UX", 107 | "website": "https://moderntech.com", 108 | "phone": "(222) 222-2222", 109 | "email": "enroll@moderntech.com", 110 | "address": "220 Pawtucket St, Lowell, MA 01854", 111 | "careers": ["Web Development", "UI/UX", "Mobile Development"], 112 | "housing": false, 113 | "jobAssistance": true, 114 | "jobGuarantee": false, 115 | "acceptGi": true 116 | }' \ 117 | -H "Content-Type: application/json" \ 118 | -X POST localhost:5000/api/v1/bootcamps \ 119 | | python -m json.tool 120 | 121 |
122 | 123 | $ curl -d '{ 124 | "user": "5c8a1d5b0190b214360dc031", 125 | "name": "Codemasters", 126 | "description": "Is coding your passion? Codemasters will give you the skills and the tools to become the best developer possible. We specialize in full stack web development and data science", 127 | "website": "https://codemasters.com", 128 | "phone": "(333) 333-3333", 129 | "email": "enroll@codemasters.com", 130 | "address": "85 South Prospect Street Burlington VT 05405", 131 | "careers": ["Web Development", "Data Science", "Business"], 132 | "housing": false, 133 | "jobAssistance": false, 134 | "jobGuarantee": false, 135 | "acceptGi": false 136 | }' \ 137 | -H "Content-Type: application/json" \ 138 | -X POST localhost:5000/api/v1/bootcamps \ 139 | | python -m json.tool 140 | 141 |
142 | 143 | $ curl -d '{ 144 | "user": "5c8a1d5b0190b214360dc032", 145 | "name": "Devcentral Bootcamp", 146 | "description": "Is coding your passion? Codemasters will give you the skills and the tools to become the best developer possible. We specialize in front end and full stack web development", 147 | "website": "https://devcentral.com", 148 | "phone": "(444) 444-4444", 149 | "email": "enroll@devcentral.com", 150 | "address": "45 Upper College Rd Kingston RI 02881", 151 | "careers": [ 152 | "Mobile Development", 153 | "Web Development", 154 | "Data Science", 155 | "Business" 156 | ], 157 | "housing": false, 158 | "jobAssistance": true, 159 | "jobGuarantee": true, 160 | "acceptGi": true 161 | }' \ 162 | -H "Content-Type: application/json" \ 163 | -X POST localhost:5000/api/v1/bootcamps \ 164 | | python -m json.tool 165 | 166 |
167 | 168 | ### 6. Fetching Bootcamps - GET 169 | 170 | $ curl \ 171 | -H "Content-Type: application/json" \ 172 | -X GET localhost:5000/api/v1/bootcamps \ 173 | | python -m json.tool 174 | 175 |
176 | 177 | $ curl \ 178 | -H "Content-Type: application/json" \ 179 | -X GET localhost:5000/api/v1/bootcamps/5db62fd567c1170dd52c2c34 \ 180 | | python -m json.tool 181 | 182 |
183 | 184 | ### 7. Updating & Deleting Bootcamps - PUT & DELETE 185 | 186 | $ curl \ 187 | -d '{ 188 | "housing": true 189 | }' \ 190 | -H "Content-Type: application/json" \ 191 | -X PUT localhost:5000/api/v1/bootcamps/5db62fd567c1170dd52c2c34 \ 192 | | python -m json.tool 193 | 194 |
195 | 196 | $ curl -d '{ 197 | "careers": ["UI/UX"] 198 | }' \ 199 | -H "Content-Type: application/json" \ 200 | -X PUT localhost:5000/api/v1/bootcamps/5db62fd567c1170dd52c2c34 \ 201 | | python -m json.tool 202 | 203 |
204 | 205 | $ curl \ 206 | -H "Content-Type: application/json" \ 207 | -X DELETE localhost:5000/api/v1/bootcamps/5db62fd567c1170dd52c2c34 \ 208 | | python -m json.tool 209 | 210 |
211 | 212 | ## 5. Custom Error Handling & Mongoose Middleware 213 | 214 |
215 | 216 | ### 1. Error Handler Middleware 217 | 218 |
219 | 220 | ### 2. Custom ErrorResponse Class 221 | 222 |
223 | 224 | ### 3. Mongoose Error Handling [1] 225 | 226 |
227 | 228 | ### 4. Mongoose Error Handling [2] 229 | 230 | $ curl \ 231 | -H "Content-Type: application/json" \ 232 | -X POST localhost:5000/api/v1/bootcamps \ 233 | | python -m json.tool 234 | 235 |
236 | 237 | ### 4. Mongoose Error Handling [2] 238 | 239 |
240 | 241 | ### 5. AsyncAwait Middleware 242 | 243 |
244 | 245 | ### 6. Mongoose Middleware & Slugify 246 | 247 | $ npm install --save slugify 248 | 249 | We deleted all documents in the database 250 | 251 | $ curl -d '{ 252 | "user": "5d7a514b5d2c12c7449be045", 253 | "name": "Devworks Bootcamp", 254 | "description": "Devworks is a full stack JavaScript Bootcamp located in the heart of Boston that focuses on the technologies you need to get a high paying job as a web developer", 255 | "website": "https://devworks.com", 256 | "phone": "(111) 111-1111", 257 | "email": "enroll@devworks.com", 258 | "address": "233 Bay State Rd Boston MA 02215", 259 | "careers": ["Web Development", "UI/UX", "Business"], 260 | "housing": true, 261 | "jobAssistance": true, 262 | "jobGuarantee": false, 263 | "acceptGi": true 264 | }' \ 265 | -H "Content-Type: application/json" \ 266 | -X POST localhost:5000/api/v1/bootcamps \ 267 | | python -m json.tool 268 | 269 |
270 | 271 | ``` 272 | *** 273 | "slug": "devworks-bootcamp", 274 | *** 275 | ``` 276 | 277 |
278 | 279 | ### 7. GeoJSON Location & Geocoder Hook - MapQuest API 280 | 281 | register 282 | 283 | https://developer.mapquest.com/ 284 | 285 | Manage Keys --> My Application's Key --> Consumer Key --> insert to config 286 | 287 |
288 | 289 | $ npm install --save node-geocoder 290 | 291 |
292 | 293 | We deleted all documents in the database 294 | 295 |
296 | 297 | $ curl -d '{ 298 | "user": "5d7a514b5d2c12c7449be045", 299 | "name": "Devworks Bootcamp", 300 | "description": "Devworks is a full stack JavaScript Bootcamp located in the heart of Boston that focuses on the technologies you need to get a high paying job as a web developer", 301 | "website": "https://devworks.com", 302 | "phone": "(111) 111-1111", 303 | "email": "enroll@devworks.com", 304 | "address": "233 Bay State Rd Boston MA 02215", 305 | "careers": ["Web Development", "UI/UX", "Business"], 306 | "housing": true, 307 | "jobAssistance": true, 308 | "jobGuarantee": false, 309 | "acceptGi": true 310 | }' \ 311 | -H "Content-Type: application/json" \ 312 | -X POST localhost:5000/api/v1/bootcamps \ 313 | | python -m json.tool 314 | 315 |
316 | 317 | ## 6. Mongoose Advanced Querying & Relationships 318 | 319 |
320 | 321 | ### 1. Database Seeder For Bootcamps 322 | 323 | // ImportData 324 | $ node seeder -i 325 | 326 | // DestroyData 327 | $ node seeder -d 328 | 329 |
330 | 331 | ### 2. Geospatial Query - Get Bootcamps Within Radius 332 | 333 | $ curl \ 334 | -H "Content-Type: application/json" \ 335 | -X GET localhost:5000/api/v1/bootcamps/radius/02118/10 \ 336 | | python -m json.tool 337 | 338 |
339 | 340 | ### 3. Advanced Filtering 341 | 342 | $ curl \ 343 | -H "Content-Type: application/json" \ 344 | -X GET localhost:5000/api/v1/bootcamps?careers[in]=Business \ 345 | | python -m json.tool 346 | 347 |
348 | 349 | ### 4. Select & Sorting 350 | 351 | http://localhost:5000/api/v1/bootcamps?select=name,description,housing&housing=true 352 | 353 |
354 | 355 | http://localhost:5000/api/v1/bootcamps?select=name,description,housing&sort=name 356 | 357 |
358 | 359 | http://localhost:5000/api/v1/bootcamps?select=name,description,housing&sort=-name 360 | 361 |
362 | 363 | ### 5. Adding Pagination 364 | 365 | http://localhost:5000/api/v1/bootcamps?page=2 366 | 367 |
368 | 369 | ### 6. Course Model & Seeding 370 | 371 | // DestroyData 372 | $ node seeder -d 373 | 374 | // ImportData 375 | $ node seeder -i 376 | 377 |
378 | 379 | ### 7. Course Routes & Controller 380 | 381 | http://localhost:5000/api/v1/courses 382 | 383 | http://localhost:5000/api/v1/bootcamps/5d713995b721c3bb38c1f5d0/courses 384 | 385 |
386 | 387 | ### 8. Populate, Virtuals & Cascade Delete 388 | 389 | http://localhost:5000/api/v1/bootcamps 390 | 391 |
392 | 393 | $ curl \ 394 | -H "Content-Type: application/json" \ 395 | -X DELETE localhost:5000/api/v1/bootcamps/5d725a1b7b292f5f8ceff788 \ 396 | | python -m json.tool 397 | 398 |
399 | 400 | ### 9. Single Course & Add Course 401 | 402 | http://localhost:5000/api/v1/courses/5d725a4a7b292f5f8ceff789 403 | 404 | http://localhost:5000/api/v1/bootcamps/ 405 | 406 |
407 | 408 | $ curl -d '{ 409 | "title": "Front End Web Development", 410 | "description": "This course will provide you with all of the essentials to become a successful frontend web developer. You will learn to master HTML, CSS and front end JavaScript, along with tools like Git, VSCode and front end frameworks like Vue", 411 | "weeks": 8, 412 | "tuition": 8000, 413 | "minimumSkill": "beginner", 414 | "scholarhipsAvailable": true 415 | }' \ 416 | -H "Content-Type: application/json" \ 417 | -X POST localhost:5000/api/v1/bootcamps/5d713995b721c3bb38c1f5d0/courses \ 418 | | python -m json.tool 419 | 420 |
421 | 422 | $ curl -d '{ 423 | "title": "Full Stack Web Development", 424 | "description": "In this course you will learn full stack web development, first learning all about the frontend with HTML/CSS/JS/Vue and then the backend with Node.js/Express/MongoDB", 425 | "weeks": 12, 426 | "tuition": 10000, 427 | "minimumSkill": "intermediate", 428 | "scholarhipsAvailable": true 429 | }' \ 430 | -H "Content-Type: application/json" \ 431 | -X POST localhost:5000/api/v1/bootcamps/5d713995b721c3bb38c1f5d0/courses \ 432 | | python -m json.tool 433 | 434 |
435 | 436 | http://localhost:5000/api/v1/bootcamps/ 437 | 438 |
439 | 440 | ### 10. Update & Delete Course 441 | 442 | // DestroyData 443 | $ node seeder -d 444 | 445 | // ImportData 446 | $ node seeder -i 447 | 448 |
449 | 450 | http://localhost:5000/api/v1/courses 451 | 452 |
453 | 454 | $ curl -d '{ 455 | "tuition": 13000, 456 | "minimumSkill": "advanced" 457 | }' \ 458 | -H "Content-Type: application/json" \ 459 | -X PUT localhost:5000/api/v1/courses/5d725a4a7b292f5f8ceff789 \ 460 | | python -m json.tool 461 | 462 |
463 | 464 | http://localhost:5000/api/v1/courses/5d725a4a7b292f5f8ceff789 465 | 466 |
467 | 468 | $ curl \ 469 | -H "Content-Type: application/json" \ 470 | -X DELETE localhost:5000/api/v1/courses/5d725a4a7b292f5f8ceff789 \ 471 | | python -m json.tool 472 | 473 |
474 | 475 | ### 11. Aggregate - Calculating The Average Course Cost 476 | 477 |
478 | 479 | ### 12. Photo Upload For Bootcamp 480 | 481 |
482 | 483 | $ npm install --save express-fileupload 484 | 485 |
486 | 487 | $ curl \ 488 | -F "file=@/home/marley/1/pic1.jpg" \ 489 | -X PUT localhost:5000/api/v1/bootcamps/5d725a1b7b292f5f8ceff788/photo \ 490 | | python -m json.tool 491 | 492 |
493 | 494 | http://localhost:5000/uploads/photo_5d725a1b7b292f5f8ceff788.jpg 495 | 496 |
497 | 498 | ![Application](/img/pic-06-12.png?raw=true) 499 | 500 |
501 | 502 | ### 13. Advanced Results Middleware 503 | 504 | http://localhost:5000/api/v1/bootcamps/ 505 | 506 | http://localhost:5000/api/v1/bootcamps?page=2 507 | 508 | http://localhost:5000/api/v1/bootcamps?select=name,description 509 | 510 | http://localhost:5000/api/v1/courses?select=title 511 | 512 | http://localhost:5000/api/v1/courses?page=2&limit=2 513 | 514 |
515 | 516 | ## 7. Authentication, Users & Permissions - Part 1 517 | 518 |
519 | 520 | ### 1. User Model 521 | 522 | $ npm install --save jsonwebtoken 523 | $ npm install --save bcryptjs 524 | 525 |
526 | 527 | ### 2. User Register & Encrypting Passwords 528 | 529 |
530 | 531 | $ curl \ 532 | -d '{"name": "John Doe", 533 | "email": "john@gmail.com", 534 | "password": "123456", 535 | "role": "publisher"}' \ 536 | -H "Content-Type: application/json" \ 537 | -X POST localhost:5000/api/v1/auth/register \ 538 | | python -m json.tool 539 | 540 |
541 | 542 | ### 3. Sign & Get JSON Web Token 543 | 544 | https://jwt.io/ 545 | 546 | $ curl \ 547 | -d '{"name": "John Doe", 548 | "email": "john@gmail.com", 549 | "password": "123456", 550 | "role": "publisher"}' \ 551 | -H "Content-Type: application/json" \ 552 | -X POST localhost:5000/api/v1/auth/register \ 553 | | python -m json.tool 554 | 555 |
556 | 557 | ### 4. User Login 558 | 559 | $ curl \ 560 | -d '{ 561 | "email": "john@gmail.com", 562 | "password": "123456" 563 | }' \ 564 | -H "Content-Type: application/json" \ 565 | -X POST localhost:5000/api/v1/auth/login \ 566 | | python -m json.tool 567 | 568 |
569 | 570 | ### 5. Sending JWT In a Cookie 571 | 572 | $ npm install --save cookie-parser 573 | 574 |
575 | 576 | ### 6. Auth Protect Middleware 577 | 578 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmI0YTg5OWE1ODE1M2ZmNjEzYWEzOSIsImlhdCI6MTU3MjU1NTQwMSwiZXhwIjoxNTc1MTQ3NDAxfQ.2G0jVvVPpHPem-SEGLGg3-_JMmYqnOsIuY3RjhVkfQY" 579 | 580 |
581 | 582 | $ curl -d '{ 583 | "user": "5d7a514b5d2c12c7449be045", 584 | "name": "Devworks Bootcamp", 585 | "description": "Devworks is a full stack JavaScript Bootcamp located in the heart of Boston that focuses on the technologies you need to get a high paying job as a web developer", 586 | "website": "https://devworks.com", 587 | "phone": "(111) 111-1111", 588 | "email": "enroll@devworks.com", 589 | "address": "233 Bay State Rd Boston MA 02215", 590 | "careers": ["Web Development", "UI/UX", "Business"], 591 | "housing": true, 592 | "jobAssistance": true, 593 | "jobGuarantee": false, 594 | "acceptGi": true 595 | }' \ 596 | -H "Content-Type: application/json" \ 597 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmI0YTg5OWE1ODE1M2ZmNjEzYWEzOSIsImlhdCI6MTU3MjU1NTQwMSwiZXhwIjoxNTc1MTQ3NDAxfQ.2G0jVvVPpHPem-SEGLGg3-_JMmYqnOsIuY3RjhVkfQY" \ 598 | -X POST localhost:5000/api/v1/bootcamps \ 599 | | python -m json.tool 600 | 601 |
602 | 603 | $ curl \ 604 | -H "Content-Type: application/json" \ 605 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmI0YTg5OWE1ODE1M2ZmNjEzYWEzOSIsImlhdCI6MTU3MjU1NTQwMSwiZXhwIjoxNTc1MTQ3NDAxfQ.2G0jVvVPpHPem-SEGLGg3-_JMmYqnOsIuY3RjhVkfQY" \ 606 | -X GET localhost:5000/api/v1/auth/me \ 607 | | python -m json.tool 608 | 609 |
610 | 611 | ### 7. Storing The Token In Postman 612 | 613 |
614 | 615 | ### 8. Role Authorization 616 | 617 | Only user 'publisher' and 'admin' can do actions to create / update / delete 618 | 619 |
620 | 621 | ## 8. Authentication, Users & Permissions - Part 2 622 | 623 |
624 | 625 | ### 1. Bootcamp & User Relationship 626 | 627 | // DestroyData 628 | $ node seeder -d 629 | 630 | // ImportData 631 | $ node seeder -i 632 | 633 |
634 | 635 | http://localhost:5000/api/v1/bootcamps/ 636 | 637 |
638 | 639 | ### 2. Bootcamp Ownership 640 | 641 | Only onwer or admin can modify bootcamp 642 | 643 |
644 | 645 | ### 3. Course Ownership 646 | 647 | Only onwer or admin can modify course 648 | 649 |
650 | 651 | ### 4. Forgot Password - Generate Token 652 | 653 | $ curl \ 654 | -d '{ 655 | "email": "john@gmail.com" 656 | }' \ 657 | -H "Content-Type: application/json" \ 658 | -X POST localhost:5000/api/v1/auth/forgotpassword \ 659 | | python -m json.tool 660 | 661 |
662 | 663 | ### 5. Forgot Password - Send Email 664 | 665 | https://mailtrap.io/ 666 | http://nodemailer.com/about/ 667 | 668 | $ npm install --save nodemailer 669 | 670 |
671 | 672 | $ curl \ 673 | -d '{ 674 | "email": "john@gmail.com" 675 | }' \ 676 | -H "Content-Type: application/json" \ 677 | -X POST localhost:5000/api/v1/auth/forgotpassword \ 678 | | python -m json.tool 679 | 680 |
681 | 682 | ![Application](/img/pic-08-05-01.png?raw=true) 683 | 684 |
685 | 686 | ![Application](/img/pic-08-05-02.png?raw=true) 687 | 688 |
689 | 690 | ### 6. Reset Password 691 | 692 | $ curl \ 693 | -d '{ 694 | "password": "654321" 695 | }' \ 696 | -H "Content-Type: application/json" \ 697 | -X PUT http://localhost:5000/api/v1/auth/resetpassword/bcedda5593f1799bd34ba1a49608f92a0434d154 \ 698 | | python -m json.tool 699 | 700 |
701 | 702 | // Invalid 703 | $ curl \ 704 | -d '{ 705 | "email": "john@gmail.com", 706 | "password": "123456" 707 | }' \ 708 | -H "Content-Type: application/json" \ 709 | -X POST localhost:5000/api/v1/auth/login \ 710 | | python -m json.tool 711 | 712 |
713 | 714 | // Valid 715 | $ curl \ 716 | -d '{ 717 | "email": "john@gmail.com", 718 | "password": "654321" 719 | }' \ 720 | -H "Content-Type: application/json" \ 721 | -X POST localhost:5000/api/v1/auth/login \ 722 | | python -m json.tool 723 | 724 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk 725 | 726 |
727 | 728 | ### 7. Update User Details 729 | 730 | $ curl \ 731 | -d '{ 732 | "email": "john@gmail.com", 733 | "name": "John Smith" 734 | }' \ 735 | -H "Content-Type: application/json" \ 736 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 737 | -X PUT localhost:5000/api/v1/auth/updatedetails \ 738 | | python -m json.tool 739 | 740 |
741 | 742 | $ curl \ 743 | -d '{ 744 | "currentPassword": "654321", 745 | "newPassword": "123456" 746 | }' \ 747 | -H "Content-Type: application/json" \ 748 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 749 | -X PUT localhost:5000/api/v1/auth/updatepassword \ 750 | | python -m json.tool 751 | 752 |
753 | 754 | ### 8. Admin Users CRUD 755 | 756 | mongodb -> set role "admin" to user. 757 | 758 |
759 | 760 | // Me 761 | $ curl \ 762 | -H "Content-Type: application/json" \ 763 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 764 | -X GET localhost:5000/api/v1/auth/me \ 765 | | python -m json.tool 766 | 767 |
768 | 769 | // Get all user 770 | $ curl \ 771 | -H "Content-Type: application/json" \ 772 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 773 | -X GET localhost:5000/api/v1/users \ 774 | | python -m json.tool 775 | 776 |
777 | 778 | // Get single user 779 | $ curl \ 780 | -H "Content-Type: application/json" \ 781 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 782 | -X GET localhost:5000/api/v1/users/5c8a1d5b0190b214360dc032 \ 783 | | python -m json.tool 784 | 785 |
786 | 787 | // Get single user 788 | $ curl \ 789 | -H "Content-Type: application/json" \ 790 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 791 | -X GET localhost:5000/api/v1/users/5c8a1d5b0190b214360dc032 \ 792 | | python -m json.tool 793 | 794 |
795 | 796 | // Create user 797 | $ curl \ 798 | -d '{ 799 | "name": "Nate Smith", 800 | "email": "nate@gmail.com", 801 | "password": "123456" 802 | }' \ 803 | -H "Content-Type: application/json" \ 804 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 805 | -X POST localhost:5000/api/v1/users/ \ 806 | | python -m json.tool 807 | 808 |
809 | 810 | // Update user 811 | $ curl \ 812 | -d '{ 813 | "name": "Nate Johnson" 814 | }' \ 815 | -H "Content-Type: application/json" \ 816 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 817 | -X PUT localhost:5000/api/v1/users/5dbbd7c42041035e7eb80426 \ 818 | | python -m json.tool 819 | 820 |
821 | 822 | // Delete user 823 | $ curl \ 824 | -H "Content-Type: application/json" \ 825 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkN2E1MTRiNWQyYzEyYzc0NDliZTA0NSIsImlhdCI6MTU3MjU4NTY1OCwiZXhwIjoxNTc1MTc3NjU4fQ.vhxaMRCksKb0LHx5T91JqrX4xo0i2Im_BOuv3vShmXk" \ 826 | -X DELETE localhost:5000/api/v1/users/5dbbd7c42041035e7eb80426 \ 827 | | python -m json.tool 828 | 829 |
830 | 831 | ## 9. Bootcamp Reviews & Ratings 832 | 833 |
834 | 835 | ### 1. Review Model & Get Reviews 836 | 837 |
838 | 839 | ### 2. Get Single Review & Update Seeder 840 | 841 | // DestroyData 842 | $ node seeder -d 843 | 844 | // ImportData 845 | $ node seeder -i 846 | 847 |
848 | 849 | // Get all reviews 850 | $ curl \ 851 | -H "Content-Type: application/json" \ 852 | -X GET localhost:5000/api/v1/reviews \ 853 | | python -m json.tool 854 | 855 |
856 | 857 | $ curl \ 858 | -H "Content-Type: application/json" \ 859 | -X GET localhost:5000/api/v1/reviews/5d7a514b5d2c12c7449be020 \ 860 | | python -m json.tool 861 | 862 |
863 | 864 | $ curl \ 865 | -H "Content-Type: application/json" \ 866 | -X GET localhost:5000/api/v1/bootcamps/5d725a1b7b292f5f8ceff788/reviews \ 867 | | python -m json.tool 868 | 869 |
870 | 871 | ### 3. Add Review For Bootcamp 872 | 873 | User with 'publisher' role shouldn't create reviews 874 | 875 | $ curl \ 876 | -d '{ 877 | "email": "greg@gmail.com", 878 | "password": "123456" 879 | }' \ 880 | -H "Content-Type: application/json" \ 881 | -X POST localhost:5000/api/v1/auth/login \ 882 | | python -m json.tool 883 | 884 |
885 | 886 | $ curl \ 887 | -H "Content-Type: application/json" \ 888 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjOGExZDViMDE5MGIyMTQzNjBkYzAzMyIsImlhdCI6MTU3MjYwNjQ5NSwiZXhwIjoxNTc1MTk4NDk1fQ.lgUqJEJDp9dShq4HeA9-CiiTt9zfB-7ZVaRotI928l0" \ 889 | -X GET localhost:5000/api/v1/auth/me \ 890 | | python -m json.tool 891 | 892 |
893 | 894 | $ curl \ 895 | -d '{ 896 | "title": "Nice Bootcamp", 897 | "text": "I learned a lot", 898 | "rating": 8 899 | }' \ 900 | -H "Content-Type: application/json" \ 901 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjOGExZDViMDE5MGIyMTQzNjBkYzAzMyIsImlhdCI6MTU3MjYwNjQ5NSwiZXhwIjoxNTc1MTk4NDk1fQ.lgUqJEJDp9dShq4HeA9-CiiTt9zfB-7ZVaRotI928l0" \ 902 | -X POST localhost:5000/api/v1/bootcamps/5d725a1b7b292f5f8ceff788/reviews/ \ 903 | | python -m json.tool 904 | 905 |
906 | 907 | ### 4. Aggregate - Calculate Average Rating 908 | 909 | // DestroyData 910 | $ node seeder -d 911 | 912 |
913 | 914 | $ curl \ 915 | -d '{"name": "John Doe", 916 | "email": "john@gmail.com", 917 | "password": "123456", 918 | "role": "user"}' \ 919 | -H "Content-Type: application/json" \ 920 | -X POST localhost:5000/api/v1/auth/register \ 921 | | python -m json.tool 922 | 923 |
924 | 925 | $ curl \ 926 | -d '{"name": "Jack Smith", 927 | "email": "jack@gmail.com", 928 | "password": "123456", 929 | "role": "user"}' \ 930 | -H "Content-Type: application/json" \ 931 | -X POST localhost:5000/api/v1/auth/register \ 932 | | python -m json.tool 933 | 934 |
935 | 936 | $ curl \ 937 | -d '{"name": "Mary Smith", 938 | "email": "mary@gmail.com", 939 | "password": "123456", 940 | "role": "user"}' \ 941 | -H "Content-Type: application/json" \ 942 | -X POST localhost:5000/api/v1/auth/register \ 943 | | python -m json.tool 944 | 945 |
946 | 947 | // Login 948 | $ curl \ 949 | -d '{ 950 | "email": "john@gmail.com", 951 | "password": "123456" 952 | }' \ 953 | -H "Content-Type: application/json" \ 954 | -X POST localhost:5000/api/v1/auth/login \ 955 | | python -m json.tool 956 | 957 |
958 | 959 | Did not test. Need to create a bootcamp, then create review and check average rating for 3 users. User with role 'user' has no premission to create bootcamp. 960 | 961 |
962 | 963 | ### 5. Update & Delete Reviews 964 | 965 | // DestroyData 966 | $ node seeder -d 967 | 968 | // ImportData 969 | $ node seeder -i 970 | 971 |
972 | 973 | // Login 974 | $ curl \ 975 | -d '{ 976 | "email": "greg@gmail.com", 977 | "password": "123456" 978 | }' \ 979 | -H "Content-Type: application/json" \ 980 | -X POST localhost:5000/api/v1/auth/login \ 981 | | python -m json.tool 982 | 983 |
984 | 985 | // Get all reviews 986 | $ curl \ 987 | -H "Content-Type: application/json" \ 988 | -X GET localhost:5000/api/v1/reviews \ 989 | | python -m json.tool 990 | 991 |
992 | 993 | $ curl \ 994 | -d '{ 995 | "title": "Had Fun", 996 | "text": "Super", 997 | "rating": 10 998 | }' \ 999 | -H "Content-Type: application/json" \ 1000 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjOGExZDViMDE5MGIyMTQzNjBkYzAzMyIsImlhdCI6MTU3MjYwOTE4NiwiZXhwIjoxNTc1MjAxMTg2fQ.3fY2OVWxj7YWxD3vfhr459MX0vb4ewtSm9BU8nWQrfc" \ 1001 | -X PUT localhost:5000/api/v1/reviews/5d7a514b5d2c12c7449be020 \ 1002 | | python -m json.tool 1003 | 1004 |
1005 | 1006 | $ curl \ 1007 | -H "Content-Type: application/json" \ 1008 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjOGExZDViMDE5MGIyMTQzNjBkYzAzMyIsImlhdCI6MTU3MjYwOTE4NiwiZXhwIjoxNTc1MjAxMTg2fQ.3fY2OVWxj7YWxD3vfhr459MX0vb4ewtSm9BU8nWQrfc" \ 1009 | -X DELETE localhost:5000/api/v1/reviews/5d7a514b5d2c12c7449be020 \ 1010 | | python -m json.tool 1011 | 1012 |
1013 | 1014 | ## 10. API Security 1015 | 1016 |
1017 | 1018 | ### 1. Logout To Clear Token Cookie 1019 | 1020 | // Logout 1021 | $ curl \ 1022 | -H "Content-Type: application/json" \ 1023 | -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjOGExZDViMDE5MGIyMTQzNjBkYzAzMyIsImlhdCI6MTU3MjYwNjQ5NSwiZXhwIjoxNTc1MTk4NDk1fQ.lgUqJEJDp9dShq4HeA9-CiiTt9zfB-7ZVaRotI928l0" \ 1024 | -X GET localhost:5000/api/v1/auth/logout \ 1025 | | python -m json.tool 1026 | 1027 |
1028 | 1029 | ### 2. Prevent NoSQL Injection & Sanitize Data 1030 | 1031 | $ npm install --save express-mongo-sanitize 1032 | 1033 |
1034 | 1035 | ### 3. XSS Protection & Security Headers 1036 | 1037 | https://helmetjs.github.io/ 1038 | 1039 | $ npm install --save helmet 1040 | 1041 | https://github.com/jsonmaur/xss-clean 1042 | 1043 | $ npm install --save xss-clean 1044 | 1045 |
1046 | 1047 | ### 4. Rate Limiting, HPP & CORS 1048 | 1049 | $ npm install --save express-rate-limit 1050 | $ npm install --save hpp 1051 | 1052 | https://github.com/expressjs/cors 1053 | 1054 | $ npm install --save cors 1055 | 1056 |
1057 | 1058 | ## 11. Documentation & Deploy 1059 | 1060 | **Steps to deploy:** 1061 | https://gist.github.com/bradtraversy/cd90d1ed3c462fe3bddd11bf8953a896 1062 | 1063 |
1064 | 1065 | ### 1. Documentation With Postman & Docgen 1066 | 1067 |
1068 | 1069 | ### 2. Digital Ocean Droplet & Server Log In 1070 | 1071 |
1072 | 1073 | ### 3. Prepare & Push To Github 1074 | 1075 |
1076 | 1077 | ### 4. Clone Repo On Server 1078 | 1079 |
1080 | 1081 | ### 5. PM2 Process Manager Setup 1082 | 1083 |
1084 | 1085 | ### 6. NGINX Reverse Proxy Setup 1086 | 1087 |
1088 | 1089 | ### 7. Domain, SSL & Wrap Up 1090 | 1091 |
1092 | 1093 | --- 1094 | 1095 |
1096 | 1097 | **Marley** 1098 | 1099 | Any questions on eng: https://jsdev.org/chat/ 1100 | Любые вопросы на русском: https://jsdev.ru/chat/ 1101 | --------------------------------------------------------------------------------