├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── app.js ├── config.env.example ├── controllers ├── authController.js ├── bookingController.js ├── errorController.js ├── handlerFactory.js ├── reviewController.js ├── tourController.js ├── userController.js └── viewsController.js ├── dev-data ├── data │ ├── import-dev-data.js │ ├── reviews.json │ ├── tour5.js │ ├── tours-simple.json │ ├── tours.json │ └── users.json ├── img │ ├── aarav.jpg │ ├── leo.jpg │ ├── monica.jpg │ ├── new-tour-1.jpg │ ├── new-tour-2.jpg │ ├── new-tour-3.jpg │ └── new-tour-4.jpg └── templates │ ├── accountTemplate.pug │ ├── emailTemplate.pug │ ├── errorTemplate.pug │ ├── loginTemplate.pug │ ├── tourCardTemplate.pug │ └── tourTemplate.pug ├── models ├── bookingModel.js ├── errorModel.js ├── reviewModel.js ├── tourModel.js └── userModel.js ├── package.json ├── public ├── css │ └── style.css ├── img │ ├── favicon.png │ ├── icons.svg │ ├── logo-green-round.png │ ├── logo-green-small.png │ ├── logo-green.png │ ├── logo-white.png │ ├── pin.png │ ├── tours │ │ ├── tour-1-1.jpg │ │ ├── tour-1-2.jpg │ │ ├── tour-1-3.jpg │ │ ├── tour-1-cover.jpg │ │ ├── tour-2-1.jpg │ │ ├── tour-2-2.jpg │ │ ├── tour-2-3.jpg │ │ ├── tour-2-cover.jpg │ │ ├── tour-3-1.jpg │ │ ├── tour-3-2.jpg │ │ ├── tour-3-3.jpg │ │ ├── tour-3-cover.jpg │ │ ├── tour-4-1.jpg │ │ ├── tour-4-2.jpg │ │ ├── tour-4-3.jpg │ │ ├── tour-4-cover.jpg │ │ ├── tour-5-1.jpg │ │ ├── tour-5-2.jpg │ │ ├── tour-5-3.jpg │ │ ├── tour-5-cover.jpg │ │ ├── tour-5ddd6c9cbd0e4847d01c699c-1574797608434-cover.jpeg │ │ ├── tour-5ddd6c9cbd0e4847d01c699c-1574798101834-cover.jpeg │ │ ├── tour-5ddd6c9cbd0e4847d01c699c-1574798102073-1.jpeg │ │ ├── tour-5ddd6c9cbd0e4847d01c699c-1574798102073-2.jpeg │ │ ├── tour-5ddd6c9cbd0e4847d01c699c-1574798102073-3.jpeg │ │ ├── tour-6-1.jpg │ │ ├── tour-6-2.jpg │ │ ├── tour-6-3.jpg │ │ ├── tour-6-cover.jpg │ │ ├── tour-7-1.jpg │ │ ├── tour-7-2.jpg │ │ ├── tour-7-3.jpg │ │ ├── tour-7-cover.jpg │ │ ├── tour-8-1.jpg │ │ ├── tour-8-2.jpg │ │ ├── tour-8-3.jpg │ │ ├── tour-8-cover.jpg │ │ ├── tour-9-1.jpg │ │ ├── tour-9-2.jpg │ │ ├── tour-9-3.jpg │ │ └── tour-9-cover.jpg │ └── users │ │ ├── default.jpg │ │ ├── user-1.jpg │ │ ├── user-10.jpg │ │ ├── user-11.jpg │ │ ├── user-12.jpg │ │ ├── user-13.jpg │ │ ├── user-14.jpg │ │ ├── user-15.jpg │ │ ├── user-16.jpg │ │ ├── user-17.jpg │ │ ├── user-18.jpg │ │ ├── user-19.jpg │ │ ├── user-2.jpg │ │ ├── user-20.jpg │ │ ├── user-3.jpg │ │ ├── user-4.jpg │ │ ├── user-5.jpg │ │ ├── user-5c8a1f292f8fb814b56fa184-1574777070682.jpeg │ │ ├── user-5c8a21f22f8fb814b56fa18a-1574778661963.jpeg │ │ ├── user-5c8a21f22f8fb814b56fa18a-1574788383024.jpeg │ │ ├── user-5ddd332c8b4c7c66947c7e47-1574777663812.jpeg │ │ ├── user-6.jpg │ │ ├── user-7.jpg │ │ ├── user-8.jpg │ │ └── user-9.jpg ├── js │ ├── alerts.js │ ├── index.js │ ├── login.js │ ├── mapbox.js │ ├── signup.js │ ├── stripe.js │ └── updateSettings.js ├── overview.html └── tour.html ├── routes ├── bookingRoutes.js ├── reviewRoutes.js ├── tourRoutes.js ├── userRoutes.js └── viewRoutes.js ├── server.js ├── utils ├── apiFeatures.js ├── appError.js ├── catchAsync.js └── email.js └── views ├── _footer.pug ├── _header.pug ├── _reviewCard.pug ├── account.pug ├── base.pug ├── email ├── _style.pug ├── baseEmail.pug ├── passwordReset.pug └── welcome.pug ├── error.pug ├── login.pug ├── nullbooking.pug ├── overview.pug ├── signup.pug └── tour.pug /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "plugin:node/recommended"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "spaced-comment": "off", 7 | "no-console": "warn", 8 | "consistent-return": "off", 9 | "func-names": "off", 10 | "object-shorthand": "off", 11 | "no-process-exit": "off", 12 | "no-param-reassign": "off", 13 | "no-return-await": "off", 14 | "no-underscore-dangle": "off", 15 | "class-methods-use-this": "off", 16 | "dot-notation": "warn", 17 | "prefer-destructuring": ["error", { "object": true, "array": false }], 18 | "no-unused-vars": ["error", { "argsIgnorePattern": "req|res|next|val" }], 19 | "arrow-body-style": ["error", "always"], 20 | "avoidEscape": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.env 3 | .cache/ 4 | config/ 5 | dist 6 | bundle.js* 7 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "printWidth": 100, 4 | "semi": true 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Natours 4 |
5 | Natours 6 |
7 |

8 | 9 |

An awesome tour booking site built on top of NodeJS.

10 | 11 |

12 | Demo • 13 | Key Features • 14 | Demonstration • 15 | How To Use • 16 | API Usage • 17 | Deployment • 18 | Build With • 19 | To-do • 20 | Installation • 21 | Known Bugs • 22 | Future Updates • 23 | Acknowledgement 24 |

