Initial commit 🚀

This commit is contained in:
Jonas Schmedtmann
2019-06-13 15:43:15 +01:00
commit 7f81af0ddf
1052 changed files with 2123177 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
const crypto = require('crypto');
const { promisify } = require('util');
const jwt = require('jsonwebtoken');
const User = require('./../models/userModel');
const catchAsync = require('./../utils/catchAsync');
const AppError = require('./../utils/appError');
const sendEmail = require('./../utils/email');
const signToken = id => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
});
};
const createSendToken = (user, statusCode, res) => {
const token = signToken(user._id);
const cookieOptions = {
expires: new Date(
Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000
),
httpOnly: true
};
if (process.env.NODE_ENV === 'production') cookieOptions.secure = true;
res.cookie('jwt', token, cookieOptions);
// Remove password from output
user.password = undefined;
res.status(statusCode).json({
status: 'success',
token,
data: {
user
}
});
};
exports.signup = catchAsync(async (req, res, next) => {
const newUser = await User.create(req.body);
createSendToken(newUser, 201, res);
});
exports.login = catchAsync(async (req, res, next) => {
const { email, password } = req.body;
// 1) Check if email and password exist
if (!email || !password) {
return next(new AppError('Please provide email and password!', 400));
}
// 2) Check if user exists && password is correct
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.correctPassword(password, user.password))) {
return next(new AppError('Incorrect email or password', 401));
}
// 3) If everything ok, send token to client
createSendToken(user, 200, res);
});
exports.logout = (req, res) => {
res.cookie('jwt', 'loggedout', {
expires: new Date(Date.now() + 10 * 1000),
httpOnly: true
});
res.status(200).json({ status: 'success' });
};
exports.protect = catchAsync(async (req, res, next) => {
// 1) Getting token and check of it's there
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
token = req.headers.authorization.split(' ')[1];
} else if (req.cookies.jwt) {
token = req.cookies.jwt;
}
if (!token) {
return next(
new AppError('You are not logged in! Please log in to get access.', 401)
);
}
// 2) Verification token
const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);
// 3) Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return next(
new AppError(
'The user belonging to this token does no longer exist.',
401
)
);
}
// 4) Check if user changed password after the token was issued
if (currentUser.changedPasswordAfter(decoded.iat)) {
return next(
new AppError('User recently changed password! Please log in again.', 401)
);
}
// GRANT ACCESS TO PROTECTED ROUTE
req.user = currentUser;
res.locals.user = currentUser;
next();
});
// Only for rendered pages, no errors!
exports.isLoggedIn = async (req, res, next) => {
if (req.cookies.jwt) {
try {
// 1) verify token
const decoded = await promisify(jwt.verify)(
req.cookies.jwt,
process.env.JWT_SECRET
);
// 2) Check if user still exists
const currentUser = await User.findById(decoded.id);
if (!currentUser) {
return next();
}
// 3) Check if user changed password after the token was issued
if (currentUser.changedPasswordAfter(decoded.iat)) {
return next();
}
// THERE IS A LOGGED IN USER
res.locals.user = currentUser;
return next();
} catch (err) {
return next();
}
}
next();
};
exports.restrictTo = (...roles) => {
return (req, res, next) => {
// roles ['admin', 'lead-guide']. role='user'
if (!roles.includes(req.user.role)) {
return next(
new AppError('You do not have permission to perform this action', 403)
);
}
next();
};
};
exports.forgotPassword = catchAsync(async (req, res, next) => {
// 1) Get user based on POSTed email
const user = await User.findOne({ email: req.body.email });
if (!user) {
return next(new AppError('There is no user with email address.', 404));
}
// 2) Generate the random reset token
const resetToken = user.createPasswordResetToken();
await user.save({ validateBeforeSave: false });
// 3) Send it to user's email
const resetURL = `${req.protocol}://${req.get(
'host'
)}/api/v1/users/resetPassword/${resetToken}`;
const message = `Forgot your password? Submit a PATCH request with your new password and passwordConfirm to: ${resetURL}.\nIf you didn't forget your password, please ignore this email!`;
try {
await sendEmail({
email: user.email,
subject: 'Your password reset token (valid for 10 min)',
message
});
res.status(200).json({
status: 'success',
message: 'Token sent to email!'
});
} catch (err) {
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save({ validateBeforeSave: false });
return next(
new AppError('There was an error sending the email. Try again later!'),
500
);
}
});
exports.resetPassword = catchAsync(async (req, res, next) => {
// 1) Get user based on the token
const hashedToken = crypto
.createHash('sha256')
.update(req.params.token)
.digest('hex');
const user = await User.findOne({
passwordResetToken: hashedToken,
passwordResetExpires: { $gt: Date.now() }
});
// 2) If token has not expired, and there is user, set the new password
if (!user) {
return next(new AppError('Token is invalid or has expired', 400));
}
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save();
// 3) Update changedPasswordAt property for the user
// 4) Log the user in, send JWT
createSendToken(user, 200, res);
});
exports.updatePassword = catchAsync(async (req, res, next) => {
// 1) Get user from collection
const user = await User.findById(req.user.id).select('+password');
// 2) Check if POSTed current password is correct
if (!(await user.correctPassword(req.body.passwordCurrent, user.password))) {
return next(new AppError('Your current password is wrong.', 401));
}
// 3) If so, update password
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
await user.save();
// User.findByIdAndUpdate will NOT work as intended!
// 4) Log user in, send JWT
createSendToken(user, 200, res);
});

