Now that we can create an account and login, we should also be able to edit our profile information. We can make this happen because we have a middleware that is able to save users information within a database.
export const localsMiddlware = (req, res, next) => {
res.locals.loggedIn = Boolean(req.session.loggedIn);
res.locals.siteName = "Wetube";
res.locals.loggedInUser = req.session.user || {};
next();
};
We then simply repeat the same process similar to all the other functionalities (routers, controllers, pug.js files). The only difference is that we will add the values of the loggedInUser object within the edit-profile pug.js file.
input(placeholder="Name" name="name", type="text", required, value=loggedInUser.name)
input(placeholder="Email" name="email", type="email", required, value=loggedInUser.email)
input(placeholder="Username" name="username", type="text", required, value=loggedInUser.username)
input(placeholder="Location" name="location", type="text", required, value=loggedInUser.location)
In order to protect user information on a website, it is always a good practice to create middlewares that does exactly that. Without a protection middleware, classified information can be visible even to users without an account.
This middleware can easily be created using the session object which remembers user information.
export const protectorMiddleware = (req, res, next) => {
if (req.session.loggedIn) {
return next();
} else {
return res.redirect("/login");
}
};
On the other hand, we can also create a middleware that only grants access to certain pages for users without an account.
export const publicOnlyMiddleware = (req, res, next) => {
if (!req.session.loggedIn) {
return next();
} else {
return res.render("/");
}
};
Lets not forget to add our middlewares on the routers. If the route is linked to a route() function, we can use the all() function to apply the middleware to post GET and POST.
userRouter.get("/logout", protectorMiddleware, logout);
userRouter.route("/edit").all(protectorMiddleware).get(getEdit).post(postEdit);
With the GET controller rendering the content of the pug.js file on the webpage, we can finally create a POST controller for the 'edit'. It is important to note that we need the user's information to edit the user's profile. By accessing the session id within the session object, we can edit the information of the user who is currently logged in.
After we have bring all the information on the controller, we use the findByIdAndUpdate() function to send the information.
export const postEdit = async (req, res) => {
const {
session: {
user: { id },
},
} = req;
const id = req.session.user.id
const { name, email. username, location } = req.body;
await User.findByIdandUpdate(id {
name,
email,
username,
location,
});
return res.render("edit-profile");
};
If we run this code, however, it will not edit the profile. The reason is because even though the database reflects the updated profile, the session is not updated with the new information. Without an updated session, the webpage cannot display the newly updated information.
In order to update the session, we simply add the user information of the session within the user object. This is simply done by creating a new property and setting it to true on the code
const updatedUser = await User.findByIdAndUpdate(
_id,
{
name,
email,
username,
location,
},
{ new: true }
);
Repeating all the previous steps, we can create a brand new router and controller for changing the password of the user.
If the website also has a social login feature, we can prevent social-login accounts from changing the password by applying the protector middleware on the previously created social-login router. Then similarly, we can use the session object to create a condition where the social-login accounts will not have access to the password change functionality.
export const getChangePassword = (req, res) => {
if (req.session.user.socialOnly === true) {
return res.redirect("/");
}
return res.render("users/change-password", { pageTitle: "Change Password" });
};
On the POST controller, we can create conditionals for confirming the password during login/join and updating a new password.
We use the findByID() function to search for the session of a particular user and the save() function to update the newly edited password on the webpage.
export const postChangePassword = async (req, res) => {
const {
session: {
user: { _id },
},
body: { oldPassword, newPassword, newPasswordConfirmation },
} = req;
const user = await User.findById(_id);
const ok = await bcrypt.compare(oldPassword, user.password);
if (!ok) {
return res.status(400).render("users/change-password", {
pageTitle: "Change Password",
errorMessage: "The current password is incorrect",
});
}
if (newPassword !== newPasswordConfirmation) {
return res.status(400).render("users/change-password", {
pageTitle: "Change Password",
errorMessage: "The password does not match the confirmation",
});
}
user.password = newPassword;
await user.save();
return res.redirect("/users/logout");
};
To upload a file, we need to add an input on the template file where the type will be a file and a separate id selector to add some CSS (Don't forget to add a name!). This will create a functionality where users can upload file to the webpage. In cases, where the users are uploading an image, we use an accept property with the value of image/*. With this we can add every type of images on the webpage (jpeg, png, etc).
input(type="file", id="avatar", name="avatar", accept="image/*")
In order to make the uploading process as smooth as possible, we can install and use a package called multer which allows us to configure the way in which we want our files to be uploaded.
npm i multer
This npm, however, has a rule in which it will not run if the form is not a multipart. Without thinking too hard on this concept, all we need to do is add a property called enctype with the value of multipart/form-data and our issue will be solved.
form(method="POST", enctype="multipart/form-data")
Next, we need to create middleware that configures the uploaded files to be stored in a separate folder. By configuring a destination, we can create a folder where the uploaded files will be saved (the folder is created automatically).
Before proceeding, we first need to put the name of the middleware on the POST router and select the amount of files we want to put for each upload.
.post(avatarUpload.single("avatar"), postEdit);
Then when we upload a file, a folder will be created where the data of the file will be stored. Then, the postEdit controller will access the folder and display the file on the webpage.
A thing to note is that if we have a pre-existing file uploaded on the form and we try to upload a new file, we can get an error where the path on the server will become undefined. To check, we should create a conditional to check whether a file exists on the path.
avatarUrl: file ? file.path : avatarUrl
(Completed Code)
export const postEdit = async (req, res) => {
const {
session: {
user: { _id, avatarUrl },
},
body: { name, email, username, location },
file,
} = req;
const updatedUser = await User.findByIdAndUpdate(
_id,
{
avatarUrl: file ? file.path : avatarUrl,
name,
email,
username,
location,
},
{ new: true }
);
req.session.user = updatedUser;
return res.redirect("/users/edit");
};
(edit-profile.js file)
extends base.pug
block content
img(src="/" + loggedInUser.avatarUrl, width="100", height="100")
form(method="POST", enctype="multipart/form-data")
label(for="avatar") Avatar
input(type="file", id="avatar", name="avatar", accept="image/*")
input(placeholder="Name" name="name", type="text", required, value=loggedInUser.name)
input(placeholder="Email" name="email", type="email", required, value=loggedInUser.email)
input(placeholder="Username" name="username", type="text", required, value=loggedInUser.username)
input(placeholder="Location" name="location", type="text", required, value=loggedInUser.location)
input(type="submit", value="Update Profile")
if !loggedInUser.socialOnly
hr
a(href="change-password") Change Password →
IMPORTANT NOTE: NEVER SAVE A FILE ON A DATABASE, BUT THE LOCATION OF THE FILE!
In order for the browser to display the uploaded file, we need to designate the folder for the browser to give access.
We first need to create a route that exposes the folder of choice by applying the static() function. In this case it would be the /uploads folder.
app.use("/uploads", express.static("uploads"));
To create a video upload form, we add an input that receives a video format file. The process will be similar with that of the image file upload.
input(type="file", accept="video/*", required, id="video", name="video")
To make the process easier we can create separate middleware for each type of upload. Additionally, we can configure the size of the file we want to limit on our upload.
export const avatarUpload = multer({
dest: "uploads/avatars/",
limits: {
fileSize: 3000000,
},
});
export const videoUpload = multer({
dest: "uploads/videos/",
limits: {
fileSize: 10000000,
},
});
We can then apply each middleware to its designated router.
.post(videoUpload.single("video"), postUpload);
To upload the file, we create a file variable that gets the requested file and add a fileURL property on the schema.
const file = req.file;
fileUrl: { type: String, required: true }
Just like the image upload, add the enctype property with the multipart data.
form(method="POST", enctype="multipart/form-data")
Finally, we add a path to the watch.pug file
video(src="/" + video.fileUrl, controls)
Although users without an account should not be able to view personal information, they should still be able to view contents that logged-in users post on the website.
We create another controller and request id from req.params. This way, only contents posted by logged-in users are visible.
export const see = async (req, res) => {
const { id } = req.params;
const user = await User.findById(id);
if (!user) {
return res.status(404).render("404", { pageTitle: "User not found." });
}
return res.render("users/profile", {
pageTitle: user.name,
user,
});
};
In order for the users to be able to post videos and save it on their profile, we need to create another form that saves the id of the user that uploads the video.
Make sure to write the objectID of the model.
owner: { type: mongoose.Schema.Types.ObjectId, required: true, ref: "User" }
We then send the id of the user sending the video.
const {
user: { _id },
} = req.session;
try {
await Video.create({
title,
description,
fileUrl,
owner: _id,
hashtags: Video.formatHashtags(hashtags),
});
return res.redirect("/");
Create another pug.js file for checking the owner of the video.
div
small Uploaded by #{owner.name}
if String(video.owner) === String(loggedInUser._id)
a(href=`${video.id}/edit`) Edit Video →
br
a(href=`${video.id}/delete`) Delete Video →
Using the populate() function, mongoose searches for the user of the video and displays information of the video. This is all done by saving the id.
const video = await Video.findById(id).populate("owner");
div
small Uploaded by
a(href=`/users/${video.owner._id}`)=video.owner.name
if String(video.owner._id) === String(loggedInUser._id)
a(href=`${video.id}/edit`) Edit Video →
br
a(href=`${video.id}/delete`) Delete Video →
We also need to create another pug file that handles mixins. This will display all the user's uploaded videos on a single webpage.
block content
each video in videos
+video(video)
else
li Sorry nothing found.
To help users upload videos, we can configure the schema that collects video in an array.
videos: [{ type: mongoose.Schema.Types.ObjectId, ref: "Video" }]
We then configure the controller to push new video to the array.
const user = await User.findById(_id);
user.videos.push(newVideo._id);
user.save();