Master Node by building a real-world RESTful API and web app (with authentication, Node.js security, payments & more)
Section | Topic | Problem Sets |
---|---|---|
Section 0 | RestfulAPI | code |
Section 1 | Mongodb & mongoose | code |
Section 2 | Error handling | code |
Section 3 | Authentication_and_security | code |
Section 4 | Modelling_data_and_advanced_mongoose | code |
Section 5 | Server_Side_Rendering-with_Pug_Templates | code |
Section 6 | Advanced_Features_Payments_Email_File_Uploads | code |
Section 7 | Setting_Up_Git_and_Deployment | code |
-
Install Project Dependencies:
- Run
npm install
in the project's root directory to download and install all the required dependencies. - You can use the
--dev
flag withnpm install
to install development dependencies.
- Run
-
Configure Environment Variables and Configuration Files:
- If your project requires environment variables or configuration files, follow the project documentation to configure them.
- Create a
.env
file and fill in the necessary variables or modify relevant sections in the project's configuration files.
-
Start the Project:
- In the project's root directory, run
npm start
to start the project. - This will execute the command specified in the
"start"
script in thepackage.json
file.
- In the project's root directory, run
-
package.json
:- The
package.json
file contains metadata about your project and its dependencies. - It includes scripts that can be executed using
npm run <script-name>
. - The
"start"
script is used to start the project usingnodemon
to automatically restart the server on file changes. - The
"start:prod"
script starts the project in production mode. - The
"debug"
script can be used for debugging withndb
.
- The
-
config.env
:- The
config.env
file contains environment variables used in the project. - It defines variables like
NODE_ENV
,PORT
, andDATABASE
. - The
DATABASE_PASSWORD
variable is specific to your MongoDB connection.
- The
-
server.js
:- The
server.js
file is the entry point of your application. - It imports necessary modules like
mongoose
anddotenv
. - It sets up a connection to the MongoDB database using the
mongoose.connect()
method. - The
dotenv.config()
method loads environment variables from theconfig.env
file. - The
app
module is imported from theapp.js
file. - The server listens on the specified
port
and logs a success message. - Error handling is implemented for unhandled exceptions and rejections.
- The
echo ".cache/" >> .gitignore
rm -rf node_modules
rm -rf .cache
npm install deasync@latest --save-dev
These steps and code snippets provide a basic structure for starting a Node.js project with dependencies, environment variables, and server setup.
config.env
NODE_ENV=development
PORT=3000
DATABASE=mongodb+srv://itsyuimorii:<PASSWORD>@cluster0.r7nrqwu.mongodb.net/Yumekobo?retryWrites=true&w=majority
DATABASE_PASSWORD=lX0f8LolPcIEQvOk
server.js
const mongoose = require('mongoose');
const dotenv = require('dotenv');
process.on('uncaughtException', err => {
console.log('UNCAUGHT EXCEPTION! π₯ Shutting down...');
console.log(err.name, err.message);
process.exit(1);
});
dotenv.config({ path: './config.env' });
const app = require('./app');
const DB = process.env.DATABASE.replace(
'<PASSWORD>',
process.env.DATABASE_PASSWORD
);
mongoose
.connect(DB, {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
useUnifiedTopology: true
})
.then(() => console.log('DB connection successful!'));
const port = process.env.PORT || 3000;
const server = app.listen(port, () => {
console.log(`App running on port ${port}...`);
});
process.on('unhandledRejection', err => {
console.log('UNHANDLED REJECTION! π₯ Shutting down...');
console.log(err.name, err.message);
server.close(() => {
process.exit(1);
});
});
In your package.json
file, you can configure the scripts
section to define custom commands for running your server. Here's an example of how you can configure the start
and start:prod
scripts:
"scripts": {
"start": "nodemon server.js",
"start:prod": "NODE_ENV=production nodemon server.js"
}
"start:prod": "NODE_ENV=production nodemon server.js" This is a customised command to start the application in production environment mode. The command NODE_ENV=production sets the environment variable NODE_ENV to "production" so that specific settings or configurations can be made in the application based on this variable. Next, use the nodemon package to monitor and automatically restart the server.js file. Such a command is typically used to run an application in a production environment.
"start": "nodemon server.js" This is another custom command to start the application in default mode. It simply uses the nodemon package to monitor and automatically restart the server.js file. Commands like this are typically used to run an application in a development environment.
Using the npm start command will run the command specified by the start attribute, i.e. "nodemon server.js", and use the nodemon package to run the server.js file in the development environment.
Use the npm start start:prod command to run the command specified by the start:prod attribute, "NODE_ENV=production nodemon server.js", and use the nodemon package to run the server.js file in the production environment with the environment variable NODE_ENV set to "production". and set the environment variable NODE_ENV to "production".
With the above configuration, you can run the server using the following commands:
Start server in development mode
npm start
This command will use nodemon
to run the server.js
file, allowing automatic server restarts whenever changes are made to the code during development.
Start server in production mode
npm run start:prod
mongoose
npm i mongoose --legacy-peer-deps
npm run watch:js
This command will set the NODE_ENV
environment variable to "production"
and then use nodemon
to run the server.js
file. Running the server in production mode may involve additional optimizations and configurations specific to your application.
Note that when using npm run
to execute a script, you need to prefix the script name with run
.
Now, you can start your server by using either npm start
or npm run start:prod
, depending on the desired mode.
With the above configuration, you can run the server using the following commands:
In the root directory of your project, run the following command to install the necessary dependencies:
npm install eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-config-airbnb eslint-plugin-node eslint-plugin-jsx-a11y eslint-plugin-import eslint-plugin-react --save-dev
- Visit the MongoDB Atlas website: https://www.mongodb.com/cloud/atlas.
- Create an account or log in to your existing account.
- In the Atlas dashboard, click on "Create New Cluster".
- Choose your preferred cloud provider (e.g., AWS, Azure, or Google Cloud Platform).
- Select the region (geographic location) where you want your data to be stored.
- Configure cluster options, including cluster name, cluster size, and storage capacity.
- Advanced options such as authentication, virtual private cloud (VPC), network connectivity, etc., can be configured even if you're using the free M0 cluster.
- Click on "Create Cluster".
- On the cluster overview page, click on "Connect".
- Choose "Connect your application".
- Copy the connection string from the "Connection String Only" tab.
APPLICATION LOGIC (controller)
- π Code that is only concerned about the applicationβs implementation, not the underlying business problem weβre trying to solve (e.g. showing and selling tours);
- π Concerned about managing requests and responses;
- π About the appβ s more technical aspects;
- π Bridge between model and view layers.
BUSINESS LOGIC(model)
- π Code that actually solve sthe business problem we set
out to solve;
-
π Directly related to business rules, how the business works,and business needs;
-
π Examples:
-
π Creating new tours in the database;
-
π Checking if userβs password is correct;
-
π Validating user input data;
-
π Ensuring only users who bought a tour can review it.
-
Fat models/thin controllers: offload as much logic as possible into the models, and keep the controllers as simple and lean as possible.
Mongoose(https://mongoosejs.com/docs/guide.html)
Mongoose query middleware(https://mongoosejs.com/docs/middleware.html#types-of-middleware)
This code sets the toJSON
and toObject
options on the tourSchema
object, enabling it to include virtual properties when converting to a JSON string or a regular JavaScript object.
Here are the comments for this code:
const tourSchema = new mongoose.Schema(
{
// Schema definitions...
},
{
toJSON: { virtuals: true }, // Include virtual properties when converting to JSON string
toObject: { virtuals: true } // Include virtual properties when converting to regular JavaScript object
}
);
This code is typically used within the Mongoose schema definition. By setting the virtuals
property of the toJSON
and toObject
options to true
, it instructs Mongoose to include the defined virtual properties when converting the document to a JSON string or a regular JavaScript object.
Virtual properties are properties that are not stored in the database but are computed or derived based on existing properties. They can be used to provide additional data or calculated property values without the need to explicitly store them in the database.
With the toJSON
and toObject
options set, when you convert the document to a JSON string using JSON.stringify()
or to a regular JavaScript object using the toObject()
method, the virtual properties will be included in the result.
node dev-data/data/import-dev-data.js --import
node dev-data/data/import-dev-data.js --delete
import-dev-data.js
const fs = require('fs');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const Tour = require('./../../models/tourModels');
dotenv.config({ path: './config.env' });
const DB = process.env.DATABASE.replace(
'<PASSWORD>',
process.env.DATABASE_PASSWORD
);
const connectDB = async () => {
try {
await mongoose.connect(DB, {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
useUnifiedTopology: true
});
console.log('DB connection successful!');
} catch (err) {
console.error('DB connection failed:', err);
process.exit(1);
}
};
// READ JSON FILE
const tours = JSON.parse(
fs.readFileSync(`${__dirname}/tours-simple.json`, 'utf-8')
);
// IMPORT DATA INTO DB
const importDevData = async () => {
try {
await connectDB();
await Tour.create(tours);
console.log('Data successfully loaded!');
} catch (err) {
console.error('Error loading data:', err);
}
process.exit();
};
// DELETE ALL DATA FROM DB
const deleteDevData = async () => {
try {
await connectDB();
await Tour.deleteMany();
console.log('Data successfully deleted!');
} catch (err) {
console.error('Error deleting data:', err);
}
process.exit();
};
if (process.argv[2] === '--import') {
importDevData();
} else if (process.argv[2] === '--delete') {
deleteDevData();
}
class APIFeatures {
constructor(query, queryString) {
this.query = query; // Initialize the query property with the query parameter
this.queryString = queryString; // Initialize the queryString property with the queryString parameter
}
filter() {
const queryObj = { ...this.queryString }; // Create a copy of the queryString object
const excludedFields = ['page', 'sort', 'limit', 'fields']; // Specify the fields to be excluded from the query
// Remove the excluded fields from the queryObj
excludedFields.forEach(el => delete queryObj[el]);
// Advanced filtering
let queryStr = JSON.stringify(queryObj); // Convert the queryObj to a string
queryStr = queryStr.replace(/\b(gte|gt|lte|lt)\b/g, match => `$${match}`); // Replace certain operators with MongoDB operators
this.query = this.query.find(JSON.parse(queryStr)); // Update the query with the filtered results
return this;
}
sort() {
if (this.queryString.sort) {
const sortBy = this.queryString.sort.split(',').join(' '); // Extract the sort criteria from the queryString and format it
this.query = this.query.sort(sortBy); // Sort the query based on the sortBy criteria
} else {
this.query = this.query.sort('-createdAt'); // If no sort criteria provided, sort by 'createdAt' field in descending order
}
return this;
}
limitFields() {
if (this.queryString.fields) {
const fields = this.queryString.fields.split(',').join(' '); // Extract the fields to be included from the queryString and format it
this.query = this.query.select(fields); // Select only the specified fields in the query
} else {
this.query = this.query.select('-__v'); // Exclude the '__v' field from the query result
}
return this;
}
paginate() {
const page = this.queryString.page * 1 || 1; // Extract the page number from the queryString, convert to number, default to 1 if not provided
const limit = this.queryString.limit * 1 || 100; // Extract the limit from the queryString, convert to number, default to 100 if not provided
const skip = (page - 1) * limit; // Calculate the number of documents to skip based on the page and limit
this.query = this.query.skip(skip).limit(limit); // Skip the specified number of documents and limit the result to the specified number
return this;
}
}
module.exports = APIFeatures; // Export the APIFeatures class for external use
- QUERY MIDDLEWARE
- MODEL MIDDLEWARE
- DOCUMENT MIDDLEWARE
- AGGREGATION MIDDLEWARE
npm i ndb --save-dev
"scripts": {
"start": "nodemon server.js",
"start:prod": "NODE_ENV=production nodemon server.js",
"debug": "ndb npm start"
}
- operational errors Problems that we can predict will happen at some point, so we just need to handle them in advance.
π Invalid path accessed; π Invalid user input (validator error from mongoose); π Failed to connect to server; π Failed to connect to database; π Request timeout;
- programming errors Bugs that we developers introduce into our code. Difficult to find and handle.
π Reading properties on undefined; π Passing a number where an object is expected; π Using await without async; π Using req.query instead of req.body;
reference: jsonwebtoken ο½ jwt.io(https://jwt.io/)
install
$ npm install jsonwebtoken
usage
jwt.sign(payload, secretOrPrivateKey, [options, callback])
πThe input parameter id of the function is a unique identifier for the user (e.g. user ID) and is used as the content in the payload of the JWT.
//--------------**GENERATE TOKEN**----------------
const signToken = id => {
//payload, secret, options
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
});
};
Inside the function, the jwt.sign method is called to generate the JWT, and it accepts three parameters:
payload: this is an object { id } containing the user's ID, which serves as the JWT's payload. secret: this is the key used to sign the JWT, process.env.JWT_SECRET. The key should be a secure random string to ensure that only the server with the correct key can authenticate and decode the JWT. options: this is an object containing options to set the attributes of the JWT, such as the expiration time (expiresIn). process.env.JWT_EXPIRES_IN should be a string indicating the expiration time, which can be a time interval (e.g., '2d' for 2 days) or a specific date/time. Finally, the signToken function returns the generated JWT and assigns it to the variable token in the following code, which may be used after a successful user authentication or registration to generate and provide the JWT to the user for further authorisation or authentication.
πThis code defines a function called createSendToken that creates and sends a JWT (JSON Web Token) to the user.
//--------------**CREATE TOKEN & SEND TOKEN**----------------
const createSendToken = (user, statusCode, res) => {
//create token
const token = signToken(user._id);
res.status(statusCode).json({
status: 'success',
token,
data: {
user
}
});
};
The input parameters of the function are as follows:
user: user object containing information about the user. statusCode: HTTP status code, used to set the status code of the response. res: the response object, used to send the response to the client. Inside the function, it first calls the signToken function and passes in the user's _id to generate the JWT, then uses the res.status() method to set the status code of the response, which is usually the value of the statusCode parameter.
Next, it uses the res.json() method to send a JSON-formatted response to the client. The response contains the following:
status: a string indicating the status of the operation, here it is set to 'success'. token: the generated JWT, the value returned by the signToken function. data: an object containing the user's data. Here, the user parameter is passed into the data attribute. The purpose of this function is to generate a JWT after user authentication or registration, and respond to the client with the user data. The client can save the JWT and use it in future requests to authenticate and obtain authorisation.
Testing: generate token
post: 127.0.0.1:3000/api/v1/users/signup
body JSON
{
"name": "test3",
"email":"test3@yuimorii.com",
"password":"11111111",
"passwordConfirm":"11111111"
}
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0YjBiNTljZjkyZTg0ZWY4MmFmN2YxYSIsImlhdCI6MTY4OTMwMjQyOCwiZXhwIjoxNjk3MDc4NDI4fQ.nFDpacNCRnhyRtBhjA4oqJprDd6yQQrLwwI7eVdgiE4",
"data": {
"user": {
"role": "user",
"active": true,
"_id": "64b0b59cf92e84ef82af7f1a",
"name": "test3",
"email": "test3@yuimorii.com",
"password": "$2a$12$PpQtl4bBQBEYkKj03aUGJu3VpO8mxh1L9Y1c0q7vbV7Lkfa6/R10C",
"__v": 0
}
}
}
Dev:yumekobo -> http://127.0.0.1:3000/
Prod:yumekobo -> ??
eg. {{URL}}api/v1/tours
According to the provided code, it is a method to set an environment variable in the Postman testing tool using a pre-request script.
pm.environment.set('jwt', pm.response.json().token);
The purpose of this code is to extract a value named "token" from the response of a request and set it as the value of an environment variable in Postman named "jwt".
Let's break down each part:
pm.environment.set
: This is one of the built-in functions in the Postman script, used to set the value of an environment variable."jwt"
: This is the name of the environment variable that is being set, the variable name that will be assigned the value.pm.response.json()
: This is a combination of Postman built-in objects and methods used to access the response of a request and parse it as JSON format..token
: Assuming the response is a JSON object, this code retrieves the value of the property named "token" from the JSON object using the.token
syntax.
Therefore, the purpose of this code is to extract the value of "token" from the request response and set it as the value of the "jwt" environment variable in Postman. This allows you to reference this environment variable in subsequent requests or scripts.
npm i nodemailer
- create inbox
- copy SMTP settings
- paste in .env file
EMAIL_USERNAME= fde41408f46031
EMAIL_PASSWORD=4bf6283683be45
EMAIL_HOST=sandbox.smtp.mailtrap.io
EMAIL_PORT=25
/utils/email.js
/**
* @description: This file is used to send emails to the user
*/
const nodemailer = require('nodemailer');
//----------------**SEND EMAIL**----------------
const sendEmail = async options => {
// 1) Create a transporter
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD
}
});
// 2) Define the email options
const mailOptions = {
from: 'yui morii <user@yuimorii.com>',
to: options.email,
subject: options.subject,
text: options.message
};
// 3) Actually send the email
await transporter.sendMail(mailOptions);
};
module.exports = sendEmail;
controllers/authController.js
//--------------**FORGOT PASSWORD**----------------
/**
* Creates a password reset token and sends it to the user's email.
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {function} next - The next middleware function.
* @returns {Object} - The response object.
* @throws {AppError} - If there is no user with the email address.
* @throws {AppError} - If there is an error sending the email.
*/
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
);
}
});
routes/userRoutes.js
router.post('/forgotPassword', authController.forgotPassword);
router.patch('/resetPassword/:token', authController.resetPassword);
models/userModel.js
// models/userModel.js
const crypto = require('crypto');
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please tell us your name!']
},
email: {
type: String,
required: [true, 'Please provide your email'],
unique: true,
lowercase: true,
validate: [validator.isEmail, 'Please provide a valid email']
},
photo: String,
role: {
type: String,
enum: ['user', 'guide', 'lead-guide', 'admin'],
default: 'user'
},
password: {
type: String,
required: [true, 'Please provide a password'],
minlength: 8,
select: false
},
passwordConfirm: {
type: String,
required: [true, 'Please confirm your password'],
validate: {
// This only works on CREATE and SAVE!!!
validator: function(el) {
return el === this.password;
},
message: 'Passwords are not the same!'
}
},
passwordChangedAt: Date,
passwordResetToken: String,
passwordResetExpires: Date,
active: {
type: Boolean,
default: true,
select: false
}
});
//----------------**MIDDLEWARE: ENCRYPT PASSWORD**----------------
userSchema.pre('save', async function(next) {
// Only run this function if password was actually modified
if (!this.isModified('password')) return next();
// Hash the password with cost of 12
this.password = await bcrypt.hash(this.password, 12);
// Delete passwordConfirm field
this.passwordConfirm = undefined;
next();
});
//----------------**INSTANCE METHOD: COMPARE PASSWORD**----------------
/**
*
* @param {string} candidatePassword
* @param {string} userPassword
* @returns {boolean}
*/
userSchema.methods.correctPassword = async function(
candidatePassword,
userPassword
) {
return await bcrypt.compare(candidatePassword, userPassword);
};
//----------------**INSTANCE METHOD: CHECK IF PASSWORD CHANGED AFTER JWT ISSUED**----------------
/**
*
* @param {number} JWTTimestamp
* @returns {boolean}
*/
userSchema.methods.changedPasswordAfter = function(JWTTimestamp) {
if (this.passwordChangedAt) {
//Convert password change times to timestamps
const changedTimestamp = parseInt(
this.passwordChangedAt.getTime() / 1000,
10
);
// If the JWT timestamp is earlier than the password change timestamp, the password has been changed after the JWT was issued
return JWTTimestamp < changedTimestamp;
}
// False means NOT changed
return false;
};
//----------------**INSTANCE METHOD: CREATE PASSWORD RESET TOKEN**----------------
/**
* @returns {string} resetToken
*/
userSchema.methods.createPasswordResetToken = function() {
// Create a random token
const resetToken = crypto.randomBytes(32).toString('hex');
// Encrypt the token and store it in the database
this.passwordResetToken = crypto
.createHash('sha256')
.update(resetToken)
.digest('hex');
// Set the token expiration date
console.log({ resetToken }, this.passwordResetToken);
// 10 minutes
this.passwordResetExpires = Date.now() + 10 * 60 * 1000;
return resetToken;
};
const User = mongoose.model('User', userSchema);
module.exports = User;
Here is the flow of the exports.forgotPassword
function across the authController.js
, userModel.js
, and userRoutes.js
files:
- In the
authController.js
file, theexports.forgotPassword
function is defined to handle the logic for the forgot password feature. - In the
authController.js
file, theUser
model is imported using therequire
statement, which is located in theuserModel.js
file. - In the
exports.forgotPassword
function, theUser.findOne
method is called to search for the user in the database using the provided email address. - In the
userModel.js
file, theuserSchema.methods.createPasswordResetToken
method is called to generate a password reset token and store it in thepasswordResetToken
field of the user model. - In the
userModel.js
file, thecreatePasswordResetToken
method is exported ascreatePasswordResetToken
. - In the
authController.js
file, theuser.createPasswordResetToken()
method is called to generate the password reset token. - In the
authController.js
file, thesendEmail
function is called, passing an options object containing the user's email, subject, and message, to send an email to the user. - In the
userRoutes.js
file, theexports.forgotPassword
function is bound to the/forgotPassword
route usingrouter.post('/forgotPassword', authController.forgotPassword)
. - When a user accesses the
/forgotPassword
route, theexports.forgotPassword
function is triggered, and the logic within it is executed.
Summary: The exports.forgotPassword
function is defined in the authController.js
file, which retrieves user information by importing the User
model from the userModel.js
file. The function calls user.createPasswordResetToken()
to generate a password reset token and uses the sendEmail
function to send the reset password email. In the userRoutes.js
file, the exports.forgotPassword
function is bound to the corresponding route. When a user visits that route, the logic within the exports.forgotPassword
function is executed.
//--------------**RESET PASSWORD**----------------
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);
});
models/userModel.js
//----------------**MIDDLEWARE: SET PASSWORD CHANGED AT**----------------
userSchema.pre('save', function(next) {
if (!this.isModified('password') || this.isNew) return next();
this.passwordChangedAt = Date.now() - 1000;
next();
});
//----------------**MIDDLEWARE: FILTER OUT INACTIVE USERS**----------------
userSchema.pre(/^find/, function(next) {
// this points to the current query
this.find({ active: { $ne: false } });
next();
});
controllers/authController.js
//--------------**UPDATE PASSWORD**----------------
/**
* Updates the user's password.
* @throws {AppError} - If the user is not found.
* @throws {AppError} - If the POSTed password is incorrect.
* @throws {AppError} - If the POSTed password and passwordConfirm do not match.
*/
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);
});
routes/userRoutes.js
router.patch(
'/updateMyPassword',
authController.protect,
authController.updatePassword
);
![4](/Users/itsyuimoriispace/Documents/βΆ GitHub/Node.js--Express--MongoDB---More--The-Complete-Bootcamp-2023/dev-data/img/4.png)
//--------------**GENERATE TOKEN**----------------
const signToken = id => {
//payload, secret, options
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
});
};
//--------------**CREATE TOKEN & SEND TOKEN*ne*----------------
/**
* Creates and sends a JWT token as a cookie in the response.
* @param {Object} user - The user object.
* @param {number} statusCode - The HTTP status code.
* @param {Object} res - The response object.
*/
const createSendToken = (user, statusCode, res) => {
// Generate a JWT token for the user
const token = signToken(user._id);
// console.log(token);
// Set cookie options for the JWT token
const cookieOptions = {
expires: new Date(
Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000
),
httpOnly: true
};
// Set secure cookie option in production
if (process.env.NODE_ENV === 'production') {
cookieOptions.secure = true;
}
// Set the JWT token as a cookie in the response
res.cookie('jwt', token, cookieOptions);
// Remove the password field from the user object to avoid exposing it
user.password = undefined;
// Send the response with the JWT token and user data
res.status(statusCode).json({
status: 'success',
token,
data: {
user
}
});
};
npm i express-rate-limit
const rateLimit = require('express-rate-limit');
// Limit requests from the same IP address
const limiter = rateLimit({
max: 100, // 100 requests per hour
windowMs: 60 * 60 * 1000, // 1 hour
message: 'Too many requests from this IP, please try again in an hour!'
});
// Apply to all requests to the API
app.use('/api', limiter);
npm i helmet
npm i express-mongo-sanitize
// Data sanitization against NoSQL query injection
app.use(mongoSanitize());
// Data sanitization against XSS
app.use(xss());
//Prevent parameter pollution
app.use(
hpp({
whitelist: [
'duration',
'ratingsQuantity',
'ratingsAverage',
'maxGroupSize',
'difficulty',
'price'
]
})
);
Embedding vs Referencing
- 1 : many reference
- few:few embeded
![reference vs embeding](/Users/itsyuimoriispace/Documents/βΆ GitHub/Node.js--Express--MongoDB---More--The-Complete-Bootcamp-2023/dev-data/img/reference vs embeding.png)
![data model](/Users/itsyuimoriispace/Documents/βΆ GitHub/Node.js--Express--MongoDB---More--The-Complete-Bootcamp-2023/dev-data/img/datamodel2.png)
Data model
![reference vs embeding](/Users/itsyuimoriispace/Documents/βΆ GitHub/Node.js--Express--MongoDB---More--The-Complete-Bootcamp-2023/dev-data/img/data model.png)
// models/tourModel.js
startLocation: {
// GeoJSON
type: {
type: String,
default: 'Point',
enum: ['Point']
},
coordinates: [Number],
address: String,
description: String
},
// models/tourModel.js
guides: Array;
tourSchema.pre('save', async function(next) {
const guidesPromises = this.guides.map(async id => await User.findById(id));
this.guides = await Promise.all(guidesPromises);
next();
});
this pre-save middleware is responsible for fetching the
User
documents associated with theguides
array (which are represented asObjectId
references) and replacing thoseObjectId
s with the actualUser
documents in theguides
array before saving the tour document to the database. This allows for easy embedding of theUser
documents into the tour document, simplifying future querying and population of related data.
// models/tourModel.js
const User = require('./userModel');
guides: [
{
type: mongoose.Schema.ObjectId,
ref: 'User'
}
];
In the given code snippet, populate
is a method used in Mongoose, which is an Object Data Modeling (ODM) library for MongoDB and Node.js. The populate
method is used to populate references in a document with the actual objects from other collections.
In the context of the code:
const tour = await Tour.findById(req.params.id).populate('guides');
Here, Tour
is the Mongoose model representing the collection of tours in the MongoDB database. The findById
method is used to find a tour document with the specified ID. The populate('guides')
method is then used to populate the guides
field in the tour
document, which is likely a reference to another collection, such as a collection of users representing tour guides.
By using populate
, the guides
field in the tour
document will be replaced with the actual objects from the referenced collection, making it easier to access the details of the guides without having to perform an additional database query. This is particularly useful when you want to retrieve related data from different collections in a single query.
![populating1](/Users/itsyuimoriispace/Documents/βΆ GitHub/Node.js--Express--MongoDB---More--The-Complete-Bootcamp-2023/dev-data/img/populating1.png)
![populating2](/Users/itsyuimoriispace/Documents/βΆ GitHub/Node.js--Express--MongoDB---More--The-Complete-Bootcamp-2023/dev-data/img/populating2.png)
// models/reviewModel.js
reviewSchema.pre(/^find/, function(next) {
// this.populate({
// path: 'tour',
// select: 'name'
// }).populate({
// path: 'user',
// select: 'name photo'
// });
this.populate({
path: 'user',
select: 'name photo'
});
next();
});
- Virtual population allows us to fill comments into the tour document without storing comment IDs in the tour document.
- It provides an efficient way to access all comments of a tour without manual querying or storing comment IDs in the tour document.
- Virtual population improves code readability and maintainability by simplifying the access to comments for each tour.
- Comments are not persistently stored in the database through virtual population, avoiding database bloat.
// models/reviewModel.js
reviewSchema.virtual('comments', {
ref: 'Comment',
foreignField: 'review',
localField: '_id'
});
// models/commentModel.js
//virtual populate
reviewSchema.virtual('durationWeeks').get(function() {
return this.duration / 7;
});
//virtual populate
tourSchema.virtual('reviews', {
ref: 'Review',
foreignField: 'tour',
localField: '_id'
});
const path = require('path');
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));
// 1) GLOBAL MIDDLEWARES
// Serving static files
app.use(express.static(`${__dirname}/public`));
// 3) ROUTES
app.get('/', (req, res) => {
res.status(200).render('base');
});