View File

@@ -0,0 +1,108 @@
const AppError = require('./../utils/appError');
const handleCastErrorDB = err => {
const message = `Invalid ${err.path}: ${err.value}.`;
return new AppError(message, 400);
};
const handleDuplicateFieldsDB = err => {
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
console.log(value);
const message = `Duplicate field value: ${value}. Please use another value!`;
return new AppError(message, 400);
};
const handleValidationErrorDB = err => {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
};
const handleJWTError = () =>
new AppError('Invalid token. Please log in again!', 401);
const handleJWTExpiredError = () =>
new AppError('Your token has expired! Please log in again.', 401);
const sendErrorDev = (err, req, res) => {
// A) API
if (req.originalUrl.startsWith('/api')) {
return res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
}
// B) RENDERED WEBSITE
console.error('ERROR 💥', err);
return res.status(err.statusCode).render('error', {
title: 'Something went wrong!',
msg: err.message
});
};
const sendErrorProd = (err, req, res) => {
// A) API
if (req.originalUrl.startsWith('/api')) {
// A) Operational, trusted error: send message to client
if (err.isOperational) {
return res.status(err.statusCode).json({
status: err.status,
message: err.message
});
}
// B) Programming or other unknown error: don't leak error details
// 1) Log error
console.error('ERROR 💥', err);
// 2) Send generic message
return res.status(500).json({
status: 'error',
message: 'Something went very wrong!'
});
}
// B) RENDERED WEBSITE
// A) Operational, trusted error: send message to client
if (err.isOperational) {
console.log(err);
return res.status(err.statusCode).render('error', {
title: 'Something went wrong!',
msg: err.message
});
}
// B) Programming or other unknown error: don't leak error details
// 1) Log error
console.error('ERROR 💥', err);
// 2) Send generic message
return res.status(err.statusCode).render('error', {
title: 'Something went wrong!',
msg: 'Please try again later.'
});
};
module.exports = (err, req, res, next) => {
// console.log(err.stack);
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, req, res);
} else if (process.env.NODE_ENV === 'production') {
let error = { ...err };
error.message = err.message;
if (error.name === 'CastError') error = handleCastErrorDB(error);
if (error.code === 11000) error = handleDuplicateFieldsDB(error);
if (error.name === 'ValidationError')
error = handleValidationErrorDB(error);
if (error.name === 'JsonWebTokenError') error = handleJWTError();
if (error.name === 'TokenExpiredError') error = handleJWTExpiredError();
sendErrorProd(error, req, res);
}
};

View File