25 | 26 | ## Key Features 📝 27 | 28 | - Authentication and Authorization 29 | - Sign up, Log in, Logout, Update, and reset password. 30 | - User profile 31 | - Update username, photo, email, password, and other information 32 | - A user can be either a regular user or an admin or a lead guide or a guide. 33 | - When a user signs up, that user by default regular user. 34 | - Tour 35 | - Manage booking, check tour map, check users' reviews and rating 36 | - Tours can be created by an admin user or a lead-guide. 37 | - Tours can be seen by every user. 38 | - Tours can be updated by an admin user or a lead guide. 39 | - Tours can be deleted by an admin user or a lead-guide. 40 | - Bookings 41 | - Only regular users can book tours (make a payment). 42 | - Regular users can not book the same tour twice. 43 | - Regular users can see all the tours they have booked. 44 | - An admin user or a lead guide can see every booking on the app. 45 | - An admin user or a lead guide can delete any booking. 46 | - An admin user or a lead guide can create a booking (manually, without payment). 47 | - An admin user or a lead guide can not create a booking for the same user twice. 48 | - An admin user or a lead guide can edit any booking. 49 | - Reviews 50 | - Only regular users can write reviews for tours that they have booked. 51 | - All users can see the reviews of each tour. 52 | - Regular users can edit and delete their own reviews. 53 | - Regular users can not review the same tour twice. 54 | - An admin can delete any review. 55 | - Favorite Tours 56 | - A regular user can add any of their booked tours to their list of favorite tours. 57 | - A regular user can remove a tour from their list of favorite tours. 58 | - A regular user can not add a tour to their list of favorite tours when it is already a favorite. 59 | - Credit card Payment 60 | 61 | ## Demonstration 🖥️ 62 | 63 | #### Home Page : 64 | 65 | ![natoursHomePageonline-video-cutt](https://user-images.githubusercontent.com/58518192/72606801-7ebe0680-3949-11ea-8e88-613f022a64e5.gif) 66 | 67 | #### Tour Details : 68 | 69 | ![tourOverviewonline-video-cutterc](https://user-images.githubusercontent.com/58518192/72606859-a0b78900-3949-11ea-8f0d-ef44c789957b.gif) 70 | 71 | #### Payment Process : 72 | 73 | ![paymentprocess-1-ycnhrceamp4-7fW](https://user-images.githubusercontent.com/58518192/72606973-d9eff900-3949-11ea-9a2e-f84a6581bef3.gif) 74 | 75 | #### Booked Tours : 76 | 77 | ![rsz_bookedtours](https://user-images.githubusercontent.com/58518192/72607747-6a7b0900-394b-11ea-8b9f-5330531ca2eb.png) 78 | 79 | #### User Profile : 80 | 81 | ![rsz_userprofile](https://user-images.githubusercontent.com/58518192/72607635-44edff80-394b-11ea-8943-64c48f6f19aa.png) 82 | 83 | #### Admin Profile : 84 | 85 | ![rsz_adminprofile](https://user-images.githubusercontent.com/58518192/72607648-4d463a80-394b-11ea-972f-a73160cfaa5b.png) 86 | 87 | ## How To Use 🤔 88 | 89 | ### Book a tour 90 | 91 | - Login to the site 92 | - Search for tours that you want to book 93 | - Book a tour 94 | - Proceed to the payment checkout page 95 | - Enter the card details (Test Mood): 96 | ``` 97 | - Card No. : 4242 4242 4242 4242 98 | - Expiry date: 02 / 22 99 | - CVV: 222 100 | ``` 101 | - Finished! 102 | 103 | ### Manage your booking 104 | 105 | - Check the tour you have booked on the "Manage Booking" page in your user settings. You'll be automatically redirected to this 106 | page after you have completed the booking. 107 | 108 | ### Update your profile 109 | 110 | - You can update your own username, profile photo, email, and password. 111 | 112 | ## API Usage 113 | 114 | Before using the API, you need to set the variables in Postman depending on your environment (development or production). Simply add: 115 | 116 | ``` 117 | - {{URL}} with your hostname as value (Eg. http://127.0.0.1:3000 or http://www.example.com) 118 | - {{password}} with your user password as value. 119 | ``` 120 | 121 | Check [Natours API Documentation](https://documenter.getpostman.com/view/8893042/SW7c37V6) for more info. 122 | 123 | API Features: 124 | 125 | Tours List 👉🏻 https://lgope-natours.onrender.com/api/v1/tours 126 | 127 | Tours State 👉🏻 https://lgope-natours.onrender.com/api/v1/tours/tour-stats 128 | 129 | Get Top 5 Cheap Tours 👉🏻 https://lgope-natours.onrender.com/api/v1/tours/top-5-cheap 130 | 131 | Get Tours Within Radius 👉🏻 https://lgope-natours.onrender.com/api/v1/tours/tours-within/200/center/34.098453,-118.096327/unit/mi 132 | 133 | ## Deployment 🌍 134 | 135 | The website is deployed with git into Heroku. Below are the steps taken: 136 | 137 | ``` 138 | git init 139 | git add -A 140 | git commit -m "Commit message" 141 | heroku login 142 | heroku create 143 | heroku config:set CONFIG_KEY=CONFIG_VALUE 144 | parcel build ./public/js/index.js --out-dir ./public/js --out-file bundle.js 145 | git push heroku master 146 | heroku open 147 | ``` 148 | 149 | You can also change your website URL by running this command: 150 | 151 | ``` 152 | heroku apps: rename natours-users 153 | ``` 154 | 155 | ## Build With 🏗️ 156 | 157 | - [NodeJS](https://nodejs.org/en/) - JS runtime environment 158 | - [Express](http://expressjs.com/) - The web framework used 159 | - [Mongoose](https://mongoosejs.com/) - Object Data Modelling (ODM) library 160 | - [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) - Cloud database service 161 | - [Pug](https://pugjs.org/api/getting-started.html) - High performance template engine 162 | - [JSON Web Token](https://jwt.io/) - Security token 163 | - [ParcelJS](https://parceljs.org/) - Blazing fast, zero configuration web application bundler 164 | - [Stripe](https://stripe.com/) - Online payment API and Making payments on the app. 165 | - [Postman](https://www.getpostman.com/) - API testing 166 | - [Mailtrap](https://mailtrap.io/) & [Sendgrid](https://sendgrid.com/) - Email delivery platform 167 | - [Heroku](https://www.heroku.com/) - Cloud platform 168 | - [Mapbox](https://www.mapbox.com/) - Displaying the different locations of each tour. 169 | 170 | ## To-do 🗒️ 171 | 172 | - Review and rating 173 | - Allow users to add a review directly at the website after they have taken a tour 174 | - Booking 175 | - Prevent duplicate bookings after a user has booked that exact tour, implement favorite tours 176 | - Advanced authentication features 177 | - Signup, confirm user email, log in with refresh token, two-factor authentication 178 | - And More! There's always room for improvement! 179 | 180 | ## Setting Up Your Local Environment ⚙️ 181 | 182 | If you wish to play around with the code base in your local environment, do the following 183 | 184 | ``` 185 | * Clone this repo to your local machine. 186 | * Using the terminal, navigate to the cloned repo. 187 | * Install all the necessary dependencies, as stipulated in the package.json file. 188 | * If you don't already have one, set up accounts with: MONGODB, MAPBOX, STRIPE, SENDGRID, and MAILTRAP. Please ensure to have at least basic knowledge of how these services work. 189 | * In your .env file, set environment variables for the following: 190 | * DATABASE=your Mongodb database URL 191 | * DATABASE_PASSWORD=your MongoDB password 192 | 193 | * SECRET=your JSON web token secret 194 | * JWT_EXPIRES_IN=90d 195 | * JWT_COOKIE_EXPIRES_IN=90 196 | 197 | * EMAIL_USERNAME=your mailtrap username 198 | * EMAIL_PASSWORD=your mailtrap password 199 | * EMAIL_HOST=smtp.mailtrap.io 200 | * EMAIL_PORT=2525 201 | * EMAIL_FROM=your real-life email address 202 | 203 | * SENDGRID_USERNAME=apikey 204 | * SENDGRID_PASSWORD=your sendgrid password 205 | 206 | * STRIPE_SECRET_KEY=your stripe secret key 207 | * STRIPE_WEBHOOK_SECRET=your stripe webhook secret 208 | 209 | * Start the server. 210 | * Your app should be running just fine. 211 | ``` 212 | 213 | #### Demo-`.env` file : 214 | 215 | ![demo-env-file](https://github.com/lgope/Natours/assets/58518192/cf5b833f-2a48-48a1-aeb3-5ffea8967e33) 216 | 217 | ## Installation 🛠️ 218 | 219 | You can fork the app or you can git-clone the app into your local machine. Once done, please install all the 220 | dependencies by running 221 | 222 | ``` 223 | $ npm i 224 | Set your env variables 225 | $ npm run watch:js 226 | $ npm run build:js 227 | $ npm run dev (for development) 228 | $ npm run start:prod (for production) 229 | $ npm run debug (for debug) 230 | $ npm start 231 | Setting up ESLint and Prettier in VS Code 👇🏻 232 | $ npm i eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-config-airbnb eslint-plugin-node 233 | eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react --save-dev 234 | ``` 235 | 236 | ## Contributing 💡 237 | 238 | Pull requests are welcome but please open an issue and discuss what you will do before 😊 239 | 240 | ## Known Bugs 🚨 241 | 242 | Feel free to email me at lakshman.gope2@gmail.com if you run into any issues or have questions, ideas or concerns. 243 | Please enjoy and feel free to share your opinion, constructive criticism, or comments about my work. Thank you! 🙂 244 | 245 | ## Future Updates 🪴 246 | 247 | - Enable PWA 248 | - Improve overall UX/UI and fix bugs 249 | - Featured Tours 250 | - Recently Viewed Tours 251 | - And More! There's always room for improvement! 252 | 253 | ## License 📄 254 | 255 | This project is open-sourced under the [MIT license](https://opensource.org/licenses/MIT). 256 | 257 | ## Deployed Version 🚀 258 | 259 | Live demo (Feel free to visit) 👉🏻 : https://lgope-natours.onrender.com/ 260 | 261 | ## Acknowledgement 🙏🏻 262 | 263 | - This project is part of the online course I've taken at Udemy. Thanks to Jonas Schmedtmann for creating this awesome course! Link to the course: [Node.js, Express, MongoDB & More: The Complete Bootcamp 2019](https://www.udemy.com/course/nodejs-express-mongodb-bootcamp/) 264 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import express from "express"; 3 | import morgan from "morgan"; // morgan is logging middleware. That's gonna allow us to see request data in the console 4 | import rateLimit from "express-rate-limit"; 5 | import helmet from "helmet"; 6 | import mongoSanitize from "express-mongo-sanitize"; 7 | import xss from "xss-clean"; 8 | import hpp from "hpp"; 9 | import cookieParser from "cookie-parser"; 10 | import bodyParser from "body-parser"; 11 | import compression from "compression"; 12 | import cors from "cors"; 13 | 14 | import AppError from "./utils/appError.js"; 15 | import globalErrorHandler from "./controllers/errorController.js"; 16 | import tourRouter from "./routes/tourRoutes.js"; 17 | import userRouter from "./routes/userRoutes.js"; 18 | import reviewRouter from "./routes/reviewRoutes.js"; 19 | import bookingRouter from "./routes/bookingRoutes.js"; 20 | import * as bookingController from "./controllers/bookingController.js"; 21 | import viewRouter from "./routes/viewRoutes.js"; 22 | import { fileURLToPath } from "url"; 23 | 24 | const __filename = fileURLToPath(import.meta.url); 25 | 26 | const __dirname = path.dirname(__filename); 27 | 28 | // Start express app 29 | const app = express(); 30 | 31 | app.enable("trust proxy"); 32 | 33 | app.set("view engine", "pug"); 34 | app.set("views", path.join(__dirname, "views")); 35 | 36 | // 1) GLOBAL MIDDLEWARES 37 | // Implement CORS 38 | app.use(cors()); 39 | 40 | app.options("*", cors()); 41 | // app.options('/api/v1/tours/:id', cors()); 42 | 43 | // Serving static files 44 | app.use(express.static(path.join(__dirname, "public"))); 45 | 46 | // Set security HTTP headers 47 | app.use(helmet()); 48 | 49 | // Development logging 50 | if (process.env.NODE_ENV === "development") { 51 | app.use(morgan("dev")); 52 | } 53 | 54 | // Limit requests from same API 55 | const limiter = rateLimit({ 56 | max: 100, 57 | windowMs: 60 * 60 * 1000, 58 | message: "Too many requests from this IP, please try again in an hour!" 59 | }); 60 | app.use("/api", limiter); 61 | 62 | // Stripe webhook, BEFORE body-parser, because stripe needs the body as stream 63 | app.post( 64 | "/webhook-checkout", 65 | bodyParser.raw({ type: "application/json" }), 66 | bookingController.webhookCheckout 67 | ); 68 | 69 | // Body parser, reading data from body into req.body 70 | app.use(express.json({ limit: "10kb" })); 71 | app.use(express.urlencoded({ extended: true, limit: "10kb" })); 72 | app.use(cookieParser()); 73 | 74 | // Data sanitization against NoSQL query injection 75 | app.use(mongoSanitize()); 76 | 77 | // Data sanitization against XSS 78 | app.use(xss()); 79 | 80 | // Prevent parameter pollution 81 | app.use( 82 | hpp({ 83 | whitelist: [ 84 | "duration", 85 | "ratingsQuantity", 86 | "ratingsAverage", 87 | "maxGroupSize", 88 | "difficulty", 89 | "price" 90 | ] 91 | }) 92 | ); 93 | 94 | app.use(compression()); 95 | 96 | // Test middleware 97 | app.use((req, res, next) => { 98 | req.requestTime = new Date().toISOString(); 99 | // console.log(req.cookies); 100 | next(); 101 | }); 102 | 103 | // 3) ROUTES 104 | app.use("/", viewRouter); 105 | app.use("/api/v1/tours", tourRouter); 106 | app.use("/api/v1/users", userRouter); 107 | app.use("/api/v1/reviews", reviewRouter); 108 | app.use("/api/v1/bookings", bookingRouter); 109 | 110 | app.all("*", (req, res, next) => { 111 | next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404)); 112 | }); 113 | 114 | app.use(globalErrorHandler); 115 | 116 | export default app; 117 | -------------------------------------------------------------------------------- /config.env.example: -------------------------------------------------------------------------------- 1 | // Must be your keys 2 | 3 | NODE_ENV= 4 | PORT= 5 | DATABASE= 6 | DATABASE_LOCAL= 7 | DATABASE_PASSWORD= 8 | 9 | 10 | JWT_SECRET= 11 | JWT_EXPIRES_IN= 12 | JWT_COOKIE_EXPIRES_IN= 13 | 14 | EMAIL_USERNAME= 15 | EMAIL_PASSWORD= 16 | EMAIL_HOST= 17 | EMAIL_PORT= 18 | 19 | EMAIL_FROM= 20 | 21 | SENDGRID_USERNAME= 22 | SENDGRID_PASSWORD= 23 | 24 | STRIPE_SECRET_KEY= 25 | STRIPE_WEBHOOK_SECRET= 26 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { promisify } from "util"; 3 | import jwt from "jsonwebtoken"; 4 | import User from "./../models/userModel.js"; 5 | import catchAsync from "./../utils/catchAsync.js"; 6 | import AppError from "./../utils/appError.js"; 7 | import Email from "./../utils/email.js"; 8 | 9 | const signToken = id => { 10 | return jwt.sign({ id }, process.env.JWT_SECRET, { 11 | expiresIn: process.env.JWT_EXPIRES_IN 12 | }); 13 | }; 14 | 15 | const createSendToken = (user, statusCode, req, res) => { 16 | const token = signToken(user._id); 17 | 18 | res.cookie("jwt", token, { 19 | expires: new Date(Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000), 20 | httpOnly: true, // cookie cannot be accessed or modified in any way by the browser 21 | secure: req.secure || req.headers["x-forwarded-proto"] === "https" 22 | }); 23 | 24 | // Remove password from output 25 | user.password = undefined; 26 | 27 | res.status(statusCode).json({ 28 | status: "success", 29 | token, 30 | data: { 31 | user 32 | } 33 | }); 34 | }; 35 | 36 | export const signup = catchAsync(async (req, res, next) => { 37 | const newUser = await User.create({ 38 | name: req.body.name, 39 | email: req.body.email, 40 | password: req.body.password, 41 | passwordConfirm: req.body.passwordConfirm 42 | }); 43 | 44 | const url = `${req.protocol}://${req.get("host")}/me`; 45 | await new Email(newUser, url).sendWelcome(); 46 | 47 | createSendToken(newUser, 201, req, res); 48 | }); 49 | 50 | export const login = catchAsync(async (req, res, next) => { 51 | const { email, password } = req.body; 52 | 53 | // 1) Check if email and password exist 54 | if (!email || !password) { 55 | return next(new AppError("Please provide email and password!", 400)); 56 | } 57 | // 2) Check if user exists && password is correct 58 | const user = await User.findOne({ email }).select("+password"); 59 | 60 | if (!user || !(await user.correctPassword(password, user.password))) { 61 | return next(new AppError("Incorrect email or password", 401)); 62 | } 63 | 64 | // 3) If everything ok, send token to client 65 | createSendToken(user, 200, req, res); 66 | }); 67 | 68 | export const logout = (req, res) => { 69 | res.cookie("jwt", "loggedout", { 70 | expires: new Date(Date.now() + 10 * 1000), 71 | httpOnly: true 72 | }); 73 | res.status(200).json({ status: "success" }); 74 | }; 75 | 76 | export const protect = catchAsync(async (req, res, next) => { 77 | // 1) Getting token and check of it's there 78 | let token; 79 | if (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) { 80 | token = req.headers.authorization.split(" ")[1]; 81 | } else if (req.cookies.jwt) { 82 | token = req.cookies.jwt; 83 | } 84 | 85 | if (!token) { 86 | return next(new AppError("You are not logged in! Please log in to get access.", 401)); 87 | } 88 | 89 | // 2) Verification token 90 | const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET); 91 | 92 | // 3) Check if user still exists 93 | const currentUser = await User.findById(decoded.id); 94 | if (!currentUser) { 95 | return next(new AppError("The user belonging to this token does no longer exist.", 401)); 96 | } 97 | 98 | // 4) Check if user changed password after the token was issued 99 | if (currentUser.changedPasswordAfter(decoded.iat)) { 100 | return next(new AppError("User recently changed password! Please log in again.", 401)); 101 | } 102 | 103 | // GRANT ACCESS TO PROTECTED ROUTE 104 | req.user = currentUser; 105 | res.locals.user = currentUser; 106 | next(); 107 | }); 108 | 109 | // Only for rendered pages, no errors! 110 | export const isLoggedIn = async (req, res, next) => { 111 | if (req.cookies.jwt) { 112 | try { 113 | // 1) verify token 114 | const decoded = await promisify(jwt.verify)(req.cookies.jwt, process.env.JWT_SECRET); 115 | 116 | // 2) Check if user still exists 117 | const currentUser = await User.findById(decoded.id); 118 | if (!currentUser) { 119 | return next(); 120 | } 121 | 122 | // 3) Check if user changed password after the token was issued 123 | if (currentUser.changedPasswordAfter(decoded.iat)) { 124 | return next(); 125 | } 126 | 127 | // THERE IS A LOGGED IN USER 128 | res.locals.user = currentUser; 129 | return next(); 130 | } catch (err) { 131 | return next(); 132 | } 133 | } 134 | next(); 135 | }; 136 | 137 | export const restrictTo = (...roles) => { 138 | return (req, res, next) => { 139 | // roles ['admin', 'lead-guide']. role='user' 140 | if (!roles.includes(req.user.role)) { 141 | return next(new AppError("You do not have permission to perform this action", 403)); 142 | } 143 | 144 | next(); 145 | }; 146 | }; 147 | 148 | export const forgotPassword = catchAsync(async (req, res, next) => { 149 | // 1) Get user based on POSTed email 150 | const user = await User.findOne({ email: req.body.email }); 151 | if (!user) { 152 | return next(new AppError("There is no user with email address.", 404)); 153 | } 154 | 155 | // 2) Generate the random reset token 156 | const resetToken = user.createPasswordResetToken(); 157 | await user.save({ validateBeforeSave: false }); 158 | 159 | // 3) Send it to user's email 160 | try { 161 | const resetURL = `${req.protocol}://${req.get( 162 | "host" 163 | )}/api/v1/users/resetPassword/${resetToken}`; 164 | await new Email(user, resetURL).sendPasswordReset(); 165 | 166 | res.status(200).json({ 167 | status: "success", 168 | message: "Token sent to email!" 169 | }); 170 | } catch (err) { 171 | user.passwordResetToken = undefined; 172 | user.passwordResetExpires = undefined; 173 | await user.save({ validateBeforeSave: false }); 174 | 175 | return next(new AppError("There was an error sending the email. Try again later!"), 500); 176 | } 177 | }); 178 | 179 | export const resetPassword = catchAsync(async (req, res, next) => { 180 | // 1) Get user based on the token 181 | const hashedToken = crypto 182 | .createHash("sha256") 183 | .update(req.params.token) 184 | .digest("hex"); 185 | 186 | const user = await User.findOne({ 187 | passwordResetToken: hashedToken, 188 | passwordResetExpires: { $gt: Date.now() } 189 | }); 190 | 191 | // 2) If token has not expired, and there is user, set the new password 192 | if (!user) { 193 | return next(new AppError("Token is invalid or has expired", 400)); 194 | } 195 | user.password = req.body.password; 196 | user.passwordConfirm = req.body.passwordConfirm; 197 | user.passwordResetToken = undefined; 198 | user.passwordResetExpires = undefined; 199 | await user.save(); 200 | 201 | // 3) Update changedPasswordAt property for the user 202 | // 4) Log the user in, send JWT 203 | createSendToken(user, 200, req, res); 204 | }); 205 | 206 | export const updatePassword = catchAsync(async (req, res, next) => { 207 | // 1) Get user from collection 208 | const user = await User.findById(req.user.id).select("+password"); 209 | 210 | // 2) Check if POSTed current password is correct 211 | if (!(await user.correctPassword(req.body.passwordCurrent, user.password))) { 212 | return next(new AppError("Your current password is wrong.", 401)); 213 | } 214 | 215 | // 3) If so, update password 216 | user.password = req.body.password; 217 | user.passwordConfirm = req.body.passwordConfirm; 218 | await user.save(); 219 | // User.findByIdAndUpdate will NOT work as intended! 220 | 221 | // 4) Log user in, send JWT 222 | createSendToken(user, 200, req, res); 223 | }); 224 | -------------------------------------------------------------------------------- /controllers/bookingController.js: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import Tour from "../models/tourModel.js"; 3 | import User from "../models/userModel.js"; 4 | import Booking from "../models/bookingModel.js"; 5 | import catchAsync from "../utils/catchAsync.js"; 6 | import * as factory from "./handlerFactory.js"; 7 | 8 | const stripe = Stripe(process.env.STRIPE_SECRET_KEY); 9 | 10 | export const getCheckoutSession = catchAsync(async (req, res, next) => { 11 | // 1) Get the currently booked tour 12 | const tour = await Tour.findById(req.params.tourId); 13 | // console.log(tour); 14 | 15 | // 2) Create checkout session 16 | const session = await stripe.checkout.sessions.create({ 17 | payment_method_types: ["card"], 18 | 19 | // success_url: `${req.protocol}://${req.get('host')}/my-tours/?tour=${ 20 | // req.params.tourId 21 | // }&user=${req.user.id}&price=${tour.price}`, 22 | 23 | success_url: `${req.protocol}://${req.get("host")}/my-tours?alert=booking`, 24 | 25 | cancel_url: `${req.protocol}://${req.get("host")}/tour/${tour.slug}`, 26 | 27 | customer_email: req.user.email, 28 | 29 | client_reference_id: req.params.tourId, 30 | 31 | line_items: [ 32 | { 33 | name: `${tour.name} Tour`, 34 | description: tour.summary, 35 | images: [`${req.protocol}://${req.get("host")}/img/tours/${tour.imageCover}`], 36 | amount: tour.price * 100, 37 | currency: "usd", 38 | quantity: 1 39 | } 40 | ] 41 | }); 42 | 43 | // 3) Create session as response 44 | res.status(200).json({ 45 | status: "success", 46 | session 47 | }); 48 | }); 49 | 50 | const createBookingCheckout = async session => { 51 | const tour = session.client_reference_id; 52 | const user = (await User.findOne({ email: session.customer_email })).id; 53 | const price = session.display_items[0].amount / 100; 54 | await Booking.create({ tour, user, price }); 55 | }; 56 | 57 | export const webhookCheckout = (req, res, next) => { 58 | const signature = req.headers["stripe-signature"]; 59 | 60 | let event; 61 | try { 62 | event = stripe.webhooks.constructEvent(req.body, signature, process.env.STRIPE_WEBHOOK_SECRET); 63 | } catch (err) { 64 | return res.status(400).send(`Webhook error: ${err.message}`); 65 | } 66 | 67 | if (event.type === "checkout.session.completed") createBookingCheckout(event.data.object); 68 | 69 | res.status(200).json({ received: true }); 70 | }; 71 | 72 | export const createBooking = factory.createOne(Booking); 73 | export const getBooking = factory.getOne(Booking); 74 | export const getAllBookings = factory.getAll(Booking); 75 | export const updateBooking = factory.updateOne(Booking); 76 | export const deleteBooking = factory.deleteOne(Booking); 77 | -------------------------------------------------------------------------------- /controllers/errorController.js: -------------------------------------------------------------------------------- 1 | import AppError from "./../utils/appError.js"; 2 | import ErrorStack from "./../models/errorModel.js"; 3 | 4 | const saveError = async err => { 5 | const newError = await ErrorStack.create({ 6 | status: err.status, 7 | error: err, 8 | message: err.message, 9 | stack: err.stack 10 | }); 11 | 12 | return newError.id; 13 | }; 14 | 15 | const handleCastErrorDB = err => { 16 | const message = `Invalid ${err.path}: ${err.value}.`; 17 | return new AppError(message, 400); 18 | }; 19 | 20 | const handleDuplicateFieldsDB = err => { 21 | const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0]; 22 | 23 | const message = `Duplicate field value: ${value}. Please use another value!`; 24 | return new AppError(message, 400); 25 | }; 26 | 27 | const handleValidationErrorDB = err => { 28 | const errors = Object.values(err.errors).map(el => el.message); 29 | 30 | const message = `Invalid input data. ${errors.join(". ")}`; 31 | return new AppError(message, 400); 32 | }; 33 | 34 | const handleJWTError = () => new AppError("Invalid token. Please log in again!", 401); 35 | 36 | const handleJWTExpiredError = () => 37 | new AppError("Your token has expired! Please log in again.", 401); 38 | 39 | const sendErrorDev = async (err, req, res) => { 40 | // A) API 41 | if (req.originalUrl.startsWith("/api")) { 42 | return res.status(err.statusCode).json({ 43 | status: err.status, 44 | error: err, 45 | message: err.message, 46 | stack: err.stack 47 | }); 48 | } 49 | 50 | // B) RENDERED WEBSITE 51 | console.error("ERROR 💥", err); 52 | const errorId = await saveError(err); 53 | return res.status(err.statusCode).render("error", { 54 | title: "Something went wrong!", 55 | msg: `${err.message} (${errorId})` 56 | }); 57 | }; 58 | 59 | const sendErrorProd = async (err, req, res) => { 60 | // A) API 61 | if (req.originalUrl.startsWith("/api")) { 62 | // A) Operational, trusted error: send message to client 63 | if (err.isOperational) { 64 | const errorId = await saveError(err); 65 | return res.status(err.statusCode).json({ 66 | status: err.status, 67 | message: `${err.message} (${errorId})` 68 | }); 69 | } 70 | 71 | // B) Programming or other unknown error: don't leak error details 72 | // 1) Log error 73 | console.error("ERROR 💥", err); 74 | // 2) Send generic message 75 | const errorId = await saveError(err); 76 | return res.status(500).json({ 77 | status: "error", 78 | message: `Something went wrong! (${errorId})` 79 | }); 80 | } 81 | 82 | // B) RENDERED WEBSITE 83 | // A) Operational, trusted error: send message to client 84 | if (err.isOperational) { 85 | // console.log(err); 86 | const errorId = await saveError(err); 87 | return res.status(err.statusCode).render("error", { 88 | title: "Something went wrong!", 89 | msg: `${err.message} (${errorId})` 90 | }); 91 | } 92 | 93 | // B) Programming or other unknown error: don't leak error details 94 | // 1) Log error 95 | console.error("ERROR 💥", err); 96 | // 2) Send generic message 97 | const errorId = await saveError(err); 98 | return res.status(err.statusCode).render("error", { 99 | title: "Something went wrong!", 100 | msg: `Please try again later. (${errorId})` 101 | }); 102 | }; 103 | 104 | export default function globalErrorHandler(err, req, res, next) { 105 | // console.log(err.stack); 106 | 107 | err.statusCode = err.statusCode || 500; 108 | err.status = err.status || "error"; 109 | 110 | if (process.env.NODE_ENV === "development") { 111 | sendErrorDev(err, req, res); 112 | } else if (process.env.NODE_ENV === "production") { 113 | let error = { ...err }; 114 | error.message = err.message; 115 | 116 | if (error.name === "CastError") error = handleCastErrorDB(error); 117 | if (error.code === 11000) error = handleDuplicateFieldsDB(error); 118 | if (error.name === "ValidationError") error = handleValidationErrorDB(error); 119 | if (error.name === "JsonWebTokenError") error = handleJWTError(); 120 | if (error.name === "TokenExpiredError") error = handleJWTExpiredError(); 121 | 122 | sendErrorProd(error, req, res); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /controllers/handlerFactory.js: -------------------------------------------------------------------------------- 1 | import catchAsync from "./../utils/catchAsync.js"; 2 | import AppError from "./../utils/appError.js"; 3 | import APIFeatures from "./../utils/apiFeatures.js"; 4 | 5 | export const deleteOne = Model => 6 | catchAsync(async (req, res, next) => { 7 | const doc = await Model.findByIdAndDelete(req.params.id); 8 | 9 | if (!doc) { 10 | return next(new AppError("No document found with that ID", 404)); 11 | } 12 | 13 | res.status(204).json({ 14 | // 204 means no content 15 | status: "Success", 16 | data: null 17 | }); 18 | }); 19 | 20 | export const updateOne = Model => 21 | catchAsync(async (req, res, next) => { 22 | const doc = await Model.findByIdAndUpdate(req.params.id, req.body, { 23 | new: true, 24 | runValidators: true 25 | }); 26 | 27 | if (!doc) { 28 | return next(new AppError("No document found with that ID", 404)); 29 | } 30 | 31 | res.status(200).json({ 32 | status: "success", 33 | data: { 34 | data: doc 35 | } 36 | }); 37 | }); 38 | 39 | export const createOne = Model => 40 | catchAsync(async (req, res, next) => { 41 | const doc = await Model.create(req.body); 42 | 43 | res.status(201).json({ 44 | status: "success", 45 | data: { 46 | data: doc 47 | } 48 | }); 49 | }); 50 | 51 | export const getOne = (Model, popOptions) => 52 | catchAsync(async (req, res, next) => { 53 | let query = Model.findById(req.params.id); 54 | if (popOptions) query = query.populate(popOptions); 55 | const doc = await query; 56 | 57 | if (!doc) { 58 | return next(new AppError("No tour found with that ID", 404)); 59 | } 60 | 61 | res.status(200).json({ 62 | status: "success", 63 | data: { 64 | data: doc 65 | } 66 | }); 67 | }); 68 | 69 | export const getAll = Model => 70 | catchAsync(async (req, res, next) => { 71 | // To allow for nested GET reviews on tour (hack) 72 | let filter = {}; 73 | if (req.params.tourId) filter = { tour: req.params.tourId }; 74 | 75 | // EXECUTE QUERY 76 | const features = new APIFeatures(Model.find(filter), req.query) 77 | .filter() 78 | .sort() 79 | .limitFields() 80 | .paginate(); 81 | // const doc = await features.query.explain(); 82 | const doc = await features.query; 83 | 84 | // SEND RESPONSE 85 | res.status(200).json({ 86 | status: "success", 87 | results: doc.length, 88 | data: { 89 | data: doc 90 | } 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /controllers/reviewController.js: -------------------------------------------------------------------------------- 1 | import Review from "./../models/reviewModel.js"; 2 | // import catchAsync from './../utils/catchAsync.js'; 3 | import * as factory from "./handlerFactory.js"; 4 | 5 | export const setTourUserIds = (req, res, next) => { 6 | // Allow nested routes 7 | if (!req.body.tour) req.body.tour = req.params.tourId; 8 | if (!req.body.user) req.body.user = req.user.id; 9 | next(); 10 | }; 11 | 12 | export const getAllReviews = factory.getAll(Review); 13 | export const getlReview = factory.getOne(Review); 14 | export const createReview = factory.createOne(Review); 15 | export const updateReview = factory.updateOne(Review); 16 | export const deleteReview = factory.deleteOne(Review); 17 | -------------------------------------------------------------------------------- /controllers/tourController.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | import sharp from "sharp"; 3 | import Tour from "./../models/tourModel.js"; 4 | import catchAsync from "./../utils/catchAsync.js"; 5 | import * as factory from "./handlerFactory.js"; 6 | import AppError from "./../utils/appError.js"; 7 | 8 | const multerStorage = multer.memoryStorage(); 9 | 10 | const multerFilter = (req, file, cb) => { 11 | if (file.mimetype.startsWith("image")) { 12 | cb(null, true); 13 | } else { 14 | cb(new AppError("Not an image! Please upload only images.", 400), false); 15 | } 16 | }; 17 | 18 | const upload = multer({ 19 | storage: multerStorage, 20 | fileFilter: multerFilter 21 | }); 22 | 23 | export const uploadTourImages = upload.fields([ 24 | { name: "imageCover", maxCount: 1 }, 25 | { name: "images", maxCount: 3 } 26 | ]); 27 | 28 | // upload.single('image') req.file 29 | // upload.array('images', 5) req.files 30 | 31 | export const resizeTourImages = catchAsync(async (req, res, next) => { 32 | if (!req.files.imageCover || !req.files.images) return next(); 33 | 34 | // 1) Cover image 35 | req.body.imageCover = `tour-${req.params.id}-${Date.now()}-cover.jpeg`; 36 | await sharp(req.files.imageCover[0].buffer) 37 | .resize(2000, 1333) 38 | .toFormat("jpeg") 39 | .jpeg({ quality: 90 }) 40 | .toFile(`public/img/tours/${req.body.imageCover}`); 41 | 42 | // 2) Images 43 | req.body.images = []; 44 | 45 | await Promise.all( 46 | req.files.images.map(async (file, i) => { 47 | const filename = `tour-${req.params.id}-${Date.now()}-${i + 1}.jpeg`; 48 | 49 | await sharp(file.buffer) 50 | .resize(2000, 1333) 51 | .toFormat("jpeg") 52 | .jpeg({ quality: 90 }) 53 | .toFile(`public/img/tours/${filename}`); 54 | 55 | req.body.images.push(filename); 56 | }) 57 | ); 58 | 59 | next(); 60 | }); 61 | 62 | // const tours = JSON.parse(fs.readFileSync(`${__dirname}/../dev-data/data/tours-simple.json`)); 63 | 64 | // export const checkID = (req, res, next, val) => { 65 | // console.log(`Tour id is: ${val}`); 66 | // if (req.params.id * 1 > tours.length) { 67 | // return res.status(404).json({ 68 | // status: 'fail', 69 | // message: 'Invalid ID' 70 | // }); 71 | // } 72 | 73 | // next(); 74 | // }; 75 | 76 | // export const checkBody = (req, res, next) => { 77 | // if (!req.body.name || !req.body.price) { 78 | // return res.status(400).json({ 79 | // status: 'Fail', 80 | // message: 'Missing name or price' 81 | // }); 82 | // } 83 | 84 | // next(); 85 | // }; 86 | 87 | export const aliasTopTours = (req, res, next) => { 88 | req.query.limit = "5"; 89 | req.query.sort = "-ratingsAverage,price"; 90 | req.query.fields = "name,price,ratingsAverage,summary,difficulty"; 91 | 92 | next(); 93 | }; 94 | 95 | export const getAllTours = factory.getAll(Tour); 96 | export const getTour = factory.getOne(Tour, { path: "reviews" }); 97 | export const createTour = factory.createOne(Tour); 98 | export const updateTour = factory.updateOne(Tour); 99 | export const deleteTour = factory.deleteOne(Tour); 100 | 101 | export const getTourStats = catchAsync(async (req, res, next) => { 102 | const stats = await Tour.aggregate([ 103 | { 104 | $match: { ratingsAverage: { $gte: 4.5 } } 105 | }, 106 | { 107 | $group: { 108 | _id: { $toUpper: "$difficulty" }, 109 | numTours: { $sum: 1 }, 110 | numRatings: { $sum: "$ratingsQuantity" }, 111 | avgRating: { $avg: "$ratingsAverage" }, 112 | avgPrice: { $avg: "$price" }, 113 | minPrice: { $min: "$price" }, 114 | maxPrice: { $max: "$price" } 115 | } 116 | }, 117 | { 118 | $sort: { avgPrice: 1 } 119 | } 120 | // { 121 | // $match: { _id: { $ne: 'EASY' } } 122 | // } 123 | ]); 124 | 125 | res.status(200).json({ 126 | status: "success", 127 | data: { 128 | stats 129 | } 130 | }); 131 | }); 132 | 133 | export const getMonthlyPlan = catchAsync(async (req, res, next) => { 134 | const year = req.params.year * 1; 135 | 136 | const plan = await Tour.aggregate([ 137 | { 138 | $unwind: "$startDates" 139 | }, 140 | { 141 | $match: { 142 | startDates: { 143 | $gte: new Date(`${year}-01-01`), 144 | $lte: new Date(`${year}-12-31`) 145 | } 146 | } 147 | }, 148 | { 149 | $group: { 150 | _id: { $month: "$startDates" }, 151 | numTourStarts: { $sum: 1 }, 152 | tours: { $push: "$name" } 153 | } 154 | }, 155 | { 156 | $addFields: { month: "$_id" } 157 | }, 158 | { 159 | $project: { _id: 0 } 160 | }, 161 | { 162 | $sort: { numTourStarts: -1 } 163 | }, 164 | { 165 | $limit: 12 166 | } 167 | ]); 168 | 169 | res.status(200).json({ 170 | status: "success", 171 | results: plan.length, 172 | data: { 173 | plan 174 | } 175 | }); 176 | }); 177 | 178 | // '/tours-within/:distance/center/:latlng/unit/:unit' 179 | // /tours-within?distance=233¢er=-40,45&unit=mi 180 | // /tours-within/233/center/33.420755, -95.781260/unit/mi 181 | 182 | export const getToursWithin = catchAsync(async (req, res, next) => { 183 | const { distance, latlng, unit } = req.params; 184 | const [lat, lng] = latlng.split(","); 185 | 186 | // const multiplier = unit === 'mi' ? 0.000621371 : 0.001; 187 | const radius = unit === "mi" ? distance / 3963.2 : distance / 6378.1; 188 | 189 | if (!lat || !lng) { 190 | next(new AppError("Please provide latitutr and longitude in the format lat,lng.", 400)); 191 | } 192 | 193 | // console.log(distance, lat, lng, unit); 194 | 195 | const tours = await Tour.find({ 196 | startLocation: { $geoWithin: { $centerSphere: [[lng, lat], radius] } } 197 | }); 198 | 199 | res.status(200).json({ 200 | status: "success", 201 | results: tours.length, 202 | data: { 203 | data: tours 204 | } 205 | }); 206 | }); 207 | 208 | export const getDistances = catchAsync(async (req, res, next) => { 209 | const { latlng, unit } = req.params; 210 | const [lat, lng] = latlng.split(","); 211 | 212 | const multiplier = unit === "mi" ? 0.000621371 : 0.001; 213 | 214 | if (!lat || !lng) { 215 | next(new AppError("Please provide latitutr and longitude in the format lat,lng.", 400)); 216 | } 217 | 218 | const distances = await Tour.aggregate([ 219 | { 220 | $geoNear: { 221 | near: { 222 | type: "Point", 223 | coordinates: [lng * 1, lat * 1] 224 | }, 225 | distanceField: "distance", 226 | distanceMultiplier: multiplier 227 | } 228 | }, 229 | { 230 | $project: { 231 | distance: 1, 232 | name: 1 233 | } 234 | } 235 | ]); 236 | 237 | res.status(200).json({ 238 | status: "success", 239 | data: { 240 | data: distances 241 | } 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /controllers/userController.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | import sharp from "sharp"; 3 | import User from "./../models/userModel.js"; 4 | import catchAsync from "./../utils/catchAsync.js"; 5 | import AppError from "./../utils/appError.js"; 6 | import * as factory from "./handlerFactory.js"; 7 | 8 | // const multerStorage = multer.diskStorage({ 9 | // destination: (req, file, cb) => { 10 | // cb(null, 'public/img/users'); 11 | // }, 12 | // filename: (req, file, cb) => { 13 | // // user-80980d0s9089d-333232325689.jpeg 14 | // const ext = file.mimetype.split('/')[1]; 15 | // cb(null, `user-${req.user.id}-${Date.now()}.${ext}`); 16 | // } 17 | // }); 18 | 19 | const multerStorage = multer.memoryStorage(); 20 | 21 | const multerFilter = (req, file, cb) => { 22 | if (file.mimetype.startsWith("image")) { 23 | cb(null, true); 24 | } else { 25 | cb(new AppError("Not an image! Please upload only images.", 400), false); 26 | } 27 | }; 28 | 29 | const upload = multer({ 30 | storage: multerStorage, 31 | fileFilter: multerFilter 32 | }); 33 | 34 | export const uploadUserPhoto = upload.single("photo"); 35 | 36 | export const resizeUserPhoto = catchAsync(async (req, res, next) => { 37 | if (!req.file) return next(); 38 | 39 | req.file.filename = `user-${req.user.id}-${Date.now()}.jpeg`; 40 | 41 | await sharp(req.file.buffer) 42 | .resize(500, 500) 43 | .toFormat("jpeg") 44 | .jpeg({ quality: 90 }) 45 | .toFile(`public/img/users/${req.file.filename}`); 46 | 47 | next(); 48 | }); 49 | 50 | const filterObj = (obj, ...allowedFields) => { 51 | const newObj = {}; 52 | Object.keys(obj).forEach(el => { 53 | if (allowedFields.includes(el)) newObj[el] = obj[el]; 54 | }); 55 | 56 | return newObj; 57 | }; 58 | 59 | export const getMe = (req, res, next) => { 60 | req.params.id = req.user.id; 61 | next(); 62 | }; 63 | 64 | export const updateMe = catchAsync(async (req, res, next) => { 65 | // 1) Create error if user POSTs password data 66 | if (req.body.password || req.body.passwordConfirm) { 67 | return next( 68 | new AppError("This route is not for password updates. Please use /updateMyPassword.", 400) 69 | ); 70 | } 71 | 72 | // 2) Filtered out unwanted fields names that are not allowed to be updated 73 | const filteredBody = filterObj(req.body, "name", "email"); 74 | if (req.file) filteredBody.photo = req.file.filename; 75 | 76 | // 3) Update user document 77 | const updatedUser = await User.findByIdAndUpdate(req.user.id, filteredBody, { 78 | new: true, 79 | runValidators: true 80 | }); 81 | 82 | // sending responce to user 83 | res.status(200).json({ 84 | status: "success", 85 | data: { 86 | user: updatedUser 87 | } 88 | }); 89 | }); 90 | 91 | export const deleteMe = catchAsync(async (req, res, next) => { 92 | await User.findByIdAndUpdate(req.user.id, { active: false }); 93 | 94 | res.status(204).json({ 95 | status: "success", 96 | data: null 97 | }); 98 | }); 99 | 100 | export const createUser = (req, res) => { 101 | res.status(500).json({ 102 | status: "error", 103 | message: "This route is not yet defined! 😒 Please use /signup instead" 104 | }); 105 | }; 106 | 107 | export const getAllUsers = factory.getAll(User); 108 | export const getUser = factory.getOne(User); 109 | 110 | // Do NOT update passwords with this! 111 | export const updateUser = factory.updateOne(User); 112 | export const deleteUser = factory.deleteOne(User); 113 | -------------------------------------------------------------------------------- /controllers/viewsController.js: -------------------------------------------------------------------------------- 1 | import Tour from "../models/tourModel.js"; 2 | import User from "../models/userModel.js"; 3 | import Booking from "../models/bookingModel.js"; 4 | import catchAsync from "../utils/catchAsync.js"; 5 | import AppError from "../utils/appError.js"; 6 | 7 | export const alerts = (req, res, next) => { 8 | const { alert } = req.query; 9 | if (alert === "booking") 10 | res.locals.alert = 11 | "Your booking was successful! Please check your email for a confirmation. If your booking doesn't show up here immediatly, please come back later."; 12 | next(); 13 | }; 14 | 15 | export const getOverview = catchAsync(async (req, res, next) => { 16 | // 1) Get tour data from collection 17 | const tours = await Tour.find(); 18 | 19 | // 2) Build template 20 | // 3) Render that template using tour data from 1) 21 | res.status(200).render("overview", { 22 | title: "All Tours", 23 | tours 24 | }); 25 | }); 26 | 27 | export const getTour = catchAsync(async (req, res, next) => { 28 | // 1) Get the data, for the requested tour (including reviews and guides) 29 | const tour = await Tour.findOne({ slug: req.params.slug }).populate({ 30 | path: "reviews", 31 | fields: "review rating user" 32 | }); 33 | 34 | if (!tour) { 35 | return next(new AppError("There is no tour with that name.", 404)); 36 | } 37 | 38 | // 2) Build template 39 | // 3) Render template using data from 1) 40 | res.status(200).render("tour", { 41 | title: `${tour.name} Tour`, 42 | tour 43 | }); 44 | }); 45 | 46 | export const getSingupForm = (req, res) => { 47 | res.status(200).render("signup", { 48 | title: "create your account!" 49 | }); 50 | }; 51 | 52 | export const getLoginForm = (req, res) => { 53 | res.status(200).render("login", { 54 | title: "Log into your account" 55 | }); 56 | }; 57 | 58 | export const getAccount = (req, res) => { 59 | res.status(200).render("account", { 60 | title: "Your account" 61 | }); 62 | }; 63 | 64 | export const getMyTours = catchAsync(async (req, res, next) => { 65 | // 1) Find all bookings 66 | const bookings = await Booking.find({ user: req.user.id }); 67 | 68 | // 2) Find tours with the returned IDs 69 | const tourIDs = bookings.map(el => el.tour); 70 | const tours = await Tour.find({ _id: { $in: tourIDs } }); 71 | 72 | if (bookings.length === 0) { 73 | res.status(200).render("nullbooking", { 74 | title: "Book Tours", 75 | headLine: `You haven't booked any tours yet!`, 76 | msg: `Please book a tour and come back. 🙂` 77 | }); 78 | } else { 79 | res.status(200).render("overview", { 80 | title: "My Tours", 81 | tours 82 | }); 83 | } 84 | }); 85 | 86 | export const updateUserData = catchAsync(async (req, res, next) => { 87 | const updatedUser = await User.findByIdAndUpdate( 88 | req.user.id, 89 | { 90 | name: req.body.name, 91 | email: req.body.email 92 | }, 93 | { 94 | new: true, 95 | runValidators: true 96 | } 97 | ); 98 | 99 | res.status(200).render("account", { 100 | title: "Your account", 101 | user: updatedUser 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /dev-data/data/import-dev-data.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import mongoose from "mongoose"; 3 | import dotenv from "dotenv"; 4 | import Tour from "./../../models/tourModel"; 5 | import Review from "./../../models/reviewModel"; 6 | import User from "./../../models/userModel"; 7 | 8 | dotenv.config({ path: "./config.env" }); 9 | 10 | const DB = process.env.DATABASE.replace("", process.env.DATABASE_PASSWORD); 11 | 12 | mongoose 13 | .connect(DB, { 14 | useNewUrlParser: true, 15 | useCreateIndex: true, 16 | useFindAndModify: false 17 | }) 18 | .then(() => console.log("DB connection successful!")); 19 | 20 | // READ JSON FILE 21 | const tours = JSON.parse(fs.readFileSync(`${__dirname}/tours.json`, "utf-8")); 22 | const users = JSON.parse(fs.readFileSync(`${__dirname}/users.json`, "utf-8")); 23 | const reviews = JSON.parse(fs.readFileSync(`${__dirname}/reviews.json`, "utf-8")); 24 | 25 | // IMPORT DATA INTO DB 26 | const importData = async () => { 27 | try { 28 | await Tour.create(tours); 29 | await User.create(users, { validateBeforeSave: false }); 30 | await Review.create(reviews); 31 | console.log("Data successfully loaded!"); 32 | } catch (err) { 33 | console.log(err); 34 | } 35 | process.exit(); 36 | }; 37 | 38 | // DELETE ALL DATA FROM DB 39 | const deleteData = async () => { 40 | try { 41 | await Tour.deleteMany(); 42 | await User.deleteMany(); 43 | await Review.deleteMany(); 44 | console.log("Data successfully deleted!"); 45 | } catch (err) { 46 | console.log(err); 47 | } 48 | process.exit(); 49 | }; 50 | 51 | if (process.argv[2] === "--import") { 52 | importData(); 53 | } else if (process.argv[2] === "--delete") { 54 | deleteData(); 55 | } 56 | 57 | console.log(process.argv); 58 | 59 | // deleteData(); 60 | // importData(); 61 | -------------------------------------------------------------------------------- /dev-data/data/reviews.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5c8a34ed14eb5c17645c9108", 4 | "review": "Cras mollis nisi parturient mi nec aliquet suspendisse sagittis eros condimentum scelerisque taciti mattis praesent feugiat eu nascetur a tincidunt", 5 | "rating": 5, 6 | "user": "5c8a1dfa2f8fb814b56fa181", 7 | "tour": "5c88fa8cf4afda39709c2955" 8 | }, 9 | { 10 | "_id": "5c8a355b14eb5c17645c9109", 11 | "review": "Tempus curabitur faucibus auctor bibendum duis gravida tincidunt litora himenaeos facilisis vivamus vehicula potenti semper fusce suspendisse sagittis!", 12 | "rating": 4, 13 | "user": "5c8a1dfa2f8fb814b56fa181", 14 | "tour": "5c88fa8cf4afda39709c295a" 15 | }, 16 | { 17 | "_id": "5c8a359914eb5c17645c910a", 18 | "review": "Convallis turpis porttitor sapien ad urna efficitur dui vivamus in praesent nulla hac non potenti!", 19 | "rating": 5, 20 | "user": "5c8a1dfa2f8fb814b56fa181", 21 | "tour": "5c88fa8cf4afda39709c295d" 22 | }, 23 | { 24 | "_id": "5c8a35b614eb5c17645c910b", 25 | "review": "Habitasse scelerisque class quam primis convallis integer eros congue nulla proin nam faucibus parturient.", 26 | "rating": 4, 27 | "user": "5c8a1dfa2f8fb814b56fa181", 28 | "tour": "5c88fa8cf4afda39709c296c" 29 | }, 30 | { 31 | "_id": "5c8a364c14eb5c17645c910c", 32 | "review": "Cras consequat fames faucibus ac aliquam dolor a euismod porttitor rhoncus venenatis himenaeos montes tristique pretium libero nisi!", 33 | "rating": 5, 34 | "user": "5c8a1e1a2f8fb814b56fa182", 35 | "tour": "5c88fa8cf4afda39709c296c" 36 | }, 37 | { 38 | "_id": "5c8a368c14eb5c17645c910d", 39 | "review": "Laoreet justo volutpat per etiam donec at augue penatibus eu facilisis lorem phasellus ipsum tristique urna quam platea.", 40 | "rating": 5, 41 | "user": "5c8a1e1a2f8fb814b56fa182", 42 | "tour": "5c88fa8cf4afda39709c2974" 43 | }, 44 | { 45 | "_id": "5c8a36a014eb5c17645c910e", 46 | "review": "Senectus lectus eleifend ex lobortis cras nam cursus accumsan tellus lacus faucibus himenaeos posuere!", 47 | "rating": 5, 48 | "user": "5c8a1e1a2f8fb814b56fa182", 49 | "tour": "5c88fa8cf4afda39709c2970" 50 | }, 51 | { 52 | "_id": "5c8a36b714eb5c17645c910f", 53 | "review": "Pulvinar taciti etiam aenean lacinia natoque interdum fringilla suspendisse nam sapien urna!", 54 | "rating": 4, 55 | "user": "5c8a1e1a2f8fb814b56fa182", 56 | "tour": "5c88fa8cf4afda39709c2955" 57 | }, 58 | { 59 | "_id": "5c8a379a14eb5c17645c9110", 60 | "review": "Pretium vel inceptos fringilla sit dui fusce varius gravida platea morbi semper erat elit porttitor potenti!", 61 | "rating": 5, 62 | "user": "5c8a24402f8fb814b56fa190", 63 | "tour": "5c88fa8cf4afda39709c2951" 64 | }, 65 | { 66 | "_id": "5c8a37b114eb5c17645c9111", 67 | "review": "Ex a bibendum quis volutpat consequat euismod vulputate parturient laoreet diam sagittis amet at blandit.", 68 | "rating": 4, 69 | "user": "5c8a24402f8fb814b56fa190", 70 | "tour": "5c88fa8cf4afda39709c295a" 71 | }, 72 | { 73 | "_id": "5c8a37cb14eb5c17645c9112", 74 | "review": "Auctor euismod interdum augue tristique senectus nascetur cras justo eleifend mattis libero id adipiscing amet placerat", 75 | "rating": 5, 76 | "user": "5c8a24402f8fb814b56fa190", 77 | "tour": "5c88fa8cf4afda39709c2961" 78 | }, 79 | { 80 | "_id": "5c8a37dd14eb5c17645c9113", 81 | "review": "A facilisi justo ornare magnis velit diam dictumst parturient arcu nullam rhoncus nec!", 82 | "rating": 4, 83 | "user": "5c8a24402f8fb814b56fa190", 84 | "tour": "5c88fa8cf4afda39709c2966" 85 | }, 86 | { 87 | "_id": "5c8a37f114eb5c17645c9114", 88 | "review": "Porttitor ullamcorper rutrum semper proin mus felis varius convallis conubia nisl erat lectus eget.", 89 | "rating": 5, 90 | "user": "5c8a24402f8fb814b56fa190", 91 | "tour": "5c88fa8cf4afda39709c2974" 92 | }, 93 | { 94 | "_id": "5c8a381714eb5c17645c9115", 95 | "review": "Porttitor ullamcorper rutrum semper proin mus felis varius convallis conubia nisl erat lectus eget.", 96 | "rating": 5, 97 | "user": "5c8a1ec62f8fb814b56fa183", 98 | "tour": "5c88fa8cf4afda39709c2951" 99 | }, 100 | { 101 | "_id": "5c8a382d14eb5c17645c9116", 102 | "review": "Semper blandit felis nostra facilisi sodales pulvinar habitasse diam sapien lobortis urna nunc ipsum orci.", 103 | "rating": 5, 104 | "user": "5c8a1ec62f8fb814b56fa183", 105 | "tour": "5c88fa8cf4afda39709c295a" 106 | }, 107 | { 108 | "_id": "5c8a384114eb5c17645c9117", 109 | "review": "Neque amet vel integer placerat ex pretium elementum vitae quis ullamcorper nullam nunc habitant cursus justo!!!", 110 | "rating": 5, 111 | "user": "5c8a1ec62f8fb814b56fa183", 112 | "tour": "5c88fa8cf4afda39709c2961" 113 | }, 114 | { 115 | "_id": "5c8a385614eb5c17645c9118", 116 | "review": "Sollicitudin sagittis ex ut fringilla enim condimentum et netus tristique.", 117 | "rating": 5, 118 | "user": "5c8a1ec62f8fb814b56fa183", 119 | "tour": "5c88fa8cf4afda39709c295d" 120 | }, 121 | { 122 | "_id": "5c8a387214eb5c17645c9119", 123 | "review": "Semper tempus curae at platea lobortis ullamcorper curabitur luctus maecenas nisl laoreet!", 124 | "rating": 5, 125 | "user": "5c8a1ec62f8fb814b56fa183", 126 | "tour": "5c88fa8cf4afda39709c296c" 127 | }, 128 | { 129 | "_id": "5c8a38ac14eb5c17645c911a", 130 | "review": "Arcu adipiscing lobortis sem finibus consequat ac justo nisi pharetra ultricies facilisi!", 131 | "rating": 5, 132 | "user": "5c8a211f2f8fb814b56fa188", 133 | "tour": "5c88fa8cf4afda39709c296c" 134 | }, 135 | { 136 | "_id": "5c8a38c714eb5c17645c911b", 137 | "review": "Rutrum viverra turpis nunc ultricies dolor ornare metus habitant ex quis sociosqu nascetur pellentesque quam!", 138 | "rating": 5, 139 | "user": "5c8a211f2f8fb814b56fa188", 140 | "tour": "5c88fa8cf4afda39709c2970" 141 | }, 142 | { 143 | "_id": "5c8a38da14eb5c17645c911c", 144 | "review": "Elementum massa porttitor enim vitae eu ligula vivamus amet imperdiet urna tristique donec mattis mus erat.", 145 | "rating": 5, 146 | "user": "5c8a211f2f8fb814b56fa188", 147 | "tour": "5c88fa8cf4afda39709c2966" 148 | }, 149 | { 150 | "_id": "5c8a38ed14eb5c17645c911d", 151 | "review": "Fusce ullamcorper gravida libero nullam lacus litora class orci habitant sollicitudin...", 152 | "rating": 5, 153 | "user": "5c8a211f2f8fb814b56fa188", 154 | "tour": "5c88fa8cf4afda39709c295d" 155 | }, 156 | { 157 | "_id": "5c8a390d14eb5c17645c911e", 158 | "review": "Varius potenti proin hendrerit felis sit convallis nunc non id facilisis aliquam platea elementum", 159 | "rating": 5, 160 | "user": "5c8a211f2f8fb814b56fa188", 161 | "tour": "5c88fa8cf4afda39709c2951" 162 | }, 163 | { 164 | "_id": "5c8a391f14eb5c17645c911f", 165 | "review": "Sem feugiat sed lorem vel dignissim platea habitasse dolor suscipit ultricies dapibus", 166 | "rating": 5, 167 | "user": "5c8a211f2f8fb814b56fa188", 168 | "tour": "5c88fa8cf4afda39709c2955" 169 | }, 170 | { 171 | "_id": "5c8a395b14eb5c17645c9120", 172 | "review": "Sem feugiat sed lorem vel dignissim platea habitasse dolor suscipit ultricies dapibus", 173 | "rating": 5, 174 | "user": "5c8a20d32f8fb814b56fa187", 175 | "tour": "5c88fa8cf4afda39709c2951" 176 | }, 177 | { 178 | "_id": "5c8a399014eb5c17645c9121", 179 | "review": "Tortor dolor sed vehicula neque ultrices varius orci feugiat dignissim auctor consequat.", 180 | "rating": 4, 181 | "user": "5c8a20d32f8fb814b56fa187", 182 | "tour": "5c88fa8cf4afda39709c295d" 183 | }, 184 | { 185 | "_id": "5c8a39a214eb5c17645c9122", 186 | "review": "Felis mauris aenean eu lectus fringilla habitasse nullam eros senectus ante etiam!", 187 | "rating": 5, 188 | "user": "5c8a20d32f8fb814b56fa187", 189 | "tour": "5c88fa8cf4afda39709c2970" 190 | }, 191 | { 192 | "_id": "5c8a39b614eb5c17645c9123", 193 | "review": "Blandit varius nascetur est felis praesent lorem himenaeos pretium dapibus tellus bibendum consequat ac duis", 194 | "rating": 3, 195 | "user": "5c8a20d32f8fb814b56fa187", 196 | "tour": "5c88fa8cf4afda39709c2974" 197 | }, 198 | { 199 | "_id": "5c8a3a7014eb5c17645c9124", 200 | "review": "Blandit varius nascetur est felis praesent lorem himenaeos pretium dapibus tellus bibendum consequat ac duis", 201 | "rating": 5, 202 | "user": "5c8a23c82f8fb814b56fa18d", 203 | "tour": "5c88fa8cf4afda39709c2955" 204 | }, 205 | { 206 | "_id": "5c8a3a8d14eb5c17645c9125", 207 | "review": "Iaculis mauris eget sed nec lobortis rhoncus montes etiam dapibus suspendisse hendrerit quam pellentesque potenti sapien!", 208 | "rating": 5, 209 | "user": "5c8a23c82f8fb814b56fa18d", 210 | "tour": "5c88fa8cf4afda39709c2951" 211 | }, 212 | { 213 | "_id": "5c8a3a9914eb5c17645c9126", 214 | "review": "Netus eleifend adipiscing ligula placerat fusce orci sollicitudin vivamus conubia.", 215 | "rating": 5, 216 | "user": "5c8a23c82f8fb814b56fa18d", 217 | "tour": "5c88fa8cf4afda39709c295a" 218 | }, 219 | { 220 | "_id": "5c8a3aaa14eb5c17645c9127", 221 | "review": "Eleifend suspendisse ultricies platea primis ut ornare purus vel taciti faucibus justo nunc", 222 | "rating": 4, 223 | "user": "5c8a23c82f8fb814b56fa18d", 224 | "tour": "5c88fa8cf4afda39709c2961" 225 | }, 226 | { 227 | "_id": "5c8a3abc14eb5c17645c9128", 228 | "review": "Malesuada consequat congue vel gravida eros conubia in sapien praesent diam!", 229 | "rating": 4, 230 | "user": "5c8a23c82f8fb814b56fa18d", 231 | "tour": "5c88fa8cf4afda39709c2966" 232 | }, 233 | { 234 | "_id": "5c8a3acf14eb5c17645c9129", 235 | "review": "Curabitur maximus montes vestibulum nulla vel dictum cubilia himenaeos nunc hendrerit amet urna.", 236 | "rating": 5, 237 | "user": "5c8a23c82f8fb814b56fa18d", 238 | "tour": "5c88fa8cf4afda39709c2970" 239 | }, 240 | { 241 | "_id": "5c8a3b1e14eb5c17645c912a", 242 | "review": "Curabitur maximus montes vestibulum nulla vel dictum cubilia himenaeos nunc hendrerit amet urna.", 243 | "rating": 5, 244 | "user": "5c8a23de2f8fb814b56fa18e", 245 | "tour": "5c88fa8cf4afda39709c296c" 246 | }, 247 | { 248 | "_id": "5c8a3b3214eb5c17645c912b", 249 | "review": "Sociosqu eleifend tincidunt aenean condimentum gravida lorem arcu pellentesque felis dui feugiat nec.", 250 | "rating": 5, 251 | "user": "5c8a23de2f8fb814b56fa18e", 252 | "tour": "5c88fa8cf4afda39709c2974" 253 | }, 254 | { 255 | "_id": "5c8a3b4714eb5c17645c912c", 256 | "review": "Ridiculus facilisis sem id aenean amet penatibus gravida phasellus a mus dui lacinia accumsan!!", 257 | "rating": 1, 258 | "user": "5c8a23de2f8fb814b56fa18e", 259 | "tour": "5c88fa8cf4afda39709c2966" 260 | }, 261 | { 262 | "_id": "5c8a3b6714eb5c17645c912e", 263 | "review": "Venenatis molestie luctus cubilia taciti tempor faucibus nostra nisi curae integer.", 264 | "rating": 5, 265 | "user": "5c8a23de2f8fb814b56fa18e", 266 | "tour": "5c88fa8cf4afda39709c2951" 267 | }, 268 | { 269 | "_id": "5c8a3b7c14eb5c17645c912f", 270 | "review": "Tempor pellentesque eu placerat auctor enim nam suscipit tincidunt natoque ipsum est.", 271 | "rating": 5, 272 | "user": "5c8a23de2f8fb814b56fa18e", 273 | "tour": "5c88fa8cf4afda39709c2955" 274 | }, 275 | { 276 | "_id": "5c8a3b9f14eb5c17645c9130", 277 | "review": "Tempor pellentesque eu placerat auctor enim nam suscipit tincidunt natoque ipsum est.", 278 | "rating": 5, 279 | "user": "5c8a24282f8fb814b56fa18f", 280 | "tour": "5c88fa8cf4afda39709c295a" 281 | }, 282 | { 283 | "_id": "5c8a3bc414eb5c17645c9131", 284 | "review": "Conubia semper efficitur rhoncus suspendisse taciti lectus ex sapien dolor molestie fusce class.", 285 | "rating": 5, 286 | "user": "5c8a24282f8fb814b56fa18f", 287 | "tour": "5c88fa8cf4afda39709c2961" 288 | }, 289 | { 290 | "_id": "5c8a3bdc14eb5c17645c9132", 291 | "review": "Conubia pharetra pulvinar libero hac class congue curabitur mi porttitor!!", 292 | "rating": 5, 293 | "user": "5c8a24282f8fb814b56fa18f", 294 | "tour": "5c88fa8cf4afda39709c2966" 295 | }, 296 | { 297 | "_id": "5c8a3bf514eb5c17645c9133", 298 | "review": "Nullam felis dictumst eros nulla torquent arcu inceptos mi faucibus ridiculus pellentesque gravida mauris.", 299 | "rating": 5, 300 | "user": "5c8a24282f8fb814b56fa18f", 301 | "tour": "5c88fa8cf4afda39709c2974" 302 | }, 303 | { 304 | "_id": "5c8a3c2514eb5c17645c9134", 305 | "review": "Euismod suscipit ipsum efficitur rutrum dis mus dictumst laoreet lectus.", 306 | "rating": 5, 307 | "user": "5c8a245f2f8fb814b56fa191", 308 | "tour": "5c88fa8cf4afda39709c2951" 309 | }, 310 | { 311 | "_id": "5c8a3c3b14eb5c17645c9135", 312 | "review": "Massa orci lacus suspendisse maximus ad integer donec arcu parturient facilisis accumsan consectetur non", 313 | "rating": 4, 314 | "user": "5c8a245f2f8fb814b56fa191", 315 | "tour": "5c88fa8cf4afda39709c295a" 316 | }, 317 | { 318 | "_id": "5c8a3c5314eb5c17645c9136", 319 | "review": "Blandit varius finibus imperdiet tortor hendrerit erat rhoncus dictumst inceptos massa in.", 320 | "rating": 5, 321 | "user": "5c8a245f2f8fb814b56fa191", 322 | "tour": "5c88fa8cf4afda39709c2961" 323 | }, 324 | { 325 | "_id": "5c8a3c6814eb5c17645c9137", 326 | "review": "Tristique semper proin pellentesque ipsum urna habitasse venenatis tincidunt morbi nisi at", 327 | "rating": 4, 328 | "user": "5c8a245f2f8fb814b56fa191", 329 | "tour": "5c88fa8cf4afda39709c295d" 330 | }, 331 | { 332 | "_id": "5c8a3c7814eb5c17645c9138", 333 | "review": "Potenti etiam placerat mi metus ipsum curae eget nisl torquent pretium", 334 | "rating": 4, 335 | "user": "5c8a245f2f8fb814b56fa191", 336 | "tour": "5c88fa8cf4afda39709c2966" 337 | }, 338 | { 339 | "_id": "5c8a3c9014eb5c17645c9139", 340 | "review": "Molestie non montes at fermentum cubilia quis dis placerat maecenas vulputate sapien facilisis", 341 | "rating": 5, 342 | "user": "5c8a245f2f8fb814b56fa191", 343 | "tour": "5c88fa8cf4afda39709c2970" 344 | }, 345 | { 346 | "_id": "5c8a3ca314eb5c17645c913a", 347 | "review": "Velit vulputate faucibus in nascetur praesent potenti primis pulvinar tempor", 348 | "rating": 5, 349 | "user": "5c8a245f2f8fb814b56fa191", 350 | "tour": "5c88fa8cf4afda39709c296c" 351 | }, 352 | { 353 | "_id": "5c8a3cdc14eb5c17645c913b", 354 | "review": "Magna magnis tellus dui vivamus donec placerat vehicula erat turpis", 355 | "rating": 5, 356 | "user": "5c8a24822f8fb814b56fa192", 357 | "tour": "5c88fa8cf4afda39709c2955" 358 | }, 359 | { 360 | "_id": "5c8a3cf414eb5c17645c913c", 361 | "review": "Ligula lorem taciti fringilla himenaeos ex aliquam litora nam ad maecenas sit phasellus lectus!!", 362 | "rating": 5, 363 | "user": "5c8a24822f8fb814b56fa192", 364 | "tour": "5c88fa8cf4afda39709c2951" 365 | }, 366 | { 367 | "_id": "5c8a3d1e14eb5c17645c913d", 368 | "review": "Nam ultrices quis leo viverra tristique curae facilisi luctus sapien eleifend fames orci lacinia pulvinar.", 369 | "rating": 4, 370 | "user": "5c8a24822f8fb814b56fa192", 371 | "tour": "5c88fa8cf4afda39709c2961" 372 | }, 373 | { 374 | "_id": "5c8a3d3a14eb5c17645c913e", 375 | "review": "Ullamcorper ac nec id habitant a commodo eget libero cras congue!", 376 | "rating": 4, 377 | "user": "5c8a24822f8fb814b56fa192", 378 | "tour": "5c88fa8cf4afda39709c2970" 379 | }, 380 | { 381 | "_id": "5c8a3d5314eb5c17645c913f", 382 | "review": "Ultrices nam dui ex posuere velit varius himenaeos bibendum fermentum sollicitudin purus", 383 | "rating": 5, 384 | "user": "5c8a24822f8fb814b56fa192", 385 | "tour": "5c88fa8cf4afda39709c2974" 386 | }, 387 | { 388 | "_id": "5c8a3d8614eb5c17645c9140", 389 | "review": "Ultrices nam dui ex posuere velit varius himenaeos bibendum fermentum sollicitudin purus", 390 | "rating": 5, 391 | "user": "5c8a24a02f8fb814b56fa193", 392 | "tour": "5c88fa8cf4afda39709c2974" 393 | }, 394 | { 395 | "_id": "5c8a3d9b14eb5c17645c9141", 396 | "review": "Vitae vulputate id quam metus orci cras mollis vivamus vehicula sapien et", 397 | "rating": 2, 398 | "user": "5c8a24a02f8fb814b56fa193", 399 | "tour": "5c88fa8cf4afda39709c296c" 400 | }, 401 | { 402 | "_id": "5c8a3de514eb5c17645c9143", 403 | "review": "Sem risus tempor auctor mattis netus montes tincidunt mollis lacinia natoque adipiscing", 404 | "rating": 5, 405 | "user": "5c8a24a02f8fb814b56fa193", 406 | "tour": "5c88fa8cf4afda39709c2961" 407 | }, 408 | { 409 | "_id": "5c8a3e0714eb5c17645c9144", 410 | "review": "Feugiat egestas ac pulvinar quis dui ligula tempor ad platea quisque scelerisque!", 411 | "rating": 5, 412 | "user": "5c8a24a02f8fb814b56fa193", 413 | "tour": "5c88fa8cf4afda39709c2951" 414 | }, 415 | { 416 | "_id": "5c8a3e63e12c44188b4dbc5b", 417 | "review": "Quisque egestas faucibus primis ridiculus mi felis tristique curabitur habitasse vehicula", 418 | "rating": 4, 419 | "user": "5c8a24a02f8fb814b56fa193", 420 | "tour": "5c88fa8cf4afda39709c2966" 421 | } 422 | ] -------------------------------------------------------------------------------- /dev-data/data/tour5.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const tour5 = { 3 | id: 5, 4 | name: "The Sports Lover", 5 | startLocation: "California, USA", 6 | nextStartDate: "July 2021", 7 | duration: 14, 8 | maxGroupSize: 8, 9 | difficulty: "difficult", 10 | avgRating: 4.7, 11 | numReviews: 23, 12 | regPrice: 2997, 13 | shortDescription: "Surfing, skating, parajumping, rock climbing and more, all in one tour", 14 | longDescription: 15 | "Nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nVoluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur!" 16 | }; 17 | -------------------------------------------------------------------------------- /dev-data/data/tours-simple.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "name": "The Forest Hiker", 5 | "duration": 5, 6 | "maxGroupSize": 25, 7 | "difficulty": "easy", 8 | "ratingsAverage": 4.7, 9 | "ratingsQuantity": 37, 10 | "price": 397, 11 | "summary": "Breathtaking hike through the Canadian Banff National Park", 12 | "description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 13 | "imageCover": "tour-1-cover.jpg", 14 | "images": [ 15 | "tour-1-1.jpg", 16 | "tour-1-2.jpg", 17 | "tour-1-3.jpg" 18 | ], 19 | "startDates": [ 20 | "2021-04-25,10:00", 21 | "2021-07-20,10:00", 22 | "2021-10-05,10:00" 23 | ] 24 | }, 25 | { 26 | "id": 1, 27 | "name": "The Sea Explorer", 28 | "duration": 7, 29 | "maxGroupSize": 15, 30 | "difficulty": "medium", 31 | "ratingsAverage": 4.8, 32 | "ratingsQuantity": 23, 33 | "price": 497, 34 | "summary": "Exploring the jaw-dropping US east coast by foot and by boat", 35 | "description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 36 | "imageCover": "tour-2-cover.jpg", 37 | "images": [ 38 | "tour-2-1.jpg", 39 | "tour-2-2.jpg", 40 | "tour-2-3.jpg" 41 | ], 42 | "startDates": [ 43 | "2021-06-19,10:00", 44 | "2021-07-20,10:00", 45 | "2021-08-18,10:00" 46 | ] 47 | }, 48 | { 49 | "id": 2, 50 | "name": "The Snow Adventurer", 51 | "duration": 4, 52 | "maxGroupSize": 10, 53 | "difficulty": "difficult", 54 | "ratingsAverage": 4.5, 55 | "ratingsQuantity": 13, 56 | "price": 997, 57 | "summary": "Exciting adventure in the snow with snowboarding and skiing", 58 | "description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!", 59 | "imageCover": "tour-3-cover.jpg", 60 | "images": [ 61 | "tour-3-1.jpg", 62 | "tour-3-2.jpg", 63 | "tour-3-3.jpg" 64 | ], 65 | "startDates": [ 66 | "2022-01-05,10:00", 67 | "2022-02-12,10:00", 68 | "2023-01-06,10:00" 69 | ] 70 | }, 71 | { 72 | "id": 3, 73 | "name": "The City Wanderer", 74 | "duration": 9, 75 | "maxGroupSize": 20, 76 | "difficulty": "easy", 77 | "ratingsAverage": 4.6, 78 | "ratingsQuantity": 54, 79 | "price": 1197, 80 | "summary": "Living the life of Wanderlust in the US' most beatiful cities", 81 | "description": "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat lorem ipsum dolor sit amet.\nConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat!", 82 | "imageCover": "tour-4-cover.jpg", 83 | "images": [ 84 | "tour-4-1.jpg", 85 | "tour-4-2.jpg", 86 | "tour-4-3.jpg" 87 | ], 88 | "startDates": [ 89 | "2021-03-11,10:00", 90 | "2021-05-02,10:00", 91 | "2021-06-09,10:00" 92 | ] 93 | }, 94 | { 95 | "id": 4, 96 | "name": "The Park Camper", 97 | "duration": 10, 98 | "maxGroupSize": 15, 99 | "difficulty": "medium", 100 | "ratingsAverage": 4.9, 101 | "ratingsQuantity": 19, 102 | "price": 1497, 103 | "summary": "Breathing in Nature in America's most spectacular National Parks", 104 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!", 105 | "imageCover": "tour-5-cover.jpg", 106 | "images": [ 107 | "tour-5-1.jpg", 108 | "tour-5-2.jpg", 109 | "tour-5-3.jpg" 110 | ], 111 | "startDates": [ 112 | "2021-08-05,10:00", 113 | "2022-03-20,10:00", 114 | "2022-08-12,10:00" 115 | ] 116 | }, 117 | { 118 | "id": 5, 119 | "name": "The Sports Lover", 120 | "duration": 14, 121 | "maxGroupSize": 8, 122 | "difficulty": "difficult", 123 | "ratingsAverage": 4.7, 124 | "ratingsQuantity": 28, 125 | "price": 2997, 126 | "summary": "Surfing, skating, parajumping, rock climbing and more, all in one tour", 127 | "description": "Nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nVoluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur!", 128 | "imageCover": "tour-6-cover.jpg", 129 | "images": [ 130 | "tour-6-1.jpg", 131 | "tour-6-2.jpg", 132 | "tour-6-3.jpg" 133 | ], 134 | "startDates": [ 135 | "2021-07-19,10:00", 136 | "2021-09-06,10:00", 137 | "2022-03-18,10:00" 138 | ] 139 | }, 140 | { 141 | "id": 6, 142 | "name": "The Wine Taster", 143 | "duration": 5, 144 | "maxGroupSize": 8, 145 | "difficulty": "easy", 146 | "ratingsAverage": 4.5, 147 | "ratingsQuantity": 35, 148 | "price": 1997, 149 | "summary": "Exquisite wines, scenic views, exclusive barrel tastings, and much more", 150 | "description": "Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nIrure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", 151 | "imageCover": "tour-7-cover.jpg", 152 | "images": [ 153 | "tour-7-1.jpg", 154 | "tour-7-2.jpg", 155 | "tour-7-3.jpg" 156 | ], 157 | "startDates": [ 158 | "2021-02-12,10:00", 159 | "2021-04-14,10:00", 160 | "2021-09-01,10:00" 161 | ] 162 | }, 163 | { 164 | "id": 7, 165 | "name": "The Star Gazer", 166 | "duration": 9, 167 | "maxGroupSize": 8, 168 | "difficulty": "medium", 169 | "ratingsAverage": 4.7, 170 | "ratingsQuantity": 28, 171 | "price": 2997, 172 | "summary": "The most remote and stunningly beautiful places for seeing the night sky", 173 | "description": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 174 | "imageCover": "tour-8-cover.jpg", 175 | "images": [ 176 | "tour-8-1.jpg", 177 | "tour-8-2.jpg", 178 | "tour-8-3.jpg" 179 | ], 180 | "startDates": [ 181 | "2021-03-23,10:00", 182 | "2021-10-25,10:00", 183 | "2022-01-30,10:00" 184 | ] 185 | }, 186 | { 187 | "id": 8, 188 | "name": "The Northern Lights", 189 | "duration": 3, 190 | "maxGroupSize": 12, 191 | "difficulty": "easy", 192 | "ratingsAverage": 4.9, 193 | "ratingsQuantity": 33, 194 | "price": 1497, 195 | "summary": "Enjoy the Northern Lights in one of the best places in the world", 196 | "description": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua, ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum!\nDolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur, exercitation ullamco laboris nisi ut aliquip. Lorem ipsum dolor sit amet, consectetur adipisicing elit!", 197 | "imageCover": "tour-9-cover.jpg", 198 | "images": [ 199 | "tour-9-1.jpg", 200 | "tour-9-2.jpg", 201 | "tour-9-3.jpg" 202 | ], 203 | "startDates": [ 204 | "2021-12-16,10:00", 205 | "2022-01-16,10:00", 206 | "2022-12-12,10:00" 207 | ] 208 | } 209 | ] -------------------------------------------------------------------------------- /dev-data/data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_id": "5c8a1d5b0190b214360dc057", 4 | "name": "Jonas Schmedtmann", 5 | "email": "admin@natours.io", 6 | "role": "admin", 7 | "active": true, 8 | "photo": "user-1.jpg", 9 | "password": "$2a$12$Q0grHjH9PXc6SxivC8m12.2mZJ9BbKcgFpwSG4Y1ZEII8HJVzWeyS" 10 | }, 11 | { 12 | "_id": "5c8a1dfa2f8fb814b56fa181", 13 | "name": "Lourdes Browning", 14 | "email": "loulou@example.com", 15 | "role": "user", 16 | "active": true, 17 | "photo": "user-2.jpg", 18 | "password": "$2a$12$hP1h2pnNp7wgyZNRwPsOTeZuNzWBv7vHmsR3DT/OaPSUBQT.y0S.." 19 | }, 20 | { 21 | "_id": "5c8a1e1a2f8fb814b56fa182", 22 | "name": "Sophie Louise Hart", 23 | "email": "sophie@example.com", 24 | "role": "user", 25 | "active": true, 26 | "photo": "user-3.jpg", 27 | "password": "$2a$12$9nFqToiTmjgfFVJiQvjmreLt4k8X4gGYCETGapSZOb2hHa55t0dDq" 28 | }, 29 | { 30 | "_id": "5c8a1ec62f8fb814b56fa183", 31 | "name": "Ayla Cornell", 32 | "email": "ayls@example.com", 33 | "role": "user", 34 | "active": true, 35 | "photo": "user-4.jpg", 36 | "password": "$2a$12$tm33.M/4pfEbZF64WbFuHuVFv85v4qEhi.ik8njbud7yaoqCZpjiy" 37 | }, 38 | { 39 | "_id": "5c8a1f292f8fb814b56fa184", 40 | "name": "Leo Gillespie", 41 | "email": "leo@example.com", 42 | "role": "guide", 43 | "active": true, 44 | "photo": "user-5.jpg", 45 | "password": "$2a$12$OOPr90tBEBF1Iho3ox0Jde0O/WXUR0VLA5xdh6tWcu7qb.qOCvSg2" 46 | }, 47 | { 48 | "_id": "5c8a1f4e2f8fb814b56fa185", 49 | "name": "Jennifer Hardy", 50 | "email": "jennifer@example.com", 51 | "role": "guide", 52 | "active": true, 53 | "photo": "user-6.jpg", 54 | "password": "$2a$12$XCXvvlhRBJ8CydKH09v1v.jpg0hB9gVVfMVEoz4MsxqL9zb5PrF42" 55 | }, 56 | { 57 | "_id": "5c8a201e2f8fb814b56fa186", 58 | "name": "Kate Morrison", 59 | "email": "kate@example.com", 60 | "role": "guide", 61 | "active": true, 62 | "photo": "user-7.jpg", 63 | "password": "$2a$12$II1F3aBSFDF3Xz7iB4rk/.a2dogwkClMN5gGCWrRlILrG1xtJG7q6" 64 | }, 65 | { 66 | "_id": "5c8a20d32f8fb814b56fa187", 67 | "name": "Eliana Stout", 68 | "email": "eliana@example.com", 69 | "role": "user", 70 | "active": true, 71 | "photo": "user-8.jpg", 72 | "password": "$2a$12$Jb/ILhdDV.ZpnPMu19xfe.NRh5ntE2LzNMNcsty05QWwRbmFFVMKO" 73 | }, 74 | { 75 | "_id": "5c8a211f2f8fb814b56fa188", 76 | "name": "Cristian Vega", 77 | "email": "chris@example.com", 78 | "role": "user", 79 | "active": true, 80 | "photo": "user-9.jpg", 81 | "password": "$2a$12$r7/jtdWtzNfrfC7zw3uS.eDJ3Bs.8qrO31ZdbMljL.lUY0TAsaAL6" 82 | }, 83 | { 84 | "_id": "5c8a21d02f8fb814b56fa189", 85 | "name": "Steve T. Scaife", 86 | "email": "steve@example.com", 87 | "role": "lead-guide", 88 | "active": true, 89 | "photo": "user-10.jpg", 90 | "password": "$2a$12$q7v9dm.S4DvqhAeBc4KwduedEDEkDe2GGFGzteW6xnHt120oRpkqm" 91 | }, 92 | { 93 | "_id": "5c8a21f22f8fb814b56fa18a", 94 | "name": "Aarav Lynn", 95 | "email": "aarav@example.com", 96 | "role": "lead-guide", 97 | "active": true, 98 | "photo": "user-11.jpg", 99 | "password": "$2a$12$lKWhzujFvQwG4m/X3mnTneOB3ib9IYETsOqQ8aN5QEWDjX6X2wJJm" 100 | }, 101 | { 102 | "_id": "5c8a22c62f8fb814b56fa18b", 103 | "name": "Miyah Myles", 104 | "email": "miyah@example.com", 105 | "role": "lead-guide", 106 | "active": true, 107 | "photo": "user-12.jpg", 108 | "password": "$2a$12$.XIvvmznHQSa9UOI639yhe4vzHKCYO1vpTUZc4d45oiT4GOZQe1kS" 109 | }, 110 | { 111 | "_id": "5c8a23412f8fb814b56fa18c", 112 | "name": "Ben Hadley", 113 | "email": "ben@example.com", 114 | "role": "guide", 115 | "active": true, 116 | "photo": "user-13.jpg", 117 | "password": "$2a$12$D3fyuS9ETdBBw5lOwceTMuZcDTyVq28ieeGUAanIuLMcSDz6bpfIe" 118 | }, 119 | { 120 | "_id": "5c8a23c82f8fb814b56fa18d", 121 | "name": "Laura Wilson", 122 | "email": "laura@example.com", 123 | "role": "user", 124 | "active": true, 125 | "photo": "user-14.jpg", 126 | "password": "$2a$12$VPYaAAOsI44uhq11WbZ5R.cHT4.fGdlI9gKJd95jmYw3.sAsmbvBq" 127 | }, 128 | { 129 | "_id": "5c8a23de2f8fb814b56fa18e", 130 | "name": "Max Smith", 131 | "email": "max@example.com", 132 | "role": "user", 133 | "active": true, 134 | "photo": "user-15.jpg", 135 | "password": "$2a$12$l5qamwqcqC2NlgN6o5A5..9Fxzr6X.bjx/8j3a9jYUHWGOL99oXlm" 136 | }, 137 | { 138 | "_id": "5c8a24282f8fb814b56fa18f", 139 | "name": "Isabel Kirkland", 140 | "email": "isabel@example.com", 141 | "role": "user", 142 | "active": true, 143 | "photo": "user-16.jpg", 144 | "password": "$2a$12$IUnwPH0MGFeMuz7g4gtfvOll.9wgLyxG.9C3TKlttfLtCQWEE6GIu" 145 | }, 146 | { 147 | "_id": "5c8a24402f8fb814b56fa190", 148 | "name": "Alexander Jones", 149 | "email": "alex@example.com", 150 | "role": "user", 151 | "active": true, 152 | "photo": "user-17.jpg", 153 | "password": "$2a$12$NnclhoYFNcSApoQ3ML8kk.b4B3gbpOmZJLfqska07miAnXukOgK6y" 154 | }, 155 | { 156 | "_id": "5c8a245f2f8fb814b56fa191", 157 | "name": "Eduardo Hernandez", 158 | "email": "edu@example.com", 159 | "role": "user", 160 | "active": true, 161 | "photo": "user-18.jpg", 162 | "password": "$2a$12$uB5H1OxLMOqDYTuTlptAoewlovENJvjrLwzsL1wUZ6OkAIByPPBGq" 163 | }, 164 | { 165 | "_id": "5c8a24822f8fb814b56fa192", 166 | "name": "John Riley", 167 | "email": "john@example.com", 168 | "role": "user", 169 | "active": true, 170 | "photo": "user-19.jpg", 171 | "password": "$2a$12$11JElTatQlAFo1Obw/dwd..vuVmQyYS7MT14pkl3lRvVPjGA00G8O" 172 | }, 173 | { 174 | "_id": "5c8a24a02f8fb814b56fa193", 175 | "name": "Lisa Brown", 176 | "email": "lisa@example.com", 177 | "role": "user", 178 | "active": true, 179 | "photo": "user-20.jpg", 180 | "password": "$2a$12$uA9FsDw63v6dkJKGlLQ/8ufYBs8euB7kqIQewyYlZXU5azEKeLEky" 181 | } 182 | ] -------------------------------------------------------------------------------- /dev-data/img/aarav.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/dev-data/img/aarav.jpg -------------------------------------------------------------------------------- /dev-data/img/leo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/dev-data/img/leo.jpg -------------------------------------------------------------------------------- /dev-data/img/monica.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/dev-data/img/monica.jpg -------------------------------------------------------------------------------- /dev-data/img/new-tour-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/dev-data/img/new-tour-1.jpg -------------------------------------------------------------------------------- /dev-data/img/new-tour-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/dev-data/img/new-tour-2.jpg -------------------------------------------------------------------------------- /dev-data/img/new-tour-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/dev-data/img/new-tour-3.jpg -------------------------------------------------------------------------------- /dev-data/img/new-tour-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/dev-data/img/new-tour-4.jpg -------------------------------------------------------------------------------- /dev-data/templates/accountTemplate.pug: -------------------------------------------------------------------------------- 1 | main.main 2 | .user-view 3 | nav.user-view__menu 4 | ul.side-nav 5 | li.side-nav--active 6 | a(href='#') 7 | svg 8 | use(xlink:href='img/icons.svg#icon-settings') 9 | | Settings 10 | li 11 | a(href='#') 12 | svg 13 | use(xlink:href='img/icons.svg#icon-briefcase') 14 | | My bookings 15 | li 16 | a(href='#') 17 | svg 18 | use(xlink:href='img/icons.svg#icon-star') 19 | | My reviews 20 | li 21 | a(href='#') 22 | svg 23 | use(xlink:href='img/icons.svg#icon-credit-card') 24 | | Billing 25 | .admin-nav 26 | h5.admin-nav__heading Admin 27 | ul.side-nav 28 | li 29 | a(href='#') 30 | svg 31 | use(xlink:href='img/icons.svg#icon-map') 32 | | Manage tours 33 | li 34 | a(href='#') 35 | svg 36 | use(xlink:href='img/icons.svg#icon-users') 37 | | Manage users 38 | li 39 | a(href='#') 40 | svg 41 | use(xlink:href='img/icons.svg#icon-star') 42 | | Manage reviews 43 | li 44 | a(href='#') 45 | svg 46 | use(xlink:href='img/icons.svg#icon-briefcase') 47 | 48 | .user-view__content 49 | .user-view__form-container 50 | h2.heading-secondary.ma-bt-md Your account settings 51 | form.form.form-user-data 52 | .form__group 53 | label.form__label(for='name') Name 54 | input#name.form__input(type='text', value='Jonas Schmedtmann', required) 55 | .form__group.ma-bt-md 56 | label.form__label(for='email') Email address 57 | input#email.form__input(type='email', value='admin@natours.io', required) 58 | .form__group.form__photo-upload 59 | img.form__user-photo(src='img/user.jpg', alt='User photo') 60 | a.btn-text(href='') Choose new photo 61 | .form__group.right 62 | button.btn.btn--small.btn--green Save settings 63 | .line   64 | .user-view__form-container 65 | h2.heading-secondary.ma-bt-md Password change 66 | form.form.form-user-settings 67 | .form__group 68 | label.form__label(for='password-current') Current password 69 | input#password-current.form__input(type='password', placeholder='••••••••', required, minlength='8') 70 | .form__group 71 | label.form__label(for='password') New password 72 | input#password.form__input(type='password', placeholder='••••••••', required, minlength='8') 73 | .form__group.ma-bt-lg 74 | label.form__label(for='password-confirm') Confirm password 75 | input#password-confirm.form__input(type='password', placeholder='••••••••', required, minlength='8') 76 | .form__group.right 77 | button.btn.btn--small.btn--green Save password -------------------------------------------------------------------------------- /dev-data/templates/emailTemplate.pug: -------------------------------------------------------------------------------- 1 | //- Email template adapted from https://github.com/leemunroe/responsive-html-email-template 2 | //- Converted from HTML using https://html2pug.now.sh/ 3 | 4 | doctype html 5 | html 6 | head 7 | meta(name='viewport', content='width=device-width') 8 | meta(http-equiv='Content-Type', content='text/html; charset=UTF-8') 9 | title= subject 10 | 11 | style. 12 | img { 13 | border: none; 14 | -ms-interpolation-mode: bicubic; 15 | max-width: 100%; 16 | } 17 | body { 18 | background-color: #f6f6f6; 19 | font-family: sans-serif; 20 | -webkit-font-smoothing: antialiased; 21 | font-size: 14px; 22 | line-height: 1.4; 23 | margin: 0; 24 | padding: 0; 25 | -ms-text-size-adjust: 100%; 26 | -webkit-text-size-adjust: 100%; 27 | } 28 | table { 29 | border-collapse: separate; 30 | mso-table-lspace: 0pt; 31 | mso-table-rspace: 0pt; 32 | width: 100%; } 33 | table td { 34 | font-family: sans-serif; 35 | font-size: 14px; 36 | vertical-align: top; 37 | } 38 | .body { 39 | background-color: #f6f6f6; 40 | width: 100%; 41 | } 42 | .container { 43 | display: block; 44 | margin: 0 auto !important; 45 | /* makes it centered */ 46 | max-width: 580px; 47 | padding: 10px; 48 | width: 580px; 49 | } 50 | .content { 51 | box-sizing: border-box; 52 | display: block; 53 | margin: 0 auto; 54 | max-width: 580px; 55 | padding: 10px; 56 | } 57 | .main { 58 | background: #ffffff; 59 | border-radius: 3px; 60 | width: 100%; 61 | } 62 | .wrapper { 63 | box-sizing: border-box; 64 | padding: 20px; 65 | } 66 | .content-block { 67 | padding-bottom: 10px; 68 | padding-top: 10px; 69 | } 70 | .footer { 71 | clear: both; 72 | margin-top: 10px; 73 | text-align: center; 74 | width: 100%; 75 | } 76 | .footer td, 77 | .footer p, 78 | .footer span, 79 | .footer a { 80 | color: #999999; 81 | font-size: 12px; 82 | text-align: center; 83 | } 84 | h1, 85 | h2, 86 | h3, 87 | h4 { 88 | color: #000000; 89 | font-family: sans-serif; 90 | font-weight: 400; 91 | line-height: 1.4; 92 | margin: 0; 93 | margin-bottom: 30px; 94 | } 95 | h1 { 96 | font-size: 35px; 97 | font-weight: 300; 98 | text-align: center; 99 | text-transform: capitalize; 100 | } 101 | p, 102 | ul, 103 | ol { 104 | font-family: sans-serif; 105 | font-size: 14px; 106 | font-weight: normal; 107 | margin: 0; 108 | margin-bottom: 15px; 109 | } 110 | p li, 111 | ul li, 112 | ol li { 113 | list-style-position: inside; 114 | margin-left: 5px; 115 | } 116 | a { 117 | color: #55c57a; 118 | text-decoration: underline; 119 | } 120 | .btn { 121 | box-sizing: border-box; 122 | width: 100%; } 123 | .btn > tbody > tr > td { 124 | padding-bottom: 15px; } 125 | .btn table { 126 | width: auto; 127 | } 128 | .btn table td { 129 | background-color: #ffffff; 130 | border-radius: 5px; 131 | text-align: center; 132 | } 133 | .btn a { 134 | background-color: #ffffff; 135 | border: solid 1px #55c57a; 136 | border-radius: 5px; 137 | box-sizing: border-box; 138 | color: #55c57a; 139 | cursor: pointer; 140 | display: inline-block; 141 | font-size: 14px; 142 | font-weight: bold; 143 | margin: 0; 144 | padding: 12px 25px; 145 | text-decoration: none; 146 | text-transform: capitalize; 147 | } 148 | .btn-primary table td { 149 | background-color: #55c57a; 150 | } 151 | .btn-primary a { 152 | background-color: #55c57a; 153 | border-color: #55c57a; 154 | color: #ffffff; 155 | } 156 | 157 | .last { 158 | margin-bottom: 0; 159 | } 160 | .first { 161 | margin-top: 0; 162 | } 163 | .align-center { 164 | text-align: center; 165 | } 166 | .align-right { 167 | text-align: right; 168 | } 169 | .align-left { 170 | text-align: left; 171 | } 172 | .clear { 173 | clear: both; 174 | } 175 | .mt0 { 176 | margin-top: 0; 177 | } 178 | .mb0 { 179 | margin-bottom: 0; 180 | } 181 | .preheader { 182 | color: transparent; 183 | display: none; 184 | height: 0; 185 | max-height: 0; 186 | max-width: 0; 187 | opacity: 0; 188 | overflow: hidden; 189 | mso-hide: all; 190 | visibility: hidden; 191 | width: 0; 192 | } 193 | .powered-by a { 194 | text-decoration: none; 195 | } 196 | hr { 197 | border: 0; 198 | border-bottom: 1px solid #f6f6f6; 199 | margin: 20px 0; 200 | } 201 | @media only screen and (max-width: 620px) { 202 | table[class=body] h1 { 203 | font-size: 28px !important; 204 | margin-bottom: 10px !important; 205 | } 206 | table[class=body] p, 207 | table[class=body] ul, 208 | table[class=body] ol, 209 | table[class=body] td, 210 | table[class=body] span, 211 | table[class=body] a { 212 | font-size: 16px !important; 213 | } 214 | table[class=body] .wrapper, 215 | table[class=body] .article { 216 | padding: 10px !important; 217 | } 218 | table[class=body] .content { 219 | padding: 0 !important; 220 | } 221 | table[class=body] .container { 222 | padding: 0 !important; 223 | width: 100% !important; 224 | } 225 | table[class=body] .main { 226 | border-left-width: 0 !important; 227 | border-radius: 0 !important; 228 | border-right-width: 0 !important; 229 | } 230 | table[class=body] .btn table { 231 | width: 100% !important; 232 | } 233 | table[class=body] .btn a { 234 | width: 100% !important; 235 | } 236 | table[class=body] .img-responsive { 237 | height: auto !important; 238 | max-width: 100% !important; 239 | width: auto !important; 240 | } 241 | } 242 | @media all { 243 | .ExternalClass { 244 | width: 100%; 245 | } 246 | .ExternalClass, 247 | .ExternalClass p, 248 | .ExternalClass span, 249 | .ExternalClass font, 250 | .ExternalClass td, 251 | .ExternalClass div { 252 | line-height: 100%; 253 | } 254 | .apple-link a { 255 | color: inherit !important; 256 | font-family: inherit !important; 257 | font-size: inherit !important; 258 | font-weight: inherit !important; 259 | line-height: inherit !important; 260 | text-decoration: none !important; 261 | } 262 | .btn-primary table td:hover { 263 | background-color: #2e864b !important; 264 | } 265 | .btn-primary a:hover { 266 | background-color: #2e864b !important; 267 | border-color: #2e864b !important; 268 | } 269 | } 270 | 271 | body 272 | table.body(role='presentation', border='0', cellpadding='0', cellspacing='0') 273 | tbody 274 | tr 275 | td 276 | td.container 277 | .content 278 | // START CENTERED WHITE CONTAINER 279 | table.main(role='presentation') 280 | 281 | // START MAIN AREA 282 | tbody 283 | tr 284 | td.wrapper 285 | table(role='presentation', border='0', cellpadding='0', cellspacing='0') 286 | tbody 287 | tr 288 | td 289 | // CONTENT 290 | p Hi NAME, 291 | p Welcome to Natours, we're glad to have you 🎉🙏 292 | p We're all a big familiy here, so make sure to upload your user photo so we get to know you a bit better! 293 | table.btn.btn-primary(role='presentation', border='0', cellpadding='0', cellspacing='0') 294 | tbody 295 | tr 296 | td(align='left') 297 | table(role='presentation', border='0', cellpadding='0', cellspacing='0') 298 | tbody 299 | tr 300 | td 301 | a(href='#', target='_blank') Upload user photo 302 | p If you need any help with booking your next tour, please don't hesitate to contact me! 303 | p - Jonas Schmedtmann, CEO 304 | 305 | // START FOOTER 306 | .footer 307 | table(role='presentation', border='0', cellpadding='0', cellspacing='0') 308 | tbody 309 | tr 310 | td.content-block 311 | span.apple-link Natours Inc, 123 Nowhere Road, San Francisco CA 99999 312 | br 313 | | Don't like these emails? 314 | a(href='#') Unsubscribe 315 | //- td   316 | -------------------------------------------------------------------------------- /dev-data/templates/errorTemplate.pug: -------------------------------------------------------------------------------- 1 | main.main 2 | .error 3 | .error__title 4 | h2.heading-secondary.heading-secondary--error Uh oh! Something went wrong! 5 | h2.error__emoji 😢 🤯 6 | .error__msg Page not found! 7 | -------------------------------------------------------------------------------- /dev-data/templates/loginTemplate.pug: -------------------------------------------------------------------------------- 1 | main.main 2 | .login-form 3 | h2.heading-secondary.ma-bt-lg Log into your account 4 | form.form 5 | .form__group 6 | label.form__label(for='email') Email address 7 | input#email.form__input(type='email', placeholder='you@example.com', required) 8 | .form__group.ma-bt-md 9 | label.form__label(for='password') Password 10 | input#password.form__input(type='password', placeholder='••••••••', required, minlength='8') 11 | .form__group 12 | button.btn.btn--green Login 13 | -------------------------------------------------------------------------------- /dev-data/templates/tourCardTemplate.pug: -------------------------------------------------------------------------------- 1 | .card 2 | .card__header 3 | .card__picture 4 | .card__picture-overlay   5 | img.card__picture-img(src='img/tour-1-cover.jpg', alt='Tour 1') 6 | h3.heading-tertirary 7 | span The Forest Hiker 8 | 9 | .card__details 10 | h4.card__sub-heading Easy 5-day tour 11 | p.card__text Breathtaking hike through the Canadian Banff National Park 12 | .card__data 13 | svg.card__icon 14 | use(xlink:href='img/icons.svg#icon-map-pin') 15 | span Banff, Canada 16 | .card__data 17 | svg.card__icon 18 | use(xlink:href='img/icons.svg#icon-calendar') 19 | span April 2021 20 | .card__data 21 | svg.card__icon 22 | use(xlink:href='img/icons.svg#icon-flag') 23 | span 3 stops 24 | .card__data 25 | svg.card__icon 26 | use(xlink:href='img/icons.svg#icon-user') 27 | span 25 people 28 | 29 | .card__footer 30 | p 31 | span.card__footer-value $297 32 | span.card__footer-text per person 33 | p.card__ratings 34 | span.card__footer-value 4.9 35 | span.card__footer-text rating (21) 36 | a.btn.btn--green.btn--small(href='#') Details 37 | -------------------------------------------------------------------------------- /dev-data/templates/tourTemplate.pug: -------------------------------------------------------------------------------- 1 | section.section-header 2 | .header__hero 3 | .header__hero-overlay   4 | img.header__hero-img(src='/img/tour-5-cover.jpg', alt='Tour 5') 5 | 6 | .heading-box 7 | h1.heading-primary 8 | span The Park Camper Tour 9 | .heading-box__group 10 | .heading-box__detail 11 | svg.heading-box__icon 12 | use(xlink:href='/img/icons.svg#icon-clock') 13 | span.heading-box__text 10 days 14 | .heading-box__detail 15 | svg.heading-box__icon 16 | use(xlink:href='/img/icons.svg#icon-map-pin') 17 | span.heading-box__text Las Vegas, USA 18 | 19 | section.section-description 20 | .overview-box 21 | div 22 | .overview-box__group 23 | h2.heading-secondary.ma-bt-lg Quick facts 24 | .overview-box__detail 25 | svg.overview-box__icon 26 | use(xlink:href='/img/icons.svg#icon-calendar') 27 | span.overview-box__label Next date 28 | span.overview-box__text August 2021 29 | .overview-box__detail 30 | svg.overview-box__icon 31 | use(xlink:href='/img/icons.svg#icon-trending-up') 32 | span.overview-box__label Difficulty 33 | span.overview-box__text Medium 34 | .overview-box__detail 35 | svg.overview-box__icon 36 | use(xlink:href='/img/icons.svg#icon-user') 37 | span.overview-box__label Participants 38 | span.overview-box__text 10 people 39 | .overview-box__detail 40 | svg.overview-box__icon 41 | use(xlink:href='/img/icons.svg#icon-star') 42 | span.overview-box__label Rating 43 | span.overview-box__text 4.9 / 5 44 | 45 | .overview-box__group 46 | h2.heading-secondary.ma-bt-lg Your tour guides 47 | .overview-box__detail 48 | img.overview-box__img(src='/img/users/user-19.jpg', alt='Lead guide') 49 | span.overview-box__label Lead guide 50 | span.overview-box__text Steven Miller 51 | .overview-box__detail 52 | img.overview-box__img(src='/img/users/user-18.jpg', alt='Tour guide') 53 | span.overview-box__label Tour guide 54 | span.overview-box__text Lisa Brown 55 | .overview-box__detail 56 | img.overview-box__img(src='/img/users/user-17.jpg', alt='Intern') 57 | span.overview-box__label Intern 58 | span.overview-box__text Max Smith 59 | 60 | .description-box 61 | h2.heading-secondary.ma-bt-lg About the park camper tour 62 | p.description__text Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 63 | p.description__text Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum! 64 | 65 | section.section-pictures 66 | .picture-box 67 | img.picture-box__img.picture-box__img--1(src='/img/tour-5-1.jpg', alt='The Park Camper Tour 1') 68 | .picture-box 69 | img.picture-box__img.picture-box__img--2(src='/img/tour-5-2.jpg', alt='The Park Camper Tour 1') 70 | .picture-box 71 | img.picture-box__img.picture-box__img--3(src='/img/tour-5-3.jpg', alt='The Park Camper Tour 1') 72 | 73 | section.section-map 74 | #map 75 | 76 | section.section-reviews 77 | .reviews 78 | 79 | .reviews__card 80 | .reviews__avatar 81 | img.reviews__avatar-img(src='/img/users/user-7.jpg', alt='Jim Brown') 82 | h6.reviews__user Jim Brown 83 | p.reviews__text Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque dignissimos sint quo commodi corrupti accusantium veniam saepe numquam. 84 | .reviews__rating 85 | svg.reviews__star.reviews__star--active 86 | use(xlink:href='/img/icons.svg#icon-star') 87 | svg.reviews__star.reviews__star--active 88 | use(xlink:href='/img/icons.svg#icon-star') 89 | svg.reviews__star.reviews__star--active 90 | use(xlink:href='/img/icons.svg#icon-star') 91 | svg.reviews__star.reviews__star--active 92 | use(xlink:href='/img/icons.svg#icon-star') 93 | svg.reviews__star.reviews__star--active 94 | use(xlink:href='/img/icons.svg#icon-star') 95 | 96 | section.section-cta 97 | .cta 98 | .cta__img.cta__img--logo 99 | img(src='/img/logo-white.png', alt='Natours logo') 100 | img.cta__img.cta__img--1(src='/img/tour-5-2.jpg', alt='') 101 | img.cta__img.cta__img--2(src='/img/tour-5-1.jpg', alt='') 102 | .cta__content 103 | h2.heading-secondary What are you waiting for? 104 | p.cta__text 10 days. 1 adventure. Infinite memories. Make it yours today! 105 | button.btn.btn--green.span-all-rows Book tour now! 106 | -------------------------------------------------------------------------------- /models/bookingModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const bookingSchema = new mongoose.Schema({ 4 | tour: { 5 | type: mongoose.Schema.ObjectId, 6 | ref: "Tour", 7 | required: [true, "Booking must belong to a Tour!"] 8 | }, 9 | user: { 10 | type: mongoose.Schema.ObjectId, 11 | ref: "User", 12 | required: [true, "Booking must belong to a User!"] 13 | }, 14 | price: { 15 | type: Number, 16 | require: [true, "Booking must have a price."] 17 | }, 18 | createdAt: { 19 | type: Date, 20 | default: Date.now() 21 | }, 22 | paid: { 23 | type: Boolean, 24 | default: false 25 | } 26 | }); 27 | 28 | bookingSchema.pre(/^find/, function(next) { 29 | this.populate("user").populate({ 30 | path: "tour", 31 | select: "name" 32 | }); 33 | next(); 34 | }); 35 | 36 | const Booking = mongoose.model("Booking", bookingSchema); 37 | 38 | export default Booking; 39 | -------------------------------------------------------------------------------- /models/errorModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const errrorSchema = new mongoose.Schema({ 4 | status: { 5 | type: String, 6 | required: [true, "Error has a status"] 7 | }, 8 | error: { 9 | type: Object, 10 | required: [true, "Error has a error name"] 11 | }, 12 | message: { 13 | type: String, 14 | required: [true, "Error has a message"] 15 | }, 16 | stack: { 17 | type: String 18 | // required: [true, 'Error has a stack'] 19 | } 20 | }); 21 | 22 | const ErrorStack = mongoose.model("ErrorStack", errrorSchema); 23 | 24 | export default ErrorStack; 25 | -------------------------------------------------------------------------------- /models/reviewModel.js: -------------------------------------------------------------------------------- 1 | // review / rating / createdAt / ref to tour / ref to user 2 | import mongoose from "mongoose"; 3 | import Tour from "./tourModel.js"; 4 | 5 | const reviewSchema = new mongoose.Schema( 6 | { 7 | review: { 8 | type: String, 9 | required: [true, "Review can not be empty!"] 10 | }, 11 | rating: { 12 | type: Number, 13 | min: 1, 14 | max: 5 15 | }, 16 | createdAt: { 17 | type: Date, 18 | default: Date.now 19 | }, 20 | tour: { 21 | type: mongoose.Schema.ObjectId, 22 | ref: "Tour", 23 | required: [true, "Review must belong to a tour."] 24 | }, 25 | user: { 26 | type: mongoose.Schema.ObjectId, 27 | ref: "User", 28 | required: [true, "Review must belong to a user."] 29 | } 30 | }, 31 | { 32 | toJSON: { virtuals: true }, 33 | toObject: { virtuals: true } 34 | } 35 | ); 36 | 37 | reviewSchema.index({ tour: 1, user: 1 }, { unique: true }); 38 | 39 | reviewSchema.pre(/^find/, function(next) { 40 | this.populate({ 41 | path: "user", 42 | select: "name photo" 43 | }); 44 | 45 | next(); 46 | }); 47 | 48 | reviewSchema.statics.calcAverageRatings = async function(tourId) { 49 | const stats = await this.aggregate([ 50 | { 51 | $match: { tour: tourId } 52 | }, 53 | { 54 | $group: { 55 | _id: "$tour", 56 | nRating: { $sum: 1 }, 57 | avgRating: { $avg: "$rating" } 58 | } 59 | } 60 | ]); 61 | // console.log(stats); 62 | 63 | if (stats.length > 0) { 64 | await Tour.findByIdAndUpdate(tourId, { 65 | ratingsQuantity: stats[0].nRating, 66 | ratingsAverage: stats[0].avgRating 67 | }); 68 | } else { 69 | await Tour.findByIdAndUpdate(tourId, { 70 | ratingsQuantity: 0, 71 | ratingsAverage: 4.5 72 | }); 73 | } 74 | }; 75 | 76 | reviewSchema.post("save", function() { 77 | // this points to current review 78 | this.constructor.calcAverageRatings(this.tour); 79 | }); 80 | 81 | // findByIdAndUpdate 82 | // findByIdAndDelete 83 | reviewSchema.pre(/^findOneAnd/, async function(next) { 84 | this.rev = await this.findOne(); 85 | // console.log(this.rev); 86 | next(); 87 | }); 88 | 89 | reviewSchema.post(/^findOneAnd/, async function() { 90 | // await this.findOne(); does NOT work here, query has already executed 91 | await this.rev.constructor.calcAverageRatings(this.rev.tour); 92 | }); 93 | 94 | const Review = mongoose.model("Review", reviewSchema); 95 | 96 | export default Review; 97 | -------------------------------------------------------------------------------- /models/tourModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import slugify from "slugify"; 3 | // const User = require('./userModel'); 4 | // const validator = require('validator'); 5 | 6 | const tourSchema = new mongoose.Schema( 7 | { 8 | name: { 9 | type: String, 10 | required: [true, "A tour must have a name"], 11 | unique: true, 12 | trim: true, 13 | maxlength: [40, "A tour name must have less or equal then 40 characters"], 14 | minlength: [10, "A tour name must have more or equal then 10 characters"] 15 | // validate: [validator.isAlpha, 'Tour name must only contain caracters'] 16 | }, 17 | slug: String, 18 | duration: { 19 | type: Number, 20 | required: [true, "A tour must have a duration"] 21 | }, 22 | maxGroupSize: { 23 | type: Number, 24 | required: [true, "A tour must have a group size"] 25 | }, 26 | difficulty: { 27 | type: String, 28 | required: [true, "A tour must have a difficulty"], 29 | enum: { 30 | values: ["easy", "medium", "difficult"], 31 | message: "Difficulty is either: easy, medium, difficulty" 32 | } 33 | }, 34 | ratingsAverage: { 35 | type: Number, 36 | default: 4.5, 37 | min: [1, "Rating must be above 1.0"], 38 | max: [5, "Rating must be below 5.0"], 39 | set: val => Math.round(val * 10) / 10 40 | }, 41 | ratingsQuantity: { 42 | type: Number, 43 | default: 0 44 | }, 45 | price: { 46 | type: Number, 47 | required: [true, "A tour must have a price"] 48 | }, 49 | priceDiscount: { 50 | type: Number, 51 | validate: { 52 | validator: function(value) { 53 | // this only points to current doc on NEW documnet creation 54 | return value < this.price; 55 | }, 56 | message: "Discount price ({VALUE}) should be below regular price" 57 | } 58 | }, 59 | summary: { 60 | type: String, 61 | trim: true, 62 | required: [true, "A tour must have a description"] 63 | }, 64 | description: { 65 | type: String, 66 | trim: true 67 | }, 68 | imageCover: { 69 | type: String, 70 | required: [true, "A tour must have a cover image"] 71 | }, 72 | images: [String], 73 | createdAt: { 74 | type: Date, 75 | default: Date.now(), 76 | select: false 77 | }, 78 | startDates: [Date], 79 | secretTour: { 80 | type: Boolean, 81 | default: false 82 | }, 83 | startLocation: { 84 | // GeoJSON 85 | type: { 86 | type: String, 87 | default: "Point", 88 | enum: ["Point"] 89 | }, 90 | coordinates: [Number], 91 | address: String, 92 | description: String 93 | }, 94 | locations: [ 95 | { 96 | type: { 97 | type: String, 98 | default: "Point", 99 | enum: ["Point"] 100 | }, 101 | coordinates: [Number], 102 | address: String, 103 | description: String, 104 | day: Number 105 | } 106 | ], 107 | guides: [ 108 | { 109 | type: mongoose.Schema.ObjectId, 110 | ref: "User" 111 | } 112 | ] 113 | }, 114 | { 115 | toJSON: { virtuals: true }, 116 | toObject: { virtuals: true } 117 | } 118 | ); 119 | 120 | tourSchema.index({ price: 1, ratingsAverage: -1 }); 121 | tourSchema.index({ slug: 1 }); 122 | tourSchema.index({ startLocation: "2dsphere" }); 123 | 124 | tourSchema.virtual("durationWeeks").get(function() { 125 | return this.duration / 7; 126 | }); 127 | 128 | // Virtual populate 129 | tourSchema.virtual("reviews", { 130 | ref: "Review", 131 | foreignField: "tour", 132 | localField: "_id" 133 | }); 134 | 135 | // DOCUMENT MIDDLEWARE: runs before .save() and .create() !.update() 136 | tourSchema.pre("save", function(next) { 137 | this.slug = slugify(this.name, { lower: true }); 138 | next(); 139 | }); 140 | 141 | // tourSchema.pre('save', async function(next) { 142 | // const guidesPromises = this.guides.map(async id => await User.findById(id)); 143 | // this.guides = await Promise.all(guidesPromises); 144 | 145 | // next(); 146 | // }); 147 | 148 | // tourSchema.pre('save', function(next) { 149 | // console.log('Document will save....'); 150 | // next(); 151 | // }); 152 | 153 | // tourSchema.post('save', function(doc, next) { 154 | // console.log(doc); 155 | // next(); 156 | // }); 157 | 158 | // QUERY MIDDLEWARE 159 | // tourSchema.pre('find', function(next) { 160 | 161 | tourSchema.pre(/^find/, function(next) { 162 | this.find({ secretTour: { $ne: true } }); 163 | 164 | this.start = Date.now(); 165 | next(); 166 | }); 167 | 168 | tourSchema.pre(/^find/, function(next) { 169 | this.populate({ 170 | path: "guides", 171 | select: "-__v -passwordChangedAt" 172 | }); 173 | 174 | next(); 175 | }); 176 | 177 | // tourSchema.post(/^find/, function(docs, next) { 178 | // console.log(`Query took ${Date.now() - this.start} millisecnds!`); 179 | // next(); 180 | // }); 181 | 182 | // AGGREGATION MIDDLEWARE 183 | // tourSchema.pre('aggregate', function(next) { 184 | // this.pipeline().unshift({ $match: { secretTour: { $ne: true } } }); // removing all the documents from the output which have secretTour set to true 185 | // console.log(this.pipeline()); 186 | // next(); 187 | // }); 188 | 189 | const Tour = mongoose.model("Tour", tourSchema); 190 | 191 | export default Tour; 192 | -------------------------------------------------------------------------------- /models/userModel.js: -------------------------------------------------------------------------------- 1 | import crypo from "crypto"; 2 | import mongoose from "mongoose"; 3 | import validator from "validator"; 4 | import bcrypt from "bcryptjs"; 5 | 6 | const userSchema = new mongoose.Schema({ 7 | name: { 8 | type: String, 9 | required: [true, "Please tell us your name"] 10 | }, 11 | email: { 12 | type: String, 13 | required: [true, "Please provide your email"], 14 | unique: true, 15 | lowercase: true, 16 | validate: [validator.isEmail, "Please provied a valid email"] 17 | }, 18 | photo: { 19 | type: String, 20 | default: "default.jpg" 21 | }, 22 | role: { 23 | type: String, 24 | enum: ["user", "guide", "lead-guide", "admin"], 25 | default: "user" 26 | }, 27 | password: { 28 | type: String, 29 | required: [true, "Please provied a password"], 30 | minlength: 8, 31 | select: false 32 | }, 33 | passwordConfirm: { 34 | type: String, 35 | required: [true, "Please confirm your password"], 36 | validate: { 37 | // This only works on CREATE and SAVE!! 38 | validator: function(el) { 39 | return el === this.password; 40 | }, 41 | message: "Passwords are not the same!" 42 | } 43 | }, 44 | passwordChangedAt: Date, 45 | passwordResetToken: String, 46 | passwordResetExpires: Date, 47 | active: { 48 | type: Boolean, 49 | default: true, 50 | select: false 51 | } 52 | }); 53 | 54 | userSchema.pre("save", async function(next) { 55 | // Only run this function if password was actually modified 56 | if (!this.isModified("password")) return next(); 57 | 58 | // Hash the password with cost of 12 59 | this.password = await bcrypt.hash(this.password, 12); 60 | 61 | // Delete passwordConfirm field 62 | this.passwordConfirm = undefined; 63 | next(); 64 | }); 65 | 66 | userSchema.pre("save", function(next) { 67 | if (!this.isModified("password") || this.isNew) return next(); 68 | 69 | this.passwordChangedAt = Date.now() - 1000; 70 | next(); 71 | }); 72 | 73 | userSchema.pre(/^find/, function(next) { 74 | // this points to the current query 75 | this.find({ active: { $ne: false } }); 76 | next(); 77 | }); 78 | 79 | userSchema.methods.correctPassword = async function(candidatePassword, userPassword) { 80 | return await bcrypt.compare(candidatePassword, userPassword); 81 | }; 82 | 83 | userSchema.methods.changedPasswordAfter = function(JWTTimestamp) { 84 | if (this.passwordChangedAt) { 85 | const changedTimestamp = parseInt(this.passwordChangedAt.getTime() / 1000, 10); 86 | 87 | return JWTTimestamp < changedTimestamp; 88 | } 89 | // False means NOT changed 90 | return false; 91 | }; 92 | 93 | userSchema.methods.createPasswordResetToken = function() { 94 | const resetToken = crypo.randomBytes(32).toString("hex"); 95 | 96 | this.passwordResetToken = crypo 97 | .createHash("sha256") 98 | .update(resetToken) 99 | .digest("hex"); 100 | 101 | // console.log({ resetToken }, this.passwordResetToken); 102 | 103 | this.passwordResetExpires = Date.now() + 10 * 60 * 1000; 104 | 105 | return resetToken; 106 | }; 107 | 108 | const User = mongoose.model("User", userSchema); 109 | 110 | export default User; 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-natours", 3 | "version": "1.0.0", 4 | "author": "Lakshman Gope", 5 | "description": "A mockup landing page for a travel agency startup.", 6 | "main": "app.js", 7 | "type": "module", 8 | "license": "ISC", 9 | "homepage": "https://github.com/lgope/Natours#readme", 10 | "scripts": { 11 | "start": "node server.js", 12 | "dev": "nodemon server.js", 13 | "start:prod": "NODE_ENV=production nodemon server.js", 14 | "debug": "ndb server.js", 15 | "watch:js": "parcel watch ./public/js/index.js --out-dir ./public/js --out-file bundle.js", 16 | "build:js": "parcel build ./public/js/index.js --out-dir ./public/js --out-file bundle.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/lgope/Natours.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/lgope/Natours/issues" 24 | }, 25 | "dependencies": { 26 | "@babel/polyfill": "^7.7.0", 27 | "a": "^3.0.1", 28 | "axios": "^0.21.2", 29 | "bcryptjs": "^2.4.3", 30 | "body-parser": "^1.19.0", 31 | "chalk": "^4.0.0", 32 | "compression": "^1.7.4", 33 | "cookie-parser": "^1.4.4", 34 | "cors": "^2.8.5", 35 | "dotenv": "^8.2.0", 36 | "express": "^4.17.3", 37 | "express-mongo-sanitize": "^1.3.2", 38 | "express-rate-limit": "^5.0.0", 39 | "helmet": "^3.21.2", 40 | "hpp": "^0.2.2", 41 | "html-to-text": "^5.1.1", 42 | "jsonwebtoken": "^8.5.1", 43 | "mongoose": "^5.13.15", 44 | "morgan": "^1.9.1", 45 | "multer": "^1.4.2", 46 | "ndb": "^1.1.5", 47 | "nodemailer": "^6.6.1", 48 | "pug": "^3.0.1", 49 | "sharp": "^0.28.3", 50 | "slugify": "^1.3.5", 51 | "stripe": "^7.14.0", 52 | "validator": "^13.7.0", 53 | "xss-clean": "^0.1.1" 54 | }, 55 | "devDependencies": { 56 | "eslint": "^6.6.0", 57 | "eslint-config-airbnb": "^18.0.1", 58 | "eslint-config-prettier": "^6.7.0", 59 | "eslint-plugin-import": "^2.18.2", 60 | "eslint-plugin-jsx-a11y": "^6.2.3", 61 | "eslint-plugin-node": "^10.0.0", 62 | "eslint-plugin-prettier": "^3.1.1", 63 | "eslint-plugin-react": "^7.16.0", 64 | "parcel-bundler": "^1.12.5", 65 | "prettier": "^1.19.1" 66 | }, 67 | "engines": { 68 | "node": "^10" 69 | } 70 | } -------------------------------------------------------------------------------- /public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/favicon.png -------------------------------------------------------------------------------- /public/img/logo-green-round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/logo-green-round.png -------------------------------------------------------------------------------- /public/img/logo-green-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/logo-green-small.png -------------------------------------------------------------------------------- /public/img/logo-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/logo-green.png -------------------------------------------------------------------------------- /public/img/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/logo-white.png -------------------------------------------------------------------------------- /public/img/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/pin.png -------------------------------------------------------------------------------- /public/img/tours/tour-1-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-1-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-1-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-1-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-1-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-1-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-1-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-1-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-2-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-2-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-2-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-2-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-2-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-2-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-2-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-2-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-3-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-3-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-3-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-3-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-3-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-3-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-3-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-3-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-4-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-4-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-4-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-4-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-4-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-4-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-4-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-4-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-5-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-5-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-5-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-5-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574797608434-cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574797608434-cover.jpeg -------------------------------------------------------------------------------- /public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798101834-cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798101834-cover.jpeg -------------------------------------------------------------------------------- /public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798102073-1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798102073-1.jpeg -------------------------------------------------------------------------------- /public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798102073-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798102073-2.jpeg -------------------------------------------------------------------------------- /public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798102073-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-5ddd6c9cbd0e4847d01c699c-1574798102073-3.jpeg -------------------------------------------------------------------------------- /public/img/tours/tour-6-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-6-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-6-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-6-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-6-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-6-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-6-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-6-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-7-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-7-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-7-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-7-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-7-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-7-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-7-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-7-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-8-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-8-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-8-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-8-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-8-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-8-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-8-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-8-cover.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-9-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-9-1.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-9-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-9-2.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-9-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-9-3.jpg -------------------------------------------------------------------------------- /public/img/tours/tour-9-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/tours/tour-9-cover.jpg -------------------------------------------------------------------------------- /public/img/users/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/default.jpg -------------------------------------------------------------------------------- /public/img/users/user-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-1.jpg -------------------------------------------------------------------------------- /public/img/users/user-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-10.jpg -------------------------------------------------------------------------------- /public/img/users/user-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-11.jpg -------------------------------------------------------------------------------- /public/img/users/user-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-12.jpg -------------------------------------------------------------------------------- /public/img/users/user-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-13.jpg -------------------------------------------------------------------------------- /public/img/users/user-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-14.jpg -------------------------------------------------------------------------------- /public/img/users/user-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-15.jpg -------------------------------------------------------------------------------- /public/img/users/user-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-16.jpg -------------------------------------------------------------------------------- /public/img/users/user-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-17.jpg -------------------------------------------------------------------------------- /public/img/users/user-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-18.jpg -------------------------------------------------------------------------------- /public/img/users/user-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-19.jpg -------------------------------------------------------------------------------- /public/img/users/user-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-2.jpg -------------------------------------------------------------------------------- /public/img/users/user-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-20.jpg -------------------------------------------------------------------------------- /public/img/users/user-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-3.jpg -------------------------------------------------------------------------------- /public/img/users/user-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-4.jpg -------------------------------------------------------------------------------- /public/img/users/user-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-5.jpg -------------------------------------------------------------------------------- /public/img/users/user-5c8a1f292f8fb814b56fa184-1574777070682.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-5c8a1f292f8fb814b56fa184-1574777070682.jpeg -------------------------------------------------------------------------------- /public/img/users/user-5c8a21f22f8fb814b56fa18a-1574778661963.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-5c8a21f22f8fb814b56fa18a-1574778661963.jpeg -------------------------------------------------------------------------------- /public/img/users/user-5c8a21f22f8fb814b56fa18a-1574788383024.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-5c8a21f22f8fb814b56fa18a-1574788383024.jpeg -------------------------------------------------------------------------------- /public/img/users/user-5ddd332c8b4c7c66947c7e47-1574777663812.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-5ddd332c8b4c7c66947c7e47-1574777663812.jpeg -------------------------------------------------------------------------------- /public/img/users/user-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-6.jpg -------------------------------------------------------------------------------- /public/img/users/user-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-7.jpg -------------------------------------------------------------------------------- /public/img/users/user-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-8.jpg -------------------------------------------------------------------------------- /public/img/users/user-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgope/Natours/424a06fb23bb4fd9e21cd0e2b2df0049491f7269/public/img/users/user-9.jpg -------------------------------------------------------------------------------- /public/js/alerts.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export const hideAlert = () => { 4 | const el = document.querySelector(".alert"); 5 | if (el) el.parentElement.removeChild(el); 6 | }; 7 | 8 | // type is 'success' or 'error' 9 | export const showAlert = (type, msg, time = 7) => { 10 | hideAlert(); 11 | const markup = `
${msg}
`; 12 | document.querySelector("body").insertAdjacentHTML("afterbegin", markup); 13 | window.setTimeout(hideAlert, time * 1000); 14 | }; 15 | -------------------------------------------------------------------------------- /public/js/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import "@babel/polyfill"; 3 | import { displayMap } from "./mapbox"; 4 | import { signup } from "./signup"; 5 | import { login, logout } from "./login"; 6 | import { updateSettings } from "./updateSettings"; 7 | import { bookTour } from "./stripe"; 8 | import { showAlert } from "./alerts"; 9 | 10 | // DOM ELEMENTS 11 | const mapBox = document.getElementById("map"); 12 | const signupForm = document.querySelector(".form--signup"); 13 | const loginForm = document.querySelector(".form--login"); 14 | const logOutBtn = document.querySelector(".nav__el--logout"); 15 | const userDataForm = document.querySelector(".form-user-data"); 16 | const userPasswordForm = document.querySelector(".form-user-password"); 17 | const bookBtn = document.getElementById("book-tour"); 18 | 19 | // DELEGATION 20 | if (mapBox) { 21 | const locations = JSON.parse(mapBox.dataset.locations); 22 | displayMap(locations); 23 | } 24 | 25 | if (signupForm) { 26 | signupForm.addEventListener("submit", e => { 27 | e.preventDefault(); 28 | 29 | const name = document.getElementById("name").value; 30 | const email = document.getElementById("email").value; 31 | const password = document.getElementById("password").value; 32 | const passwordConfirm = document.getElementById("passwordConfirm").value; 33 | 34 | signup(name, email, password, passwordConfirm); 35 | }); 36 | } 37 | 38 | if (loginForm) 39 | loginForm.addEventListener("submit", e => { 40 | e.preventDefault(); 41 | const email = document.getElementById("email").value; 42 | const password = document.getElementById("password").value; 43 | login(email, password); 44 | }); 45 | 46 | if (logOutBtn) logOutBtn.addEventListener("click", logout); 47 | 48 | if (userDataForm) 49 | userDataForm.addEventListener("submit", e => { 50 | e.preventDefault(); 51 | const form = new FormData(); 52 | form.append("name", document.getElementById("name").value); 53 | form.append("email", document.getElementById("email").value); 54 | form.append("photo", document.getElementById("photo").files[0]); 55 | 56 | updateSettings(form, "data"); 57 | }); 58 | 59 | if (userPasswordForm) 60 | userPasswordForm.addEventListener("submit", async e => { 61 | e.preventDefault(); 62 | document.querySelector(".btn--save-password").textContent = "Updating..."; 63 | 64 | const passwordCurrent = document.getElementById("password-current").value; 65 | const password = document.getElementById("password").value; 66 | const passwordConfirm = document.getElementById("password-confirm").value; 67 | await updateSettings({ passwordCurrent, password, passwordConfirm }, "password"); 68 | 69 | document.querySelector(".btn--save-password").textContent = "Save password"; 70 | document.getElementById("password-current").value = ""; 71 | document.getElementById("password").value = ""; 72 | document.getElementById("password-confirm").value = ""; 73 | }); 74 | 75 | if (bookBtn) 76 | bookBtn.addEventListener("click", e => { 77 | e.target.textContent = "Processing..."; 78 | const { tourId } = e.target.dataset; 79 | bookTour(tourId); 80 | }); 81 | 82 | const alertMessage = document.querySelector("body").dataset.alert; 83 | if (alertMessage) showAlert("success", alertMessage, 20); 84 | -------------------------------------------------------------------------------- /public/js/login.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import axios from "axios"; 3 | import { showAlert } from "./alerts"; 4 | export const login = async (email, password) => { 5 | try { 6 | const res = await axios({ 7 | method: "POST", 8 | url: "/api/v1/users/login", 9 | data: { 10 | email, 11 | password 12 | } 13 | }); 14 | 15 | if (res.data.status === "success") { 16 | showAlert("success", "Logged in successfully!"); 17 | window.setTimeout(() => { 18 | location.assign("/"); 19 | }, 1500); 20 | } 21 | } catch (err) { 22 | showAlert("error", err.response.data.message); 23 | } 24 | }; 25 | 26 | export const logout = async () => { 27 | try { 28 | const res = await axios({ 29 | method: "GET", 30 | url: "/api/v1/users/logout" 31 | }); 32 | if (res.data.status === "success") location.reload(true); 33 | } catch (err) { 34 | showAlert("error", "Error logging out! Try again."); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /public/js/mapbox.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import keys from "../../config/keys"; 3 | 4 | export const displayMap = locations => { 5 | mapboxgl.accessToken = keys.mapBoxAccessToken; 6 | var map = new mapboxgl.Map({ 7 | container: "map", 8 | style: "mapbox://styles/rasedmia/ck3cmgrsx1qa61cpiuonp0dja", 9 | scrollZoom: false 10 | // center: [-118.113491, 34.111745], 11 | // zoom: 10, 12 | // interactive: false 13 | }); 14 | 15 | const bounds = new mapboxgl.LngLatBounds(); 16 | 17 | locations.forEach(loc => { 18 | // Create marker 19 | const el = document.createElement("div"); 20 | el.className = "marker"; 21 | 22 | // Add marker 23 | new mapboxgl.Marker({ 24 | element: el, 25 | anchor: "bottom" 26 | }) 27 | .setLngLat(loc.coordinates) 28 | .addTo(map); 29 | 30 | // Add popup 31 | new mapboxgl.Popup({ 32 | offset: 30 33 | }) 34 | .setLngLat(loc.coordinates) 35 | .setHTML(`

