├── .gitignore ├── _data ├── bootcamps.json ├── courses.json ├── reviews.json └── users.json ├── config ├── config.env.env └── db.js ├── controllers ├── auth.js ├── bootcamps.js ├── courses.js ├── reviews.js └── users.js ├── middleware ├── advancedResults.js ├── async.js ├── auth.js ├── error.js └── logger.js ├── models ├── Bootcamp.js ├── Course.js ├── Review.js └── User.js ├── package-lock.json ├── package.json ├── public ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 └── index.html ├── readme.md ├── routes ├── auth.js ├── bootcamps.js ├── courses.js ├── reviews.js └── users.js ├── seeder.js ├── server.js └── utils ├── errorResponse.js ├── geocoder.js └── sendEmail.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | config/config.env -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | "scholarshipsAvailable": 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 | "scholarshipsAvailable": 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 | "scholarshipsAvailable": 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 | "scholarshipsAvailable": 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": 12000, 52 | "minimumSkill": "beginner", 53 | "scholarshipsAvailable": 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": 9000, 63 | "minimumSkill": "intermediate", 64 | "scholarshipsAvailable": 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 | "scholarshipsAvailable": 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 | "scholarshipsAvailable": 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 | "scholarshipsAvailable": false, 98 | "bootcamp": "5d725a1b7b292f5f8ceff788", 99 | "user": "5c8a1d5b0190b214360dc032" 100 | } 101 | ] 102 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /_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 | -------------------------------------------------------------------------------- /config/config.env.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=5000 3 | 4 | MONGO_URI= 5 | 6 | GEOCODER_PROVIDER=mapquest 7 | GEOCODER_API_KEY= 8 | 9 | FILE_UPLOAD_PATH= ./public/uploads 10 | MAX_FILE_UPLOAD=1000000 11 | 12 | JWT_SECRET= 13 | JWT_EXPIRE=30d 14 | JWT_COOKIE_EXPIRE=30 15 | 16 | SMTP_HOST=smtp.mailtrap.io 17 | SMTP_PORT=2525 18 | SMTP_EMAIL= 19 | SMTP_PASSWORD= 20 | FROM_EMAIL= 21 | FROM_NAME= -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /controllers/auth.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const ErrorResponse = require('../utils/errorResponse'); 3 | const asyncHandler = require('../middleware/async'); 4 | const sendEmail = require('../utils/sendEmail'); 5 | const User = require('../models/User'); 6 | 7 | // @desc Register user 8 | // @route POST /api/v1/auth/register 9 | // @access Public 10 | exports.register = asyncHandler(async (req, res, next) => { 11 | const { name, email, password, role } = req.body; 12 | 13 | // Create user 14 | const user = await User.create({ 15 | name, 16 | email, 17 | password, 18 | role, 19 | }); 20 | 21 | // grab token and send to email 22 | const confirmEmailToken = user.generateEmailConfirmToken(); 23 | 24 | // Create reset url 25 | const confirmEmailURL = `${req.protocol}://${req.get( 26 | 'host', 27 | )}/api/v1/auth/confirmemail?token=${confirmEmailToken}`; 28 | 29 | const message = `You are receiving this email because you need to confirm your email address. Please make a GET request to: \n\n ${confirmEmailURL}`; 30 | 31 | user.save({ validateBeforeSave: false }); 32 | 33 | const sendResult = await sendEmail({ 34 | email: user.email, 35 | subject: 'Email confirmation token', 36 | message, 37 | }); 38 | 39 | sendTokenResponse(user, 200, res); 40 | }); 41 | 42 | // @desc Login user 43 | // @route POST /api/v1/auth/login 44 | // @access Public 45 | exports.login = asyncHandler(async (req, res, next) => { 46 | const { email, password } = req.body; 47 | 48 | // Validate emil & password 49 | if (!email || !password) { 50 | return next(new ErrorResponse('Please provide an email and password', 400)); 51 | } 52 | 53 | // Check for user 54 | const user = await User.findOne({ email }).select('+password'); 55 | 56 | if (!user) { 57 | return next(new ErrorResponse('Invalid credentials', 401)); 58 | } 59 | 60 | // Check if password matches 61 | const isMatch = await user.matchPassword(password); 62 | 63 | if (!isMatch) { 64 | return next(new ErrorResponse('Invalid credentials', 401)); 65 | } 66 | 67 | sendTokenResponse(user, 200, res); 68 | }); 69 | 70 | // @desc Log user out / clear cookie 71 | // @route GET /api/v1/auth/logout 72 | // @access Public 73 | exports.logout = asyncHandler(async (req, res, next) => { 74 | res.cookie('token', 'none', { 75 | expires: new Date(Date.now() + 10 * 1000), 76 | httpOnly: true, 77 | }); 78 | 79 | res.status(200).json({ 80 | success: true, 81 | data: {}, 82 | }); 83 | }); 84 | 85 | // @desc Get current logged in user 86 | // @route GET /api/v1/auth/me 87 | // @access Private 88 | exports.getMe = asyncHandler(async (req, res, next) => { 89 | // user is already available in req due to the protect middleware 90 | const user = req.user; 91 | 92 | res.status(200).json({ 93 | success: true, 94 | data: user, 95 | }); 96 | }); 97 | 98 | // @desc Update user details 99 | // @route PUT /api/v1/auth/updatedetails 100 | // @access Private 101 | exports.updateDetails = asyncHandler(async (req, res, next) => { 102 | const fieldsToUpdate = { 103 | name: req.body.name, 104 | email: req.body.email, 105 | }; 106 | 107 | const user = await User.findByIdAndUpdate(req.user.id, fieldsToUpdate, { 108 | new: true, 109 | runValidators: true, 110 | }); 111 | 112 | res.status(200).json({ 113 | success: true, 114 | data: user, 115 | }); 116 | }); 117 | 118 | // @desc Update password 119 | // @route PUT /api/v1/auth/updatepassword 120 | // @access Private 121 | exports.updatePassword = asyncHandler(async (req, res, next) => { 122 | const user = await User.findById(req.user.id).select('+password'); 123 | 124 | // Check current password 125 | if (!(await user.matchPassword(req.body.currentPassword))) { 126 | return next(new ErrorResponse('Password is incorrect', 401)); 127 | } 128 | 129 | user.password = req.body.newPassword; 130 | await user.save(); 131 | 132 | sendTokenResponse(user, 200, res); 133 | }); 134 | 135 | // @desc Forgot password 136 | // @route POST /api/v1/auth/forgotpassword 137 | // @access Public 138 | exports.forgotPassword = asyncHandler(async (req, res, next) => { 139 | const user = await User.findOne({ email: req.body.email }); 140 | 141 | if (!user) { 142 | return next(new ErrorResponse('There is no user with that email', 404)); 143 | } 144 | 145 | // Get reset token 146 | const resetToken = user.getResetPasswordToken(); 147 | 148 | await user.save({ validateBeforeSave: false }); 149 | 150 | // Create reset url 151 | const resetUrl = `${req.protocol}://${req.get( 152 | 'host', 153 | )}/api/v1/auth/resetpassword/${resetToken}`; 154 | 155 | 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}`; 156 | 157 | try { 158 | await sendEmail({ 159 | email: user.email, 160 | subject: 'Password reset token', 161 | message, 162 | }); 163 | 164 | res.status(200).json({ success: true, data: 'Email sent' }); 165 | } catch (err) { 166 | console.log(err); 167 | user.resetPasswordToken = undefined; 168 | user.resetPasswordExpire = undefined; 169 | 170 | await user.save({ validateBeforeSave: false }); 171 | 172 | return next(new ErrorResponse('Email could not be sent', 500)); 173 | } 174 | }); 175 | 176 | // @desc Reset password 177 | // @route PUT /api/v1/auth/resetpassword/:resettoken 178 | // @access Public 179 | exports.resetPassword = asyncHandler(async (req, res, next) => { 180 | // Get hashed token 181 | const resetPasswordToken = crypto 182 | .createHash('sha256') 183 | .update(req.params.resettoken) 184 | .digest('hex'); 185 | 186 | const user = await User.findOne({ 187 | resetPasswordToken, 188 | resetPasswordExpire: { $gt: Date.now() }, 189 | }); 190 | 191 | if (!user) { 192 | return next(new ErrorResponse('Invalid token', 400)); 193 | } 194 | 195 | // Set new password 196 | user.password = req.body.password; 197 | user.resetPasswordToken = undefined; 198 | user.resetPasswordExpire = undefined; 199 | await user.save(); 200 | 201 | sendTokenResponse(user, 200, res); 202 | }); 203 | 204 | /** 205 | * @desc Confirm Email 206 | * @route GET /api/v1/auth/confirmemail 207 | * @access Public 208 | */ 209 | exports.confirmEmail = asyncHandler(async (req, res, next) => { 210 | // grab token from email 211 | const { token } = req.query; 212 | 213 | if (!token) { 214 | return next(new ErrorResponse('Invalid Token', 400)); 215 | } 216 | 217 | const splitToken = token.split('.')[0]; 218 | const confirmEmailToken = crypto 219 | .createHash('sha256') 220 | .update(splitToken) 221 | .digest('hex'); 222 | 223 | // get user by token 224 | const user = await User.findOne({ 225 | confirmEmailToken, 226 | isEmailConfirmed: false, 227 | }); 228 | 229 | if (!user) { 230 | return next(new ErrorResponse('Invalid Token', 400)); 231 | } 232 | 233 | // update confirmed to true 234 | user.confirmEmailToken = undefined; 235 | user.isEmailConfirmed = true; 236 | 237 | // save 238 | user.save({ validateBeforeSave: false }); 239 | 240 | // return token 241 | sendTokenResponse(user, 200, res); 242 | }); 243 | 244 | // Get token from model, create cookie and send response 245 | const sendTokenResponse = (user, statusCode, res) => { 246 | // Create token 247 | const token = user.getSignedJwtToken(); 248 | 249 | const options = { 250 | expires: new Date( 251 | Date.now() + process.env.JWT_COOKIE_EXPIRE * 24 * 60 * 60 * 1000, 252 | ), 253 | httpOnly: true, 254 | }; 255 | 256 | if (process.env.NODE_ENV === 'production') { 257 | options.secure = true; 258 | } 259 | 260 | res.status(statusCode).cookie('token', token, options).json({ 261 | success: true, 262 | token, 263 | }); 264 | }; 265 | -------------------------------------------------------------------------------- /controllers/bootcamps.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slugify = require("slugify"); 3 | const ErrorResponse = require('../utils/errorResponse'); 4 | const asyncHandler = require('../middleware/async'); 5 | const geocoder = require('../utils/geocoder'); 6 | const Bootcamp = require('../models/Bootcamp'); 7 | 8 | // @desc Get all bootcamps 9 | // @route GET /api/v1/bootcamps 10 | // @access Public 11 | exports.getBootcamps = asyncHandler(async (req, res, next) => { 12 | res.status(200).json(res.advancedResults); 13 | }); 14 | 15 | // @desc Get single bootcamp 16 | // @route GET /api/v1/bootcamps/:id 17 | // @access Public 18 | exports.getBootcamp = asyncHandler(async (req, res, next) => { 19 | const bootcamp = await Bootcamp.findById(req.params.id); 20 | 21 | if (!bootcamp) { 22 | return next( 23 | new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) 24 | ); 25 | } 26 | 27 | res.status(200).json({ success: true, data: bootcamp }); 28 | }); 29 | 30 | // @desc Create new bootcamp 31 | // @route POST /api/v1/bootcamps 32 | // @access Private 33 | exports.createBootcamp = asyncHandler(async (req, res, next) => { 34 | // Add user to req,body 35 | req.body.user = req.user.id; 36 | 37 | // Check for published bootcamp 38 | const publishedBootcamp = await Bootcamp.findOne({ user: req.user.id }); 39 | 40 | // If the user is not an admin, they can only add one bootcamp 41 | if (publishedBootcamp && req.user.role !== 'admin') { 42 | return next( 43 | new ErrorResponse( 44 | `The user with ID ${req.user.id} has already published a bootcamp`, 45 | 400 46 | ) 47 | ); 48 | } 49 | 50 | const bootcamp = await Bootcamp.create(req.body); 51 | 52 | res.status(201).json({ 53 | success: true, 54 | data: bootcamp 55 | }); 56 | }); 57 | 58 | // @desc Update bootcamp 59 | // @route PUT /api/v1/bootcamps/:id 60 | // @access Private 61 | exports.updateBootcamp = asyncHandler(async (req, res, next) => { 62 | let bootcamp = await Bootcamp.findById(req.params.id); 63 | 64 | if (!bootcamp) { 65 | return next( 66 | new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) 67 | ); 68 | } 69 | 70 | // Make sure user is bootcamp owner 71 | if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { 72 | return next( 73 | new ErrorResponse( 74 | `User ${req.user.id} is not authorized to update this bootcamp`, 75 | 401 76 | ) 77 | ); 78 | } 79 | 80 | // update slug while updating name 81 | if (Object.keys(req.body).includes("name")) { 82 | req.body.slug = slugify(req.body.name, { lower: true }); 83 | } 84 | 85 | bootcamp = await Bootcamp.findByIdAndUpdate(req.params.id, req.body, { 86 | new: true, 87 | runValidators: true 88 | }); 89 | 90 | res.status(200).json({ success: true, data: bootcamp }); 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.user.id} is not authorized to delete this bootcamp`, 110 | 401 111 | ) 112 | ); 113 | } 114 | 115 | await bootcamp.remove(); 116 | 117 | res.status(200).json({ success: true, data: {} }); 118 | }); 119 | 120 | // @desc Get bootcamps within a radius 121 | // @route GET /api/v1/bootcamps/radius/:zipcode/:distance 122 | // @access Private 123 | exports.getBootcampsInRadius = asyncHandler(async (req, res, next) => { 124 | const { zipcode, distance } = req.params; 125 | 126 | // Get lat/lng from geocoder 127 | const loc = await geocoder.geocode(zipcode); 128 | const lat = loc[0].latitude; 129 | const lng = loc[0].longitude; 130 | 131 | // Calc radius using radians 132 | // Divide dist by radius of Earth 133 | // Earth Radius = 3,963 mi / 6,378 km 134 | const radius = distance / 3963; 135 | 136 | const bootcamps = await Bootcamp.find({ 137 | location: { $geoWithin: { $centerSphere: [[lng, lat], radius] } } 138 | }); 139 | 140 | res.status(200).json({ 141 | success: true, 142 | count: bootcamps.length, 143 | data: bootcamps 144 | }); 145 | }); 146 | 147 | // @desc Upload photo for bootcamp 148 | // @route PUT /api/v1/bootcamps/:id/photo 149 | // @access Private 150 | exports.bootcampPhotoUpload = asyncHandler(async (req, res, next) => { 151 | const bootcamp = await Bootcamp.findById(req.params.id); 152 | 153 | if (!bootcamp) { 154 | return next( 155 | new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404) 156 | ); 157 | } 158 | 159 | // Make sure user is bootcamp owner 160 | if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { 161 | return next( 162 | new ErrorResponse( 163 | `User ${req.user.id} is not authorized to update this bootcamp`, 164 | 401 165 | ) 166 | ); 167 | } 168 | 169 | if (!req.files) { 170 | return next(new ErrorResponse(`Please upload a file`, 400)); 171 | } 172 | 173 | const file = req.files.file; 174 | 175 | // Make sure the image is a photo 176 | if (!file.mimetype.startsWith('image')) { 177 | return next(new ErrorResponse(`Please upload an image file`, 400)); 178 | } 179 | 180 | // Check filesize 181 | if (file.size > process.env.MAX_FILE_UPLOAD) { 182 | return next( 183 | new ErrorResponse( 184 | `Please upload an image less than ${process.env.MAX_FILE_UPLOAD}`, 185 | 400 186 | ) 187 | ); 188 | } 189 | 190 | // Create custom filename 191 | file.name = `photo_${bootcamp._id}${path.parse(file.name).ext}`; 192 | 193 | file.mv(`${process.env.FILE_UPLOAD_PATH}/${file.name}`, async err => { 194 | if (err) { 195 | console.error(err); 196 | return next(new ErrorResponse(`Problem with file upload`, 500)); 197 | } 198 | 199 | await Bootcamp.findByIdAndUpdate(req.params.id, { photo: file.name }); 200 | 201 | res.status(200).json({ 202 | success: true, 203 | data: file.name 204 | }); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /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 course 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 | 33 | if (!course) { 34 | return next( 35 | new ErrorResponse(`No course with the id of ${req.params.id}`, 404) 36 | ); 37 | } 38 | 39 | res.status(200).json({ 40 | success: true, 41 | data: course 42 | }); 43 | }); 44 | 45 | // @desc Add course 46 | // @route POST /api/v1/bootcamps/:bootcampId/courses 47 | // @access Private 48 | exports.addCourse = 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 | // Make sure user is bootcamp owner 64 | if (bootcamp.user.toString() !== req.user.id && req.user.role !== 'admin') { 65 | return next( 66 | new ErrorResponse( 67 | `User ${req.user.id} is not authorized to add a course to bootcamp ${bootcamp._id}`, 68 | 401 69 | ) 70 | ); 71 | } 72 | 73 | const course = await Course.create(req.body); 74 | 75 | res.status(200).json({ 76 | success: true, 77 | data: course 78 | }); 79 | }); 80 | 81 | // @desc Update course 82 | // @route PUT /api/v1/courses/:id 83 | // @access Private 84 | exports.updateCourse = asyncHandler(async (req, res, next) => { 85 | let course = await Course.findById(req.params.id); 86 | 87 | if (!course) { 88 | return next( 89 | new ErrorResponse(`No course with the id of ${req.params.id}`, 404) 90 | ); 91 | } 92 | 93 | // Make sure user is course owner 94 | if (course.user.toString() !== req.user.id && req.user.role !== 'admin') { 95 | return next( 96 | new ErrorResponse( 97 | `User ${req.user.id} is not authorized to update course ${course._id}`, 98 | 401 99 | ) 100 | ); 101 | } 102 | 103 | course = await Course.findByIdAndUpdate(req.params.id, req.body, { 104 | new: true, 105 | runValidators: true 106 | }); 107 | 108 | course.save(); 109 | 110 | res.status(200).json({ 111 | success: true, 112 | data: course 113 | }); 114 | }); 115 | 116 | // @desc Delete course 117 | // @route DELETE /api/v1/courses/:id 118 | // @access Private 119 | exports.deleteCourse = asyncHandler(async (req, res, next) => { 120 | const course = await Course.findById(req.params.id); 121 | 122 | if (!course) { 123 | return next( 124 | new ErrorResponse(`No course with the id of ${req.params.id}`, 404) 125 | ); 126 | } 127 | 128 | // Make sure user is course owner 129 | if (course.user.toString() !== req.user.id && req.user.role !== 'admin') { 130 | return next( 131 | new ErrorResponse( 132 | `User ${req.user.id} is not authorized to delete course ${course._id}`, 133 | 401 134 | ) 135 | ); 136 | } 137 | 138 | await course.remove(); 139 | 140 | res.status(200).json({ 141 | success: true, 142 | data: {} 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /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 reviews 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 | 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 | 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 | review.save(); 94 | 95 | res.status(200).json({ 96 | success: true, 97 | data: review 98 | }); 99 | }); 100 | 101 | // @desc Delete review 102 | // @route DELETE /api/v1/reviews/:id 103 | // @access Private 104 | exports.deleteReview = asyncHandler(async (req, res, next) => { 105 | const review = await Review.findById(req.params.id); 106 | 107 | if (!review) { 108 | return next( 109 | new ErrorResponse(`No review with the id of ${req.params.id}`, 404) 110 | ); 111 | } 112 | 113 | // Make sure review belongs to user or user is admin 114 | if (review.user.toString() !== req.user.id && req.user.role !== 'admin') { 115 | return next(new ErrorResponse(`Not authorized to update review`, 401)); 116 | } 117 | 118 | await review.remove(); 119 | 120 | res.status(200).json({ 121 | success: true, 122 | data: {} 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /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/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/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 | if(!user){ 19 | return next(new ErrorResponse(`No user with the id of ${req.params.id}`, 404)); 20 | } 21 | 22 | res.status(200).json({ 23 | success: true, 24 | data: user 25 | }); 26 | }); 27 | 28 | // @desc Create user 29 | // @route POST /api/v1/users 30 | // @access Private/Admin 31 | exports.createUser = asyncHandler(async (req, res, next) => { 32 | const user = await User.create(req.body); 33 | 34 | res.status(201).json({ 35 | success: true, 36 | data: user 37 | }); 38 | }); 39 | 40 | // @desc Update user 41 | // @route PUT /api/v1/users/:id 42 | // @access Private/Admin 43 | exports.updateUser = asyncHandler(async (req, res, next) => { 44 | const user = await User.findByIdAndUpdate(req.params.id, req.body, { 45 | new: true, 46 | runValidators: true 47 | }); 48 | 49 | res.status(200).json({ 50 | success: true, 51 | data: user 52 | }); 53 | }); 54 | 55 | // @desc Delete user 56 | // @route DELETE /api/v1/users/:id 57 | // @access Private/Admin 58 | exports.deleteUser = asyncHandler(async (req, res, next) => { 59 | await User.findByIdAndDelete(req.params.id); 60 | 61 | res.status(200).json({ 62 | success: true, 63 | data: {} 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /middleware/advancedResults.js: -------------------------------------------------------------------------------- 1 | const advancedResults = (model, populate) => async (req, res, next) => { 2 | let query; 3 | 4 | // Copy req.query 5 | const reqQuery = { ...req.query }; 6 | 7 | // Fields to exclude 8 | const removeFields = ['select', 'sort', 'page', 'limit']; 9 | 10 | // Loop over removeFields and delete them from reqQuery 11 | removeFields.forEach(param => delete reqQuery[param]); 12 | 13 | // Create query string 14 | let queryStr = JSON.stringify(reqQuery); 15 | 16 | // Create operators ($gt, $gte, etc) 17 | queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`); 18 | 19 | // Finding resource 20 | query = model.find(JSON.parse(queryStr)); 21 | 22 | // Select Fields 23 | if (req.query.select) { 24 | const fields = req.query.select.split(',').join(' '); 25 | query = query.select(fields); 26 | } 27 | 28 | // Sort 29 | if (req.query.sort) { 30 | const sortBy = req.query.sort.split(',').join(' '); 31 | query = query.sort(sortBy); 32 | } else { 33 | query = query.sort('-createdAt'); 34 | } 35 | 36 | // Pagination 37 | const page = parseInt(req.query.page, 10) || 1; 38 | const limit = parseInt(req.query.limit, 10) || 25; 39 | const startIndex = (page - 1) * limit; 40 | const endIndex = page * limit; 41 | const total = await model.countDocuments(JSON.parse(queryStr)); 42 | 43 | query = query.skip(startIndex).limit(limit); 44 | 45 | if (populate) { 46 | query = query.populate(populate); 47 | } 48 | 49 | // Executing query 50 | const results = await query; 51 | 52 | // Pagination result 53 | const pagination = {}; 54 | 55 | if (endIndex < total) { 56 | pagination.next = { 57 | page: page + 1, 58 | limit 59 | }; 60 | } 61 | 62 | if (startIndex > 0) { 63 | pagination.prev = { 64 | page: page - 1, 65 | limit 66 | }; 67 | } 68 | 69 | res.advancedResults = { 70 | success: true, 71 | count: results.length, 72 | pagination, 73 | data: results 74 | }; 75 | 76 | next(); 77 | }; 78 | 79 | module.exports = advancedResults; 80 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | // Set token from cookie 17 | } 18 | // else if (req.cookies.token) { 19 | // token = req.cookies.token; 20 | // } 21 | 22 | // Make sure token exists 23 | if (!token) { 24 | return next(new ErrorResponse('Not authorized to access this route', 401)); 25 | } 26 | 27 | try { 28 | // Verify token 29 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 30 | 31 | req.user = await User.findById(decoded.id); 32 | 33 | next(); 34 | } catch (err) { 35 | return next(new ErrorResponse('Not authorized to access this route', 401)); 36 | } 37 | }); 38 | 39 | // Grant access to specific roles 40 | exports.authorize = (...roles) => { 41 | return (req, res, next) => { 42 | if (!roles.includes(req.user.role)) { 43 | return next( 44 | new ErrorResponse( 45 | `User role ${req.user.role} is not authorized to access this route`, 46 | 403 47 | ) 48 | ); 49 | } 50 | next(); 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /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 | // Log to console for dev 9 | console.log(err); 10 | 11 | // Mongoose bad ObjectId 12 | if (err.name === 'CastError') { 13 | const message = `Resource not found`; 14 | error = new ErrorResponse(message, 404); 15 | } 16 | 17 | // Mongoose duplicate key 18 | if (err.code === 11000) { 19 | const message = 'Duplicate field value entered'; 20 | error = new ErrorResponse(message, 400); 21 | } 22 | 23 | // Mongoose validation error 24 | if (err.name === 'ValidationError') { 25 | const message = Object.values(err.errors).map(val => val.message); 26 | error = new ErrorResponse(message, 400); 27 | } 28 | 29 | res.status(error.statusCode || 500).json({ 30 | success: false, 31 | error: error.message || 'Server Error' 32 | }); 33 | }; 34 | 35 | module.exports = errorHandler; 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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].countryCode 132 | }; 133 | 134 | // Do not save address in DB 135 | this.address = undefined; 136 | next(); 137 | }); 138 | 139 | // Cascade delete courses when a bootcamp is deleted 140 | BootcampSchema.pre('remove', async function(next) { 141 | console.log(`Courses being removed from bootcamp ${this._id}`); 142 | await this.model('Course').deleteMany({ bootcamp: this._id }); 143 | console.log(`Reviews being removed from bootcamp ${this._id}`); 144 | await this.model('Review').deleteMany({ bootcamp: this._id }); 145 | next(); 146 | }); 147 | 148 | // Reverse populate with virtuals 149 | BootcampSchema.virtual('courses', { 150 | ref: 'Course', 151 | localField: '_id', 152 | foreignField: 'bootcamp', 153 | justOne: false 154 | }); 155 | 156 | module.exports = mongoose.model('Bootcamp', BootcampSchema); 157 | -------------------------------------------------------------------------------- /models/Course.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const CourseSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | trim: true, 7 | required: [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 | default: 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 | const averageCost = obj[0] 61 | ? Math.ceil(obj[0].averageCost / 10) * 10 62 | : undefined; 63 | try { 64 | await this.model("Bootcamp").findByIdAndUpdate(bootcampId, { 65 | averageCost, 66 | }); 67 | } catch (err) { 68 | console.log(err); 69 | } 70 | }; 71 | 72 | // Call getAverageCost after save 73 | CourseSchema.post('save', async function() { 74 | await this.constructor.getAverageCost(this.bootcamp); 75 | }); 76 | 77 | // Call getAverageCost after remove 78 | CourseSchema.post('remove', async function () { 79 | await this.constructor.getAverageCost(this.bootcamp); 80 | }); 81 | 82 | // Call getAverageCost after tuition update 83 | CourseSchema.post("findOneAndUpdate", async function (doc) { 84 | if (this.tuition != doc.tuition) { 85 | await doc.constructor.getAverageCost(doc.bootcamp); 86 | } 87 | }); 88 | 89 | module.exports = mongoose.model('Course', CourseSchema); 90 | -------------------------------------------------------------------------------- /models/Review.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ReviewSchema = new mongoose.Schema({ 4 | title: { 5 | type: String, 6 | trim: true, 7 | required: [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 | if (obj[0]) { 55 | await this.model("Bootcamp").findByIdAndUpdate(bootcampId, { 56 | averageRating: obj[0].averageRating.toFixed(1), 57 | }); 58 | } else { 59 | await this.model("Bootcamp").findByIdAndUpdate(bootcampId, { 60 | averageRating: undefined, 61 | }); 62 | } 63 | } catch (err) { 64 | console.error(err); 65 | } 66 | }; 67 | 68 | // Call getAverageCost after save 69 | ReviewSchema.post('save', async function() { 70 | await this.constructor.getAverageRating(this.bootcamp); 71 | }); 72 | 73 | // Call getAverageCost before remove 74 | ReviewSchema.post('remove', async function() { 75 | await this.constructor.getAverageRating(this.bootcamp); 76 | }); 77 | 78 | module.exports = mongoose.model('Review', ReviewSchema); 79 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const mongoose = require('mongoose'); 3 | const bcrypt = require('bcryptjs'); 4 | const jwt = require('jsonwebtoken'); 5 | const randomize = require('randomatic'); 6 | 7 | const UserSchema = new mongoose.Schema({ 8 | name: { 9 | type: String, 10 | required: [true, 'Please add a name'], 11 | }, 12 | email: { 13 | type: String, 14 | required: [true, 'Please add an email'], 15 | unique: true, 16 | match: [ 17 | /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 18 | 'Please add a valid email', 19 | ], 20 | }, 21 | role: { 22 | type: String, 23 | enum: ['user', 'publisher'], 24 | default: 'user', 25 | }, 26 | password: { 27 | type: String, 28 | required: [true, 'Please add a password'], 29 | minlength: 6, 30 | select: false, 31 | }, 32 | resetPasswordToken: String, 33 | resetPasswordExpire: Date, 34 | confirmEmailToken: String, 35 | isEmailConfirmed: { 36 | type: Boolean, 37 | default: false, 38 | }, 39 | twoFactorCode: String, 40 | twoFactorCodeExpire: Date, 41 | twoFactorEnable: { 42 | type: Boolean, 43 | default: false, 44 | }, 45 | createdAt: { 46 | type: Date, 47 | default: Date.now, 48 | }, 49 | }); 50 | 51 | // Encrypt password using bcrypt 52 | UserSchema.pre('save', async function (next) { 53 | if (!this.isModified('password')) { 54 | next(); 55 | } 56 | 57 | const salt = await bcrypt.genSalt(10); 58 | this.password = await bcrypt.hash(this.password, salt); 59 | }); 60 | 61 | // Encrypt password using bcrypt while updating (admin) 62 | UserSchema.pre("findOneAndUpdate", async function (next) { 63 | if (this._update.password) { 64 | this._update.password = await bcrypt.hash(this._update.password, 10); 65 | } 66 | next(); 67 | }); 68 | 69 | // Sign JWT and return 70 | UserSchema.methods.getSignedJwtToken = function () { 71 | return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { 72 | expiresIn: process.env.JWT_EXPIRE, 73 | }); 74 | }; 75 | 76 | // Match user entered password to hashed password in database 77 | UserSchema.methods.matchPassword = async function (enteredPassword) { 78 | return await bcrypt.compare(enteredPassword, this.password); 79 | }; 80 | 81 | // Generate and hash password token 82 | UserSchema.methods.getResetPasswordToken = function () { 83 | // Generate token 84 | const resetToken = crypto.randomBytes(20).toString('hex'); 85 | 86 | // Hash token and set to resetPasswordToken field 87 | this.resetPasswordToken = crypto 88 | .createHash('sha256') 89 | .update(resetToken) 90 | .digest('hex'); 91 | 92 | // Set expire 93 | this.resetPasswordExpire = Date.now() + 10 * 60 * 1000; 94 | 95 | return resetToken; 96 | }; 97 | 98 | // Generate email confirm token 99 | UserSchema.methods.generateEmailConfirmToken = function (next) { 100 | // email confirmation token 101 | const confirmationToken = crypto.randomBytes(20).toString('hex'); 102 | 103 | this.confirmEmailToken = crypto 104 | .createHash('sha256') 105 | .update(confirmationToken) 106 | .digest('hex'); 107 | 108 | const confirmTokenExtend = crypto.randomBytes(100).toString('hex'); 109 | const confirmTokenCombined = `${confirmationToken}.${confirmTokenExtend}`; 110 | return confirmTokenCombined; 111 | }; 112 | 113 | module.exports = mongoose.model('User', UserSchema); 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devcamper_api", 3 | "version": "1.0.0", 4 | "description": "Devcamper backend API", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node server", 8 | "dev": "nodemon server" 9 | }, 10 | "author": "Brad Traversy", 11 | "license": "MIT", 12 | "dependencies": { 13 | "bcryptjs": "^2.4.3", 14 | "colors": "^1.4.0", 15 | "cookie-parser": "^1.4.4", 16 | "cors": "^2.8.5", 17 | "dotenv": "^8.1.0", 18 | "express": "^4.17.1", 19 | "express-fileupload": "^1.2.0", 20 | "express-mongo-sanitize": "^1.3.2", 21 | "express-rate-limit": "^5.0.0", 22 | "helmet": "^3.21.1", 23 | "hpp": "^0.2.2", 24 | "jsonwebtoken": "^8.5.1", 25 | "mongoose": "^5.7.5", 26 | "morgan": "^1.9.1", 27 | "node-geocoder": "^3.24.0", 28 | "nodemailer": "^6.3.0", 29 | "randomatic": "^3.1.1", 30 | "slugify": "^1.3.5", 31 | "xss-clean": "^0.1.1" 32 | }, 33 | "devDependencies": { 34 | "nodemon": "^1.19.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/devcamper-api/c9e7bd7f7f123552562f047473f801bb66e6806c/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/devcamper-api/c9e7bd7f7f123552562f047473f801bb66e6806c/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradtraversy/devcamper-api/c9e7bd7f7f123552562f047473f801bb66e6806c/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # DevCamper API 2 | 3 | > Backend API for DevCamper application, which is a bootcamp directory website 4 | 5 | ## Usage 6 | 7 | Rename "config/config.env.env" to "config/config.env" and update the values/settings to your own 8 | 9 | ## Install Dependencies 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | ## Run App 16 | 17 | ``` 18 | # Run in dev mode 19 | npm run dev 20 | 21 | # Run in prod mode 22 | npm start 23 | ``` 24 | 25 | ## Database Seeder 26 | 27 | To seed the database with users, bootcamps, courses and reviews with data from the "\_data" folder, run 28 | 29 | ``` 30 | # Destroy all data 31 | node seeder -d 32 | 33 | # Import all data 34 | node seeder -i 35 | ``` 36 | 37 | ## Demo 38 | 39 | Extensive documentation with examples [here](https://documenter.getpostman.com/view/8923145/SVtVVTzd?version=latest) 40 | 41 | - Version: 1.0.0 42 | - License: MIT 43 | - Author: Brad Traversy 44 | -------------------------------------------------------------------------------- /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 | confirmEmail, 12 | } = require('../controllers/auth'); 13 | 14 | const router = express.Router(); 15 | 16 | const { protect } = require('../middleware/auth'); 17 | 18 | router.post('/register', register); 19 | router.post('/login', login); 20 | router.get('/logout', logout); 21 | router.get('/me', protect, getMe); 22 | router.get('/confirmemail', confirmEmail); 23 | router.put('/updatedetails', protect, updateDetails); 24 | router.put('/updatepassword', protect, updatePassword); 25 | router.post('/forgotpassword', forgotPassword); 26 | router.put('/resetpassword/:resettoken', resetPassword); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /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 courseRouter = require('./courses'); 16 | const reviewRouter = 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 resource routers 24 | router.use('/:bootcampId/courses', courseRouter); 25 | router.use('/:bootcampId/reviews', reviewRouter); 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 21 | .route('/') 22 | .get(advancedResults(User), getUsers) 23 | .post(createUser); 24 | 25 | router 26 | .route('/:id') 27 | .get(getUser) 28 | .put(updateUser) 29 | .delete(deleteUser); 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /seeder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mongoose = require('mongoose'); 3 | const colors = require('colors'); 4 | const dotenv = require('dotenv'); 5 | 6 | // Load env vars 7 | dotenv.config({ path: './config/config.env' }); 8 | 9 | // Load models 10 | const Bootcamp = require('./models/Bootcamp'); 11 | const Course = require('./models/Course'); 12 | const User = require('./models/User'); 13 | const Review = require('./models/Review'); 14 | 15 | // Connect to DB 16 | mongoose.connect(process.env.MONGO_URI, { 17 | useNewUrlParser: true, 18 | useCreateIndex: true, 19 | useFindAndModify: false, 20 | useUnifiedTopology: true 21 | }); 22 | 23 | // Read JSON files 24 | const bootcamps = JSON.parse( 25 | fs.readFileSync(`${__dirname}/_data/bootcamps.json`, 'utf-8') 26 | ); 27 | 28 | const courses = JSON.parse( 29 | fs.readFileSync(`${__dirname}/_data/courses.json`, 'utf-8') 30 | ); 31 | 32 | const users = JSON.parse( 33 | fs.readFileSync(`${__dirname}/_data/users.json`, 'utf-8') 34 | ); 35 | 36 | const reviews = JSON.parse( 37 | fs.readFileSync(`${__dirname}/_data/reviews.json`, 'utf-8') 38 | ); 39 | 40 | // Import into DB 41 | const importData = async () => { 42 | try { 43 | await Bootcamp.create(bootcamps); 44 | await Course.create(courses); 45 | await User.create(users); 46 | await Review.create(reviews); 47 | console.log('Data Imported...'.green.inverse); 48 | process.exit(); 49 | } catch (err) { 50 | console.error(err); 51 | } 52 | }; 53 | 54 | // Delete data 55 | const deleteData = async () => { 56 | try { 57 | await Bootcamp.deleteMany(); 58 | await Course.deleteMany(); 59 | await User.deleteMany(); 60 | await Review.deleteMany(); 61 | console.log('Data Destroyed...'.red.inverse); 62 | process.exit(); 63 | } catch (err) { 64 | console.error(err); 65 | } 66 | }; 67 | 68 | if (process.argv[2] === '-i') { 69 | importData(); 70 | } else if (process.argv[2] === '-d') { 71 | deleteData(); 72 | } 73 | -------------------------------------------------------------------------------- /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 helmet = require('helmet'); 10 | const xss = require('xss-clean'); 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 | // Load env vars 18 | dotenv.config({ path: './config/config.env' }); 19 | 20 | // Connect to database 21 | connectDB(); 22 | 23 | // Route files 24 | const bootcamps = require('./routes/bootcamps'); 25 | const courses = require('./routes/courses'); 26 | const auth = require('./routes/auth'); 27 | const users = require('./routes/users'); 28 | const reviews = require('./routes/reviews'); 29 | 30 | const app = express(); 31 | 32 | // Body parser 33 | app.use(express.json()); 34 | 35 | // Cookie parser 36 | app.use(cookieParser()); 37 | 38 | // Dev logging middleware 39 | if (process.env.NODE_ENV === 'development') { 40 | app.use(morgan('dev')); 41 | } 42 | 43 | // File uploading 44 | app.use(fileupload()); 45 | 46 | // Sanitize data 47 | app.use(mongoSanitize()); 48 | 49 | // Set security headers 50 | app.use(helmet()); 51 | 52 | // Prevent XSS attacks 53 | app.use(xss()); 54 | 55 | // Rate limiting 56 | const limiter = rateLimit({ 57 | windowMs: 10 * 60 * 1000, // 10 mins 58 | max: 100 59 | }); 60 | app.use(limiter); 61 | 62 | // Prevent http param pollution 63 | app.use(hpp()); 64 | 65 | // Enable CORS 66 | app.use(cors()); 67 | 68 | // Set static folder 69 | app.use(express.static(path.join(__dirname, 'public'))); 70 | 71 | // Mount routers 72 | app.use('/api/v1/bootcamps', bootcamps); 73 | app.use('/api/v1/courses', courses); 74 | app.use('/api/v1/auth', auth); 75 | app.use('/api/v1/users', users); 76 | app.use('/api/v1/reviews', reviews); 77 | 78 | app.use(errorHandler); 79 | 80 | const PORT = process.env.PORT || 5000; 81 | 82 | const server = app.listen( 83 | PORT, 84 | console.log( 85 | `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow.bold 86 | ) 87 | ); 88 | 89 | // Handle unhandled promise rejections 90 | process.on('unhandledRejection', (err, promise) => { 91 | console.log(`Error: ${err.message}`.red); 92 | // Close server & exit process 93 | // server.close(() => process.exit(1)); 94 | }); 95 | -------------------------------------------------------------------------------- /utils/errorResponse.js: -------------------------------------------------------------------------------- 1 | class ErrorResponse extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | 6 | Error.captureStackTrace(this, this.constructor); 7 | } 8 | } 9 | 10 | module.exports = ErrorResponse; 11 | -------------------------------------------------------------------------------- /utils/geocoder.js: -------------------------------------------------------------------------------- 1 | const NodeGeocoder = require('node-geocoder'); 2 | 3 | const options = { 4 | provider: process.env.GEOCODER_PROVIDER, 5 | httpAdapter: 'https', 6 | apiKey: process.env.GEOCODER_API_KEY, 7 | formatter: null 8 | }; 9 | 10 | const geocoder = NodeGeocoder(options); 11 | 12 | module.exports = geocoder; 13 | -------------------------------------------------------------------------------- /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 | const message = { 14 | from: `${process.env.FROM_NAME} <${process.env.FROM_EMAIL}>`, 15 | to: options.email, 16 | subject: options.subject, 17 | text: options.message, 18 | }; 19 | 20 | const info = await transporter.sendMail(message); 21 | 22 | console.log('Message sent: %s', info.messageId); 23 | }; 24 | 25 | module.exports = sendEmail; 26 | --------------------------------------------------------------------------------