@@ -0,0 +1,90 @@
const catchAsync = require('./../utils/catchAsync');
const AppError = require('./../utils/appError');
const APIFeatures = require('./../utils/apiFeatures');
exports.deleteOne = Model =>
catchAsync(async (req, res, next) => {
const doc = await Model.findByIdAndDelete(req.params.id);
if (!doc) {
return next(new AppError('No document found with that ID', 404));
}
res.status(204).json({
status: 'success',
data: null
});
});
exports.updateOne = Model =>
catchAsync(async (req, res, next) => {
const doc = await Model.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
if (!doc) {
return next(new AppError('No document found with that ID', 404));
}
res.status(200).json({
status: 'success',
data: {
data: doc
}
});
});
exports.createOne = Model =>
catchAsync(async (req, res, next) => {
const doc = await Model.create(req.body);
res.status(201).json({
status: 'success',
data: {
data: doc
}
});
});
exports.getOne = (Model, popOptions) =>
catchAsync(async (req, res, next) => {
let query = Model.findById(req.params.id);
if (popOptions) query = query.populate(popOptions);
const doc = await query;
if (!doc) {
return next(new AppError('No document found with that ID', 404));
}
res.status(200).json({
status: 'success',
data: {
data: doc
}
});
});
exports.getAll = Model =>
catchAsync(async (req, res, next) => {
// To allow for nested GET reviews on tour (hack)
let filter = {};
if (req.params.tourId) filter = { tour: req.params.tourId };
const features = new APIFeatures(Model.find(filter), req.query)
.filter()
.sort()
.limitFields()
.paginate();
// const doc = await features.query.explain();
const doc = await features.query;
// SEND RESPONSE
res.status(200).json({
status: 'success',
results: doc.length,
data: {
data: doc
}
});
});

View File

@@ -0,0 +1,16 @@
const Review = require('./../models/reviewModel');
const factory = require('./handlerFactory');
// const catchAsync = require('./../utils/catchAsync');
exports.setTourUserIds = (req, res, next) => {
// Allow nested routes
if (!req.body.tour) req.body.tour = req.params.tourId;
if (!req.body.user) req.body.user = req.user.id;
next();
};
exports.getAllReviews = factory.getAll(Review);
exports.getReview = factory.getOne(Review);
exports.createReview = factory.createOne(Review);
exports.updateReview = factory.updateOne(Review);
exports.deleteReview = factory.deleteOne(Review);

View File

@@ -0,0 +1,167 @@
const Tour = require('./../models/tourModel');
const catchAsync = require('./../utils/catchAsync');
const factory = require('./handlerFactory');
const AppError = require('./../utils/appError');
exports.aliasTopTours = (req, res, next) => {
req.query.limit = '5';
req.query.sort = '-ratingsAverage,price';
req.query.fields = 'name,price,ratingsAverage,summary,difficulty';
next();
};
exports.getAllTours = factory.getAll(Tour);
exports.getTour = factory.getOne(Tour, { path: 'reviews' });
exports.createTour = factory.createOne(Tour);
exports.updateTour = factory.updateOne(Tour);
exports.deleteTour = factory.deleteOne(Tour);
exports.getTourStats = catchAsync(async (req, res, next) => {
const stats = await Tour.aggregate([
{
$match: { ratingsAverage: { $gte: 4.5 } }
},
{
$group: {
_id: { $toUpper: '$difficulty' },
numTours: { $sum: 1 },
numRatings: { $sum: '$ratingsQuantity' },
avgRating: { $avg: '$ratingsAverage' },
avgPrice: { $avg: '$price' },
minPrice: { $min: '$price' },
maxPrice: { $max: '$price' }
}
},
{
$sort: { avgPrice: 1 }
}
// {
// $match: { _id: { $ne: 'EASY' } }
// }
]);
res.status(200).json({
status: 'success',
data: {
stats
}
});
});
exports.getMonthlyPlan = catchAsync(async (req, res, next) => {
const year = req.params.year * 1; // 2021
const plan = await Tour.aggregate([
{
$unwind: '$startDates'
},
{
$match: {
startDates: {
$gte: new Date(`${year}-01-01`),
$lte: new Date(`${year}-12-31`)
}
}
},
{
$group: {
_id: { $month: '$startDates' },
numTourStarts: { $sum: 1 },
tours: { $push: '$name' }
}
},
{
$addFields: { month: '$_id' }
},
{
$project: {
_id: 0
}
},
{
$sort: { numTourStarts: -1 }
},
{
$limit: 12
}
]);
res.status(200).json({
status: 'success',
data: {
plan
}
});
});
// /tours-within/:distance/center/:latlng/unit/:unit
// /tours-within/233/center/34.111745,-118.113491/unit/mi
exports.getToursWithin = catchAsync(async (req, res, next) => {
const { distance, latlng, unit } = req.params;
const [lat, lng] = latlng.split(',');
const radius = unit === 'mi' ? distance / 3963.2 : distance / 6378.1;
if (!lat || !lng) {
next(
new AppError(
'Please provide latitutr and longitude in the format lat,lng.',
400
)
);
}
const tours = await Tour.find({
startLocation: { $geoWithin: { $centerSphere: [[lng, lat], radius] } }
});
res.status(200).json({
status: 'success',
results: tours.length,
data: {
data: tours
}
});
});
exports.getDistances = catchAsync(async (req, res, next) => {
const { latlng, unit } = req.params;
const [lat, lng] = latlng.split(',');
const multiplier = unit === 'mi' ? 0.000621371 : 0.001;
if (!lat || !lng) {
next(
new AppError(
'Please provide latitutr and longitude in the format lat,lng.',
400
)
);
}
const distances = await Tour.aggregate([
{
$geoNear: {
near: {
type: 'Point',
coordinates: [lng * 1, lat * 1]
},
distanceField: 'distance',
distanceMultiplier: multiplier
}
},
{
$project: {
distance: 1,
name: 1
}
}
]);
res.status(200).json({
status: 'success',
data: {
data: distances
}
});
});