Day ${loc.day}: ${loc.description}

`) 36 | .addTo(map); 37 | 38 | // Extend map bounds to include current location 39 | bounds.extend(loc.coordinates); 40 | }); 41 | 42 | map.fitBounds(bounds, { 43 | padding: { 44 | top: 200, 45 | bottom: 150, 46 | left: 100, 47 | right: 100 48 | } 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /public/js/signup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import axios from "axios"; 3 | import { showAlert } from "./alerts"; 4 | 5 | export const signup = async (name, email, password, passwordConfirm) => { 6 | try { 7 | const res = await axios({ 8 | method: "POST", 9 | url: "/api/v1/users/signup", 10 | data: { 11 | name, 12 | email, 13 | password, 14 | passwordConfirm 15 | } 16 | }); 17 | 18 | if (res.data.status === "success") { 19 | showAlert("success", "signed up in successfully!"); 20 | window.setTimeout(() => { 21 | location.assign("/"); 22 | }, 1000); 23 | } 24 | } catch (err) { 25 | showAlert("error", err.response.data.message); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /public/js/stripe.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import axios from "axios"; 4 | import { showAlert } from "./alerts"; 5 | import keys from "../../config/keys"; 6 | 7 | const stripe = Stripe(keys.stripeKey); 8 | 9 | export const bookTour = async tourId => { 10 | try { 11 | // 1) Get checkout session from API 12 | const session = await axios(`/api/v1/bookings/checkout-session/${tourId}`); 13 | // console.log(session); 14 | 15 | // 2) Create checkout form + charge credit card 16 | await stripe.redirectToCheckout({ 17 | sessionId: session.data.session.id 18 | }); 19 | } catch (err) { 20 | console.log(err); 21 | showAlert("error", err); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /public/js/updateSettings.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import axios from "axios"; 3 | import { showAlert } from "./alerts"; 4 | 5 | // type is either 'password' or 'data' 6 | export const updateSettings = async (data, type) => { 7 | try { 8 | const url = type === "password" ? "/api/v1/users/updateMyPassword" : "/api/v1/users/updateMe"; 9 | 10 | const res = await axios({ 11 | method: "PATCH", 12 | url, 13 | data 14 | }); 15 | 16 | if (res.data.status === "success") { 17 | showAlert("success", `${type.toUpperCase()} updated successfully!`); 18 | } 19 | } catch (err) { 20 | showAlert("error", err.response.data.message); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /public/overview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Natours | Exciting tours for adventurous people 14 | 15 | 16 | 17 |
18 | 29 | 32 | 42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 |
 
51 | Tour 1 52 |
53 | 54 |

55 | The Forest Hiker 56 |

57 |
58 | 59 |
60 |

Easy 5-day tour

61 |

62 | Breathtaking hike through the Canadian Banff National Park 63 |

64 |
65 | 66 | 67 | 68 | Banff, Canada 69 |
70 |
71 | 72 | 73 | 74 | April 2021 75 |
76 |
77 | 78 | 79 | 80 | 3 stops 81 |
82 |
83 | 84 | 85 | 86 | 25 people 87 |
88 |
89 | 90 | 101 |
102 | 103 |
104 |
105 |
106 |
 
107 | Tour 1 108 |
109 | 110 |

111 | The Sea Explorer 112 |

113 |
114 | 115 |
116 |

Medium-difficult 7-day tour

117 |

118 | Exploring the jaw-dropping US east coast by foot and by boat 119 |

120 |
121 | 122 | 123 | 124 | Oregon, US 125 |
126 |
127 | 128 | 129 | 130 | June 2021 131 |
132 |
133 | 134 | 135 | 136 | 4 stops 137 |
138 |
139 | 140 | 141 | 142 | 15 people 143 |
144 |
145 | 146 | 157 |
158 | 159 |
160 |
161 |
162 |
 
163 | Tour 1 164 |
165 | 166 |

167 | The Snow Adventurer 168 |

169 |
170 | 171 |
172 |

Difficult 3-day tour

173 |

174 | Exciting adventure in the snow with snowboarding and skiing 175 |

176 |
177 | 178 | 179 | 180 | Aspen, USA 181 |
182 |
183 | 184 | 185 | 186 | January 2022 187 |
188 |
189 | 190 | 191 | 192 | 2 stops 193 |
194 |
195 | 196 | 197 | 198 | 10 people 199 |
200 |
201 | 202 | 213 |
214 | 215 |
216 |
217 |
218 |
 
219 | Tour 1 220 |
221 | 222 |

223 | The City Wanderer 224 |

225 |
226 | 227 |
228 |

Easy 7-day tour

229 |

230 | Living the life of Wanderlust in the US' most beatiful cities 231 |

232 |
233 | 234 | 235 | 236 | NYC, USA 237 |
238 |
239 | 240 | 241 | 242 | March 2021 243 |
244 |
245 | 246 | 247 | 248 | 3 stops 249 |
250 |
251 | 252 | 253 | 254 | 20 people 255 |
256 |
257 | 258 | 269 |
270 | 271 |
272 |
273 |
274 |
 
275 | Tour 1 276 |
277 | 278 |

279 | The Park Camper 280 |

281 |
282 | 283 |
284 |

Medium-Difficult 10-day tour

285 |

286 | Breathing in Nature in America's most spectacular National Parks 287 |

288 |
289 | 290 | 291 | 292 | Las Vegas, USA 293 |
294 |
295 | 296 | 297 | 298 | August 2021 299 |
300 |
301 | 302 | 303 | 304 | 4 stops 305 |
306 |
307 | 308 | 309 | 310 | 15 people 311 |
312 |
313 | 314 | 325 |
326 | 327 |
328 |
329 |
330 |
 
331 | Tour 1 332 |
333 | 334 |

335 | The Sports Lover 336 |

337 |
338 | 339 |
340 |

Difficult 14-day tour

341 |

342 | Surfing, skating, parajumping, rock climbing and more, all in one 343 | tour 344 |

345 |
346 | 347 | 348 | 349 | California, USA 350 |
351 |
352 | 353 | 354 | 355 | July 2021 356 |
357 |
358 | 359 | 360 | 361 | 5 stops 362 |
363 |
364 | 365 | 366 | 367 | 8 people 368 |
369 |
370 | 371 | 382 |
383 |
384 |
385 | 386 | 401 | 402 | 403 | 404 | -------------------------------------------------------------------------------- /public/tour.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Natours | The Park Camper Tour 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 32 | 35 | 48 |
49 | 50 |
51 |
52 |

53 | The Park
54 | Camper Tour
55 |

56 |
57 |
58 | 59 | 60 | 61 | 10 days 62 |
63 |
64 | 65 | 66 | 67 | Las Vegas, USA 68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 |
76 |
77 |

Quick facts

78 |
79 | 80 | 81 | 82 | Next date 83 | August 2021 84 |
85 |
86 | 87 | 88 | 89 | Difficulty 90 | Medium 91 |
92 |
93 | 94 | 95 | 96 | Participants 97 | 10 people 98 |
99 |
100 | 101 | 102 | 103 | Rating 104 | 4.9 / 5 105 |
106 |
107 | 108 |
109 |

Your tour guides

110 | 111 |
112 | Lead guide 113 | Lead guide 114 | Steven Miller 115 |
116 |
117 | Tour guide 118 | Tour guide 119 | Lisa Brown 120 |
121 |
122 | Intern 123 | Intern 124 | Max Smith 125 |
126 |
127 |
128 |
129 | 130 |
131 |

About the park camper tour

132 |

133 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do 134 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 135 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut 136 | aliquip ex ea commodo consequat. Duis aute irure dolor in 137 | reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla 138 | pariatur. 139 |

140 |

141 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum 142 | dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 143 | proident, sunt in culpa qui officia deserunt mollit anim id est 144 | laborum! 145 |

146 |
147 |
148 | 149 |
150 |
151 | The Park Camper Tour 1 152 |
153 |
154 | The Park Camper Tour 1 155 |
156 |
157 | The Park Camper Tour 1 158 |
159 |
160 | 161 |
162 |
163 | 284 |
285 | 286 |
287 |
288 |
289 |
290 | Jim Brown 291 |
Jim Brown
292 |
293 |

294 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Cumque 295 | dignissimos sint quo commodi corrupti accusantium veniam saepe 296 | numquam. 297 |

298 |
299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 |
315 |
316 | 317 |
318 |
319 | Laura Wilson 320 |
Laura Wilson
321 |
322 |

323 | Veniam adipisci blanditiis, corporis sit magnam aperiam ad, fuga 324 | reiciendis provident deleniti cumque similique itaque animi, 325 | sapiente obcaecati beatae accusantium. 326 |

327 |
328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 |
344 |
345 | 346 |
347 |
348 | Ben Hadley 349 |
Ben Hadley
350 |
351 |

352 | Debitis, nesciunt itaque! At quis officia natus. Suscipit, 353 | reprehenderit blanditiis mollitia distinctio ducimus porro tempore 354 | perspiciatis sunt vel. 355 |

356 |
357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 |
373 |
374 | 375 |
376 |
377 | Alexander Jones 378 |
Alexander Jones
379 |
380 |

381 | Quaerat laborum eveniet ut aut maiores doloribus mollitia aperiam 382 | quis praesentium sed inventore harum aliquam veritatis at adipisci 383 | ea assumenda! 384 |

385 |
386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 |
402 |
403 | 404 |
405 |
406 | Ayla Cornell 407 |
Ayla Cornell
408 |
409 |

410 | Perferendis quo aliquid iste quas laboriosam molestias illo est 411 | voluptatem odit ea. Vero placeat culpa provident dicta maiores! 412 |

413 |
414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 |
430 |
431 |
432 |
433 | 434 |
435 |
436 | 439 | 440 | 441 | 442 |
443 |

What are you waiting for?

444 |

445 | 10 days. 1 adventure. Infinite memories. Make it yours today! 446 |

447 | 448 |
449 |
450 |
451 | 452 | 467 | 468 | 469 | 470 | -------------------------------------------------------------------------------- /routes/bookingRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as bookingController from "./../controllers/bookingController.js"; 3 | import * as authController from "./../controllers/authController.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.use(authController.protect); 8 | 9 | router.get("/checkout-session/:tourId", bookingController.getCheckoutSession); 10 | 11 | router.use(authController.restrictTo("admin", "lead-guide")); 12 | 13 | router 14 | .route("/") 15 | .get(bookingController.getAllBookings) 16 | .post(bookingController.createBooking); 17 | 18 | // user booking routes 19 | router 20 | .route("/:id") 21 | .get(bookingController.getBooking) 22 | .patch(bookingController.updateBooking) 23 | .delete(bookingController.deleteBooking); 24 | 25 | export default router; 26 | -------------------------------------------------------------------------------- /routes/reviewRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as reviewController from "./../controllers/reviewController.js"; 3 | import * as authController from "./../controllers/authController.js"; 4 | 5 | const router = express.Router({ mergeParams: true }); 6 | 7 | // POST /tour/32434fs35/reviews 8 | // GET /tour/32434fs35/reviews 9 | // POST /reviews 10 | 11 | router.use(authController.protect); 12 | 13 | // user review routes 14 | router 15 | .route("/") 16 | .get(reviewController.getAllReviews) 17 | .post( 18 | authController.restrictTo("user"), 19 | reviewController.setTourUserIds, 20 | reviewController.createReview 21 | ); 22 | 23 | router 24 | .route("/:id") 25 | .get(reviewController.getlReview) 26 | .patch(authController.restrictTo("user", "admin"), reviewController.updateReview) 27 | .delete(authController.restrictTo("user", "admin"), reviewController.deleteReview); 28 | 29 | export default router; 30 | -------------------------------------------------------------------------------- /routes/tourRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as tourController from "./../controllers/tourController.js"; 3 | import { protect, restrictTo } from "./../controllers/authController.js"; 4 | import reviewRouter from "./../routes/reviewRoutes.js"; 5 | 6 | const router = express.Router(); 7 | 8 | // POST /tour/32434fs35/reviews 9 | // GET /tour/32434fs35/reviews 10 | // GET /tour/32434fs35/reviews/97987dssad8 11 | 12 | // router.route('/:tourId/reviews').post(protect, restrictTo('user'), reviewController.createReview); 13 | 14 | router.use("/:tourId/reviews", reviewRouter); 15 | 16 | // router.param('id', checkID); 17 | 18 | // Create a checkBody middleware 19 | // Check if body contains the name and price property 20 | // If not, send back 400 (bad requst) 21 | // Add it to the post handler stack 22 | 23 | router.route("/top-5-cheap").get(tourController.aliasTopTours, tourController.getAllTours); 24 | 25 | router.route("/tour-stats").get(tourController.getTourStats); 26 | router 27 | .route("/monthly-plan/:year") 28 | .get(protect, restrictTo("admin", "lead-guide", "guide"), tourController.getMonthlyPlan); 29 | 30 | router 31 | .route("/tours-within/:distance/center/:latlng/unit/:unit") 32 | .get(tourController.getToursWithin); 33 | // /tours-within?distance=233¢er=-40,45&unit=mi 34 | // /tours-within/233/center/-40,45/unit/mi 35 | 36 | router.route("/distances/:latlng/unit/:unit").get(tourController.getDistances); 37 | 38 | router 39 | .route("/") 40 | .get(tourController.getAllTours) 41 | .post(protect, restrictTo("admin", "lead-guide"), tourController.createTour); 42 | 43 | router 44 | .route("/:id") 45 | .get(tourController.getTour) 46 | .patch( 47 | protect, 48 | restrictTo("admin", "lead-guide"), 49 | tourController.uploadTourImages, 50 | tourController.resizeTourImages, 51 | tourController.updateTour 52 | ) 53 | .delete(protect, restrictTo("admin", "lead-guide"), tourController.deleteTour); 54 | 55 | export default router; 56 | -------------------------------------------------------------------------------- /routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as authController from "./../controllers/authController.js"; 3 | import * as userController from "./../controllers/userController.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.post("/signup", authController.signup); 8 | router.post("/login", authController.login); 9 | router.get("/logout", authController.logout); 10 | 11 | router.post("/forgotPassword", authController.forgotPassword); 12 | router.patch("/resetPassword/:token", authController.resetPassword); 13 | 14 | // Protect all routes after this middleware 15 | router.use(authController.protect); 16 | 17 | router.patch("/updateMyPassword", authController.updatePassword); 18 | router.get("/me", userController.getMe, userController.getUser); 19 | router.patch( 20 | "/updateMe", 21 | userController.uploadUserPhoto, 22 | userController.resizeUserPhoto, 23 | userController.updateMe 24 | ); 25 | router.delete("/deleteMe", userController.deleteMe); 26 | 27 | router.use(authController.restrictTo("admin")); 28 | 29 | router 30 | .route("/") 31 | .get(userController.getAllUsers) 32 | .post(userController.createUser); 33 | 34 | router 35 | .route("/:id") 36 | .get(userController.getUser) 37 | .patch(userController.updateUser) 38 | .delete(userController.deleteUser); 39 | 40 | export default router; 41 | -------------------------------------------------------------------------------- /routes/viewRoutes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as viewsController from "../controllers/viewsController.js"; 3 | import * as authController from "../controllers/authController.js"; 4 | 5 | const router = express.Router(); 6 | 7 | router.use(viewsController.alerts); 8 | 9 | router.get("/", authController.isLoggedIn, viewsController.getOverview); 10 | 11 | router.get("/tour/:slug", authController.isLoggedIn, viewsController.getTour); 12 | router.get("/signup", authController.isLoggedIn, viewsController.getSingupForm); 13 | router.get("/login", authController.isLoggedIn, viewsController.getLoginForm); 14 | router.get("/me", authController.protect, viewsController.getAccount); 15 | 16 | router.get("/my-tours", authController.protect, viewsController.getMyTours); 17 | 18 | router.post("/submit-user-data", authController.protect, viewsController.updateUserData); 19 | 20 | export default router; 21 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import chalk from "chalk"; 3 | 4 | import dotenv from "dotenv"; 5 | 6 | dotenv.config({ path: "./config.env" }); 7 | import app from "./app.js"; 8 | 9 | process.on("uncaughtException", err => { 10 | console.log("UNCAUGHT EXCEPTION! 💥 Shutting down..."); 11 | console.log(err.name, err.message); 12 | process.exit(1); 13 | }); 14 | 15 | const DB = process.env.DATABASE.replace("", process.env.DATABASE_PASSWORD); 16 | 17 | mongoose 18 | .connect(DB, { 19 | useUnifiedTopology: true, 20 | useNewUrlParser: true, 21 | useCreateIndex: true, 22 | useFindAndModify: false 23 | }) 24 | .then(() => console.log("DB connection successful!")) 25 | .catch(err => console.log(chalk.redBright(err))); 26 | 27 | const port = process.env.PORT || 3000; 28 | const server = app.listen(port, () => { 29 | console.log(`App running on port ${chalk.greenBright(port)}...`); 30 | }); 31 | 32 | process.on("unhandledRejection", err => { 33 | console.log("UNHANDLED REJECTION! 💥 Shutting down..."); 34 | console.log(err.name, err.message); 35 | server.close(() => { 36 | process.exit(1); 37 | }); 38 | }); 39 | 40 | process.on("SIGTERM", () => { 41 | console.log("👋 SIGTERM RECEIVED. Shutting down gracefully"); 42 | server.close(() => { 43 | console.log("💥 Process terminated!"); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /utils/apiFeatures.js: -------------------------------------------------------------------------------- 1 | class APIFeatures { 2 | constructor(query, queryString) { 3 | this.query = query; 4 | this.queryString = queryString; 5 | } 6 | 7 | filter() { 8 | const queryObj = { ...this.queryString }; 9 | const excludedFields = ["page", "sort", "limit", "fields"]; 10 | excludedFields.forEach(el => delete queryObj[el]); 11 | 12 | // 1B) Advanced filtering 13 | let queryStr = JSON.stringify(queryObj); 14 | queryStr = queryStr.replace(/\b(gte|gt|lte|lt)\b/g, match => `$${match}`); 15 | 16 | this.query = this.query.find(JSON.parse(queryStr)); 17 | 18 | return this; 19 | } 20 | 21 | sort() { 22 | if (this.queryString.sort) { 23 | const sortBy = this.queryString.sort.split(",").join(" "); 24 | this.query = this.query.sort(sortBy); 25 | } else { 26 | this.query = this.query.sort("-createdAt"); 27 | } 28 | 29 | return this; 30 | } 31 | 32 | limitFields() { 33 | if (this.queryString.fields) { 34 | const fields = this.queryString.fields.split(",").join(" "); 35 | this.query = this.query.select(fields); 36 | } else { 37 | this.query = this.query.select("-__v"); 38 | } 39 | 40 | return this; 41 | } 42 | 43 | paginate() { 44 | const page = this.queryString.page * 1 || 1; 45 | const limit = this.queryString.limit * 1 || 100; 46 | const skip = (page - 1) * limit; 47 | 48 | this.query = this.query.skip(skip).limit(limit); 49 | 50 | return this; 51 | } 52 | } 53 | 54 | export default APIFeatures; 55 | -------------------------------------------------------------------------------- /utils/appError.js: -------------------------------------------------------------------------------- 1 | class AppError extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | 5 | this.statusCode = statusCode; 6 | this.status = `${statusCode}`.startsWith("4") ? "fail" : "error"; 7 | this.isOperational = true; 8 | 9 | Error.captureStackTrace(this, this.constructor); 10 | } 11 | } 12 | 13 | export default AppError; 14 | -------------------------------------------------------------------------------- /utils/catchAsync.js: -------------------------------------------------------------------------------- 1 | export default function catchAsync(fn) { 2 | return (req, res, next) => { 3 | fn(req, res, next).catch(next); 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /utils/email.js: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer"; 2 | import pug from "pug"; 3 | import HtmlToText from "html-to-text"; 4 | 5 | const { htmlToText } = HtmlToText; 6 | 7 | // For create email obj to send actual emails. 8 | export default class Email { 9 | constructor(user, url) { 10 | this.to = user.email; 11 | this.firstName = user.name.split(" ")[0]; 12 | this.url = url; 13 | this.from = `Lakshman Gope <${process.env.EMAIL_FROM}>`; 14 | } 15 | 16 | // Create different transports for different environments 17 | newTransport() { 18 | if (process.env.NODE_ENV === "production") { 19 | // Sendgrid 20 | return nodemailer.createTransport({ 21 | service: "SendGrid", 22 | auth: { 23 | user: process.env.SENDGRID_USERNAME, 24 | pass: process.env.SENDGRID_PASSWORD 25 | } 26 | }); 27 | } 28 | 29 | return nodemailer.createTransport({ 30 | host: process.env.EMAIL_HOST, 31 | port: process.env.EMAIL_PORT, 32 | auth: { 33 | user: process.env.EMAIL_USERNAME, 34 | pass: process.env.EMAIL_PASSWORD 35 | } 36 | }); 37 | } 38 | 39 | // Send the actual email 40 | async send(template, subject) { 41 | // 1) Render HTML based on a pug template 42 | const html = pug.renderFile(`${__dirname}/../views/email/${template}.pug`, { 43 | firstName: this.firstName, 44 | url: this.url, 45 | subject 46 | }); 47 | 48 | // 2) Define email options 49 | const mailOptions = { 50 | from: this.from, 51 | to: this.to, 52 | subject, 53 | html, 54 | text: htmlToText.fromString(html) 55 | }; 56 | 57 | // 3) Create a transport and send email 58 | await this.newTransport().sendMail(mailOptions); 59 | } 60 | 61 | async sendWelcome() { 62 | await this.send("welcome", "Welcome to the Natours Family!"); 63 | } 64 | 65 | async sendPasswordReset() { 66 | await this.send("passwordReset", "Your password reset token (valid for only 10 minutes)"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /views/_footer.pug: -------------------------------------------------------------------------------- 1 | footer.footer 2 | .footer__logo 3 | img(src='/img/logo-green.png' alt='Natour logo') 4 | ul.footer__nav 5 | li: a(href='#') About us 6 | li: a(href='#') Download apps 7 | li: a(href='#') Become a guide 8 | li: a(href='#') Careers 9 | li: a(href='#') Contact 10 | p.footer__copyright © by Lakshman Gope. 11 | -------------------------------------------------------------------------------- /views/_header.pug: -------------------------------------------------------------------------------- 1 | header.header 2 | nav.nav.nav--tours 3 | a.nav__el(href='/') All tours 4 | .header__logo 5 | img(src='/img/logo-white.png' alt='Natours logo') 6 | nav.nav.nav--user 7 | if user 8 | a.nav__el.nav__el--logout Log out 9 | a.nav__el(href='/me') 10 | img.nav__user-img(src=`/img/users/${user.photo}` alt=`Photo of ${user.name}`) 11 | span= user.name.split(' ')[0] 12 | else 13 | a.nav__el(href='/login') Log in 14 | a.nav__el.nav__el--cta(href='/signup') Sign up 15 | -------------------------------------------------------------------------------- /views/_reviewCard.pug: -------------------------------------------------------------------------------- 1 | mixin reviewCard(review) 2 | .reviews__card 3 | .reviews__avatar 4 | img.reviews__avatar-img(src=`/img/users/${review.user.photo}`, alt=`${review.user.name}`) 5 | 6 | h6.reviews__user= review.user.name 7 | p.reviews__text= review.review 8 | .reviews__rating 9 | each star in [1, 2, 3, 4, 5] 10 | svg.reviews__star(class=`reviews__star--${review.rating >= star ? 'active' : 'inactive'}`) 11 | use(xlink:href='/img/icons.svg#icon-star') 12 | -------------------------------------------------------------------------------- /views/account.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | mixin navItem(link, text, icon, active) 4 | li(class=`${active ? 'side-nav--active' : ''}`) 5 | a(href=`${link}`) 6 | svg 7 | use(xlink:href=`img/icons.svg#icon-${icon}`) 8 | | #{text} 9 | 10 | block content 11 | main.main 12 | .user-view 13 | nav.user-view__menu 14 | ul.side-nav 15 | +navItem('#', 'Settings', 'settings', true) 16 | +navItem('/my-tours', 'My bookings', 'briefcase') 17 | +navItem('#', 'My reviews', 'star') 18 | +navItem('#', 'Billing', 'credit-card') 19 | 20 | - if (user.role === 'admin') 21 | .admin-nav 22 | h5.admin-nav__heading Admin 23 | ul.side-nav 24 | +navItem('#', 'Manage tours', 'map') 25 | +navItem('#', 'Manage users', 'users') 26 | +navItem('#', 'Manage reviews', 'star') 27 | +navItem('#', 'Manage bookings', 'briefcase') 28 | 29 | .user-view__content 30 | .user-view__form-container 31 | h2.heading-secondary.ma-bt-md Your account settings 32 | 33 | //- WITHOUT API 34 | //- form.form.form-user-data(action='/submit-user-data' method='POST' enctype='multipart/form-data') 35 | 36 | //- WITH API 37 | form.form.form-user-data 38 | .form__group 39 | label.form__label(for='name') Name 40 | input#name.form__input(type='text', value=`${user.name}`, required, name='name') 41 | .form__group.ma-bt-md 42 | label.form__label(for='email') Email address 43 | input#email.form__input(type='email', value=`${user.email}`, required, name='email') 44 | .form__group.form__photo-upload 45 | img.form__user-photo(src=`/img/users/${user.photo}`, alt='User photo') 46 | input.form__upload(type='file', accept='image/*', id='photo', name='photo') 47 | label(for='photo') Choose new photo 48 | 49 | .form__group.right 50 | button.btn.btn--small.btn--green Save settings 51 | 52 | .line   53 | 54 | .user-view__form-container 55 | h2.heading-secondary.ma-bt-md Password change 56 | form.form.form-user-password 57 | .form__group 58 | label.form__label(for='password-current') Current password 59 | input#password-current.form__input(type='password', placeholder='••••••••', required, minlength='8') 60 | .form__group 61 | label.form__label(for='password') New password 62 | input#password.form__input(type='password', placeholder='••••••••', required, minlength='8') 63 | .form__group.ma-bt-lg 64 | label.form__label(for='password-confirm') Confirm password 65 | input#password-confirm.form__input(type='password', placeholder='••••••••', required, minlength='8') 66 | .form__group.right 67 | button.btn.btn--small.btn--green.btn--save-password Save password 68 | -------------------------------------------------------------------------------- /views/base.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | meta(charset='UTF-8') 6 | meta(name='viewport' content='width=device-width, initial-scale=1.0') 7 | link(rel='stylesheet' href='/css/style.css') 8 | link(rel='shortcut icon' type='image/png' href='https://github.com/lgope/Natours/blob/master/public/img/logo-green-round.png?raw=true') 9 | link(rel='stylesheet' href='https://fonts.googleapis.com/css?family=Lato:300,300i,700') 10 | title Natours | #{title} 11 | 12 | body(data-alert=`${alert ? alert : ''}`) 13 | 14 | // HEADER 15 | include _header 16 | 17 | // CONTENT 18 | block content 19 | h1 This is a placeholder heading 20 | 21 | // FOOTER 22 | include _footer 23 | 24 | 25 | script(src='https://js.stripe.com/v3/') 26 | script(src='/js/bundle.js') 27 | -------------------------------------------------------------------------------- /views/email/_style.pug: -------------------------------------------------------------------------------- 1 | style. 2 | img { 3 | border: none; 4 | -ms-interpolation-mode: bicubic; 5 | max-width: 100%; 6 | } 7 | body { 8 | background-color: #f6f6f6; 9 | font-family: sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | font-size: 14px; 12 | line-height: 1.4; 13 | margin: 0; 14 | padding: 0; 15 | -ms-text-size-adjust: 100%; 16 | -webkit-text-size-adjust: 100%; 17 | } 18 | 19 | table { 20 | border-collapse: separate; 21 | mso-table-lspace: 0pt; 22 | mso-table-rspace: 0pt; 23 | width: 100%; } 24 | table td { 25 | font-family: sans-serif; 26 | font-size: 14px; 27 | vertical-align: top; 28 | } 29 | .body { 30 | background-color: #f6f6f6; 31 | width: 100%; 32 | } 33 | .container { 34 | display: block; 35 | margin: 0 auto !important; 36 | /* makes it centered */ 37 | max-width: 580px; 38 | padding: 10px; 39 | width: 580px; 40 | } 41 | .content { 42 | box-sizing: border-box; 43 | display: block; 44 | margin: 0 auto; 45 | max-width: 580px; 46 | padding: 10px; 47 | } 48 | .main { 49 | background: #ffffff; 50 | border-radius: 3px; 51 | width: 100%; 52 | } 53 | .wrapper { 54 | box-sizing: border-box; 55 | padding: 20px; 56 | } 57 | .content-block { 58 | padding-bottom: 10px; 59 | padding-top: 10px; 60 | } 61 | .footer { 62 | clear: both; 63 | margin-top: 10px; 64 | text-align: center; 65 | width: 100%; 66 | } 67 | .footer td, 68 | .footer p, 69 | .footer span, 70 | .footer a { 71 | color: #999999; 72 | font-size: 12px; 73 | text-align: center; 74 | } 75 | h1, 76 | h2, 77 | h3, 78 | h4 { 79 | color: #000000; 80 | font-family: sans-serif; 81 | font-weight: 400; 82 | line-height: 1.4; 83 | margin: 0; 84 | margin-bottom: 30px; 85 | } 86 | h1 { 87 | font-size: 35px; 88 | font-weight: 300; 89 | text-align: center; 90 | text-transform: capitalize; 91 | } 92 | p, 93 | ul, 94 | ol { 95 | font-family: sans-serif; 96 | font-size: 14px; 97 | font-weight: normal; 98 | margin: 0; 99 | margin-bottom: 15px; 100 | } 101 | p li, 102 | ul li, 103 | ol li { 104 | list-style-position: inside; 105 | margin-left: 5px; 106 | } 107 | a { 108 | color: #55c57a; 109 | text-decoration: underline; 110 | } 111 | .btn { 112 | box-sizing: border-box; 113 | width: 100%; } 114 | .btn > tbody > tr > td { 115 | padding-bottom: 15px; } 116 | .btn table { 117 | width: auto; 118 | } 119 | .btn table td { 120 | background-color: #ffffff; 121 | border-radius: 5px; 122 | text-align: center; 123 | } 124 | .btn a { 125 | background-color: #ffffff; 126 | border: solid 1px #55c57a; 127 | border-radius: 5px; 128 | box-sizing: border-box; 129 | color: #55c57a; 130 | cursor: pointer; 131 | display: inline-block; 132 | font-size: 14px; 133 | font-weight: bold; 134 | margin: 0; 135 | padding: 12px 25px; 136 | text-decoration: none; 137 | text-transform: capitalize; 138 | } 139 | .btn-primary table td { 140 | background-color: #55c57a; 141 | } 142 | .btn-primary a { 143 | background-color: #55c57a; 144 | border-color: #55c57a; 145 | color: #ffffff; 146 | } 147 | .last { 148 | margin-bottom: 0; 149 | } 150 | .first { 151 | margin-top: 0; 152 | } 153 | .align-center { 154 | text-align: center; 155 | } 156 | .align-right { 157 | text-align: right; 158 | } 159 | .align-left { 160 | text-align: left; 161 | } 162 | .clear { 163 | clear: both; 164 | } 165 | .mt0 { 166 | margin-top: 0; 167 | } 168 | .mb0 { 169 | margin-bottom: 0; 170 | } 171 | .preheader { 172 | color: transparent; 173 | display: none; 174 | height: 0; 175 | max-height: 0; 176 | max-width: 0; 177 | opacity: 0; 178 | overflow: hidden; 179 | mso-hide: all; 180 | visibility: hidden; 181 | width: 0; 182 | } 183 | .powered-by a { 184 | text-decoration: none; 185 | } 186 | hr { 187 | border: 0; 188 | border-bottom: 1px solid #f6f6f6; 189 | margin: 20px 0; 190 | } 191 | @media only screen and (max-width: 620px) { 192 | table[class=body] h1 { 193 | font-size: 28px !important; 194 | margin-bottom: 10px !important; 195 | } 196 | table[class=body] p, 197 | table[class=body] ul, 198 | table[class=body] ol, 199 | table[class=body] td, 200 | table[class=body] span, 201 | table[class=body] a { 202 | font-size: 16px !important; 203 | } 204 | table[class=body] .wrapper, 205 | table[class=body] .article { 206 | padding: 10px !important; 207 | } 208 | table[class=body] .content { 209 | padding: 0 !important; 210 | } 211 | table[class=body] .container { 212 | padding: 0 !important; 213 | width: 100% !important; 214 | } 215 | table[class=body] .main { 216 | border-left-width: 0 !important; 217 | border-radius: 0 !important; 218 | border-right-width: 0 !important; 219 | } 220 | table[class=body] .btn table { 221 | width: 100% !important; 222 | } 223 | table[class=body] .btn a { 224 | width: 100% !important; 225 | } 226 | table[class=body] .img-responsive { 227 | height: auto !important; 228 | max-width: 100% !important; 229 | width: auto !important; 230 | } 231 | } 232 | @media all { 233 | .ExternalClass { 234 | width: 100%; 235 | } 236 | .ExternalClass, 237 | .ExternalClass p, 238 | .ExternalClass span, 239 | .ExternalClass font, 240 | .ExternalClass td, 241 | .ExternalClass div { 242 | line-height: 100%; 243 | } 244 | .apple-link a { 245 | color: inherit !important; 246 | font-family: inherit !important; 247 | font-size: inherit !important; 248 | font-weight: inherit !important; 249 | line-height: inherit !important; 250 | text-decoration: none !important; 251 | } 252 | .btn-primary table td:hover { 253 | background-color: #2e864b !important; 254 | } 255 | .btn-primary a:hover { 256 | background-color: #2e864b !important; 257 | border-color: #2e864b !important; 258 | } 259 | } -------------------------------------------------------------------------------- /views/email/baseEmail.pug: -------------------------------------------------------------------------------- 1 | //- Email template adapted from https://github.com/leemunroe/responsive-html-email-template 2 | //- Converted from HTML using https://html2pug.now.sh/ 3 | 4 | doctype html 5 | html 6 | head 7 | meta(name='viewport', content='width=device-width') 8 | meta(http-equiv='Content-Type', content='text/html; charset=UTF-8') 9 | title= subject 10 | 11 | include _style 12 | body 13 | table.body(role='presentation', border='0', cellpadding='0', cellspacing='0') 14 | tbody 15 | tr 16 | td 17 | td.container 18 | .content 19 | // START CENTERED WHITE CONTAINER 20 | table.main(role='presentation') 21 | 22 | // START MAIN AREA 23 | tbody 24 | tr 25 | td.wrapper 26 | table(role='presentation', border='0', cellpadding='0', cellspacing='0') 27 | tbody 28 | tr 29 | td 30 | // CONTENT 31 | block content 32 | 33 | // START FOOTER 34 | .footer 35 | table(role='presentation', border='0', cellpadding='0', cellspacing='0') 36 | tbody 37 | tr 38 | td.content-block 39 | span.apple-link Natours Inc, 123 Nowhere Road, San Francisco CA 99999 40 | br 41 | | Don't like these emails? 42 | a(href='#') Unsubscribe 43 | //- td -------------------------------------------------------------------------------- /views/email/passwordReset.pug: -------------------------------------------------------------------------------- 1 | extends baseEmail 2 | 3 | block content 4 | p Hi #{firstName}, 5 | p Forgot your password? Submit a PATCH request with your new password and passwordConfirm to: #{url}. 6 | p (Website for this action not yet implememnted.) 7 | table.btn.btn-primary(role='presentation', border='0', cellpadding='0', cellspacing='0') 8 | tbody 9 | tr 10 | td(align='left') 11 | table(role='presentation', border='0', cellpadding='0', cellspacing='0') 12 | tbody 13 | tr 14 | td 15 | a(href=`${url}`, target='_blank') Reset your password 16 | 17 | p If you didn't forget your password, please ignore this email! 18 | -------------------------------------------------------------------------------- /views/email/welcome.pug: -------------------------------------------------------------------------------- 1 | extends baseEmail 2 | 3 | block content 4 | p Hi #{firstName}, 5 | p Welcome to Natours, we're glad to have you 🎉🙏 6 | p We're all a big familiy here, so make sure to upload your user photo so we get to know you a bit better! 7 | table.btn.btn-primary(role='presentation', border='0', cellpadding='0', cellspacing='0') 8 | tbody 9 | tr 10 | td(align='left') 11 | table(role='presentation', border='0', cellpadding='0', cellspacing='0') 12 | tbody 13 | tr 14 | td 15 | a(href=`${url}`, target='_blank') Upload user photo 16 | p If you need any help with booking your next tour, please don't hesitate to contact me! 17 | p - Lakshman Gope, CEO 18 | -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | block content 4 | main.main 5 | .error 6 | .error__title 7 | h2.heading-secondary.heading-secondary--error Uh oh! Something went wrong! 8 | 9 | h2.error__emoji 😢 🤯 10 | .error__msg= msg 11 | -------------------------------------------------------------------------------- /views/login.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | 4 | block content 5 | main.main 6 | .login-form 7 | h2.heading-secondary.ma-bt-lg Log into your account 8 | form.form.form--login 9 | .form__group 10 | label.form__label(for='email') Email address 11 | input#email.form__input(type='email', placeholder='you@example.com', required) 12 | .form__group.ma-bt-md 13 | label.form__label(for='password') Password 14 | input#password.form__input(type='password', placeholder='••••••••', required, minlength='8') 15 | .form__group 16 | button.btn.btn--green Login 17 | -------------------------------------------------------------------------------- /views/nullbooking.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | block content 4 | main.main 5 | .error 6 | .error__title 7 | h2.heading-secondary.heading-secondary--error=headLine 8 | h2.error__emoji 🙁 9 | .error__msg=msg 10 | -------------------------------------------------------------------------------- /views/overview.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | block content 4 | main.main 5 | .card-container 6 | 7 | each tour in tours 8 | .card 9 | .card__header 10 | .card__picture 11 | .card__picture-overlay   12 | img.card__picture-img(src=`/img/tours/${tour.imageCover}`, alt=`${tour.name}`) 13 | h3.heading-tertirary 14 | span= tour.name 15 | 16 | .card__details 17 | h4.card__sub-heading= `${tour.difficulty} ${tour.duration}-day tour` 18 | p.card__text= tour.summary 19 | .card__data 20 | svg.card__icon 21 | use(xlink:href='/img/icons.svg#icon-map-pin') 22 | span= tour.startLocation.description 23 | .card__data 24 | svg.card__icon 25 | use(xlink:href='/img/icons.svg#icon-calendar') 26 | span= tour.startDates[0].toLocaleString('en-us', {month: 'long', year: 'numeric'}) 27 | .card__data 28 | svg.card__icon 29 | use(xlink:href='/img/icons.svg#icon-flag') 30 | span= `${tour.locations.length} stops` 31 | .card__data 32 | svg.card__icon 33 | use(xlink:href='/img/icons.svg#icon-user') 34 | span= `${tour.maxGroupSize} people` 35 | 36 | .card__footer 37 | p 38 | span.card__footer-value= `$${tour.price}` 39 | | 40 | span.card__footer-text per person 41 | p.card__ratings 42 | span.card__footer-value= tour.ratingsAverage 43 | | 44 | span.card__footer-text= `rating (${tour.ratingsQuantity})` 45 | a.btn.btn--green.btn--small(href=`/tour/${tour.slug}`) Details 46 | -------------------------------------------------------------------------------- /views/signup.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | block content 4 | main.main 5 | .singup-form 6 | h2.heading-secondary.ma-bt-lg create your account! 7 | form.form.form--signup 8 | .form__group 9 | label.form__label(for='name') Your name 10 | input#name.form__input(type='text' placeholder='' required='') 11 | .form__group 12 | label.form__label(for='email') Email address 13 | input#email.form__input(type='email' placeholder='you@example.com' required='') 14 | .form__group.ma-bt-md 15 | label.form__label(for='password') Password 16 | input#password.form__input(type='password' placeholder='••••••••' required='' minlength='8') 17 | .form__group.ma-bt-md 18 | label.form__label(for='passwordConfirm') Confirm password 19 | input#passwordConfirm.form__input(type='password' placeholder='••••••••' required='' minlength='8') 20 | .form__group 21 | button.btn.btn--green Sign up 22 | -------------------------------------------------------------------------------- /views/tour.pug: -------------------------------------------------------------------------------- 1 | extends base 2 | include _reviewCard 3 | 4 | block append head 5 | 6 | 7 | 8 | mixin overviewBox(label, text, icon) 9 | .overview-box__detail 10 | svg.overview-box__icon 11 | use(xlink:href=`/img/icons.svg#icon-${icon}`) 12 | span.overview-box__label= label 13 | span.overview-box__text= text 14 | 15 | block content 16 | section.section-header 17 | .header__hero 18 | .header__hero-overlay   19 | img.header__hero-img(src=`/img/tours/${tour.imageCover}`, alt=`${tour.name}`) 20 | 21 | .heading-box 22 | h1.heading-primary 23 | span= `${tour.name} tour` 24 | .heading-box__group 25 | .heading-box__detail 26 | svg.heading-box__icon 27 | use(xlink:href='/img/icons.svg#icon-clock') 28 | span.heading-box__text= `${tour.duration} days` 29 | .heading-box__detail 30 | svg.heading-box__icon 31 | use(xlink:href='/img/icons.svg#icon-map-pin') 32 | span.heading-box__text= tour.startLocation.description 33 | 34 | section.section-description 35 | .overview-box 36 | div 37 | .overview-box__group 38 | h2.heading-secondary.ma-bt-lg Quick facts 39 | 40 | - const date = tour.startDates[0].toLocaleString('en-us', {month: 'long', year: 'numeric'}) 41 | +overviewBox('Next date', date, 'calendar') 42 | +overviewBox('Difficulty', tour.difficulty, 'trending-up') 43 | +overviewBox('Participants', `${tour.maxGroupSize} people`, 'user') 44 | +overviewBox('Rating', `${tour.ratingsAverage} / 5`, 'star') 45 | 46 | .overview-box__group 47 | h2.heading-secondary.ma-bt-lg Your tour guides 48 | 49 | each guide in tour.guides 50 | .overview-box__detail 51 | img.overview-box__img(src=`/img/users/${guide.photo}`, alt=`${guide.name}`) 52 | 53 | - if (guide.role === 'lead-guide') 54 | span.overview-box__label Lead guide 55 | - if (guide.role === 'guide') 56 | span.overview-box__label Tour guide 57 | span.overview-box__text= guide.name 58 | 59 | .description-box 60 | h2.heading-secondary.ma-bt-lg= `About ${tour.name} tour` 61 | - const parapraphs = tour.description.split('\n'); 62 | each p in parapraphs 63 | p.description__text= p 64 | 65 | section.section-pictures 66 | each img, i in tour.images 67 | .picture-box 68 | img.picture-box__img(src=`/img/tours/${img}`, alt=`The Park Camper Tour ${i + 1}`, class=`picture-box__img--${i + 1}`) 69 | 70 | section.section-map 71 | #map(data-locations=`${JSON.stringify(tour.locations)}`) 72 | 73 | section.section-reviews 74 | .reviews 75 | each review in tour.reviews 76 | +reviewCard(review) 77 | 78 | section.section-cta 79 | .cta 80 | .cta__img.cta__img--logo 81 | img(src='/img/logo-white.png', alt='Natours logo') 82 | img.cta__img.cta__img--1(src=`/img/tours/${tour.images[1]}`, alt='Tour picture') 83 | img.cta__img.cta__img--2(src=`/img/tours/${tour.images[2]}`, alt='Tour picture') 84 | .cta__content 85 | h2.heading-secondary What are you waiting for? 86 | p.cta__text= `${tour.duration} days. 1 adventure. Infinite memories. Make it yours today!` 87 | 88 | if user 89 | button.btn.btn--green.span-all-rows#book-tour(data-tour-id=`${tour.id}`) Book tour now! 90 | else 91 | a.btn.btn--green.span-all-rows(href='/login') Log in to book tour 92 | --------------------------------------------------------------------------------