├── .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 |
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 | 
66 |
67 | #### Tour Details :
68 |
69 | 
70 |
71 | #### Payment Process :
72 |
73 | 
74 |
75 | #### Booked Tours :
76 |
77 | 
78 |
79 | #### User Profile :
80 |
81 | 
82 |
83 | #### Admin Profile :
84 |
85 | 
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 | 
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 |
43 |
44 |
45 |
46 |
47 |
48 |
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 |
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 |
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 |
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 |
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 |
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 |
49 |
50 |
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 |
113 |
Lead guide
114 |
Steven Miller
115 |
116 |
117 |
118 |
Tour guide
119 |
Lisa Brown
120 |
121 |
122 |
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 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
285 |
286 |
287 |
288 |
289 |
290 |
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 |
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 |
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 |
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 |
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 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
What are you waiting for?
444 |
445 | 10 days. 1 adventure. Infinite memories. Make it yours today!
446 |
447 |
Book tour now!
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 |
--------------------------------------------------------------------------------