View File

@@ -0,0 +1,68 @@
const User = require('./../models/userModel');
const catchAsync = require('./../utils/catchAsync');
const AppError = require('./../utils/appError');
const factory = require('./handlerFactory');
const filterObj = (obj, ...allowedFields) => {
const newObj = {};
Object.keys(obj).forEach(el => {
if (allowedFields.includes(el)) newObj[el] = obj[el];
});
return newObj;
};
exports.getMe = (req, res, next) => {
req.params.id = req.user.id;
next();
};
exports.updateMe = catchAsync(async (req, res, next) => {
// 1) Create error if user POSTs password data
if (req.body.password || req.body.passwordConfirm) {
return next(
new AppError(
'This route is not for password updates. Please use /updateMyPassword.',
400
)
);
}
// 2) Filtered out unwanted fields names that are not allowed to be updated
const filteredBody = filterObj(req.body, 'name', 'email');
// 3) Update user document
const updatedUser = await User.findByIdAndUpdate(req.user.id, filteredBody, {
new: true,
runValidators: true
});
res.status(200).json({
status: 'success',
data: {
user: updatedUser
}
});
});
exports.deleteMe = catchAsync(async (req, res, next) => {
await User.findByIdAndUpdate(req.user.id, { active: false });
res.status(204).json({
status: 'success',
data: null
});
});
exports.createUser = (req, res) => {
res.status(500).json({
status: 'error',
message: 'This route is not defined! Please use /signup instead'
});
};
exports.getUser = factory.getOne(User);
exports.getAllUsers = factory.getAll(User);
// Do NOT update passwords with this!
exports.updateUser = factory.updateOne(User);
exports.deleteUser = factory.deleteOne(User);

View File

@@ -0,0 +1,66 @@
const Tour = require('../models/tourModel');
const User = require('../models/userModel');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/appError');
exports.getOverview = catchAsync(async (req, res, next) => {
// 1) Get tour data from collection
const tours = await Tour.find();
// 2) Build template
// 3) Render that template using tour data from 1)
res.status(200).render('overview', {
title: 'All Tours',
tours
});
});
exports.getTour = catchAsync(async (req, res, next) => {
// 1) Get the data, for the requested tour (including reviews and guides)
const tour = await Tour.findOne({ slug: req.params.slug }).populate({
path: 'reviews',
fields: 'review rating user'
});
if (!tour) {
return next(new AppError('There is no tour with that name.', 404));
}
// 2) Build template
// 3) Render template using data from 1)
res.status(200).render('tour', {
title: `${tour.name} Tour`,
tour
});
});
exports.getLoginForm = (req, res) => {
res.status(200).render('login', {
title: 'Log into your account'
});
};
exports.getAccount = (req, res) => {
res.status(200).render('account', {
title: 'Your account'
});
};
exports.updateUserData = catchAsync(async (req, res, next) => {
const updatedUser = await User.findByIdAndUpdate(
req.user.id,
{
name: req.body.name,
email: req.body.email
},
{
new: true,
runValidators: true
}
);
res.status(200).render('account', {
title: 'Your account',
user: updatedUser
});
});