├── .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 | 
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 | 
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 | 
683 |
684 |
685 |
686 | 
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 |
--------------------------------------------------------------------------------