Repository: manikandanraji/instaclone-backend Branch: master Commit: 5124424007f6 Files: 18 Total size: 23.5 KB Directory structure: gitextract_g_axr77_/ ├── .gitignore ├── README.md ├── package.json └── src/ ├── controllers/ │ ├── auth.js │ ├── post.js │ └── user.js ├── middlewares/ │ ├── asyncHandler.js │ ├── auth.js │ └── errorHandler.js ├── models/ │ ├── Comment.js │ ├── Post.js │ └── User.js ├── routes/ │ ├── auth.js │ ├── post.js │ └── user.js ├── seeder.js ├── server.js └── utils/ └── db.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ .env ================================================ FILE: README.md ================================================ # Instaclone Backend **NOTE: As of 10-06-2022 19:52 IST, I am archiving this repository. It was fun while it lasted.** Instagram clone using MERN stack This is the backend repo built with Express and MongoDB. If you looking for the frontend repo, [click here](https://github.com/manikandanraji/instaclone-frontend) ## Running Locally At the root of the project, you should have a .env with the following contents ```js JWT_SECRET= JWT_EXPIRE=30d // or anything you prefer MONGOURI= ``` Then run npm i && npm run dev to start the development server ## Deploying the backend to heroku First create an heroku account and install the heroku cli globally and login ```bash npm i -g heroku heroku login ``` Once logged in, create a new heroku application and push it to the remote 'heroku' ```bash heroku create git push heroku master ``` Then we need to manually setup the environmental variables using the heroku dashboard ## UI ### Home ![Home](screenshots/home_new.png) ### Explore ![Explore](screenshots/explore_new.png) ### Followers ![Followers](screenshots/followers_new.png) ### Profile ![Profile](screenshots/profile_new.png) ### Edit Profile ![Edit Profile](screenshots/edit_profile_new.png) ### New Post ![New Post](screenshots/new_post_new.png) ================================================ FILE: package.json ================================================ { "name": "instaclone-backend", "version": "1.0.0", "description": "Instaclone backend using Express, MongoDB", "main": "src/server.js", "scripts": { "start": "NODE_ENV=production node src/server", "dev": "nodemon src/server" }, "author": "Manikandan Raji", "license": "MIT", "dependencies": { "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", "mongoose": "^5.9.19", "nodemon": "^2.0.4" } } ================================================ FILE: src/controllers/auth.js ================================================ const User = require("../models/User"); const asyncHandler = require("../middlewares/asyncHandler"); exports.login = asyncHandler(async (req, res, next) => { const { email, password } = req.body; // make sure the email, pw is not empty if (!email || !password) { return next({ message: "Please provide email and password", statusCode: 400, }); } // check if the user exists const user = await User.findOne({ email }); if (!user) { return next({ message: "The email is not yet registered to an accout", statusCode: 400, }); } // if exists, make sure the password matches const match = await user.checkPassword(password); if (!match) { return next({ message: "The password does not match", statusCode: 400 }); } const token = user.getJwtToken(); // then send json web token as response res.status(200).json({ success: true, token }); }); exports.signup = asyncHandler(async (req, res, next) => { const { fullname, username, email, password } = req.body; const user = await User.create({ fullname, username, email, password }); const token = user.getJwtToken(); res.status(200).json({ success: true, token }); }); exports.me = asyncHandler(async (req, res, next) => { const { avatar, username, fullname, email, _id, website, bio } = req.user; res .status(200) .json({ success: true, data: { avatar, username, fullname, email, _id, website, bio }, }); }); ================================================ FILE: src/controllers/post.js ================================================ const mongoose = require("mongoose"); const Post = require("../models/Post"); const User = require("../models/User"); const Comment = require("../models/Comment"); const asyncHandler = require("../middlewares/asyncHandler"); exports.getPosts = asyncHandler(async (req, res, next) => { const posts = await Post.find(); res.status(200).json({ success: true, data: posts }); }); exports.getPost = asyncHandler(async (req, res, next) => { const post = await Post.findById(req.params.id) .populate({ path: "comments", select: "text", populate: { path: "user", select: "username avatar", }, }) .populate({ path: "user", select: "username avatar", }) .lean() .exec(); if (!post) { return next({ message: `No post found for id ${req.params.id}`, statusCode: 404, }); } // is the post belongs to loggedin user? post.isMine = req.user.id === post.user._id.toString(); // is the loggedin user liked the post?? const likes = post.likes.map((like) => like.toString()); post.isLiked = likes.includes(req.user.id); // is the loggedin user liked the post?? const savedPosts = req.user.savedPosts.map((post) => post.toString()); post.isSaved = savedPosts.includes(req.params.id); // is the comment on the post belongs to the logged in user? post.comments.forEach((comment) => { comment.isCommentMine = false; const userStr = comment.user._id.toString(); if (userStr === req.user.id) { comment.isCommentMine = true; } }); res.status(200).json({ success: true, data: post }); }); exports.deletePost = asyncHandler(async (req, res, next) => { const post = await Post.findById(req.params.id); if (!post) { return next({ message: `No post found for id ${req.params.id}`, statusCode: 404, }); } if (post.user.toString() !== req.user.id) { return next({ message: "You are not authorized to delete this post", statusCode: 401, }); } await User.findByIdAndUpdate(req.user.id, { $pull: { posts: req.params.id }, $inc: { postCount: -1 }, }); await post.remove(); res.status(200).json({ success: true, data: {} }); }); exports.addPost = asyncHandler(async (req, res, next) => { const { caption, files, tags } = req.body; const user = req.user.id; let post = await Post.create({ caption, files, tags, user }); await User.findByIdAndUpdate(req.user.id, { $push: { posts: post._id }, $inc: { postCount: 1 }, }); post = await post .populate({ path: "user", select: "avatar username fullname" }) .execPopulate(); res.status(200).json({ success: true, data: post }); }); exports.toggleLike = asyncHandler(async (req, res, next) => { // make sure that the post exists const post = await Post.findById(req.params.id); if (!post) { return next({ message: `No post found for id ${req.params.id}`, statusCode: 404, }); } if (post.likes.includes(req.user.id)) { const index = post.likes.indexOf(req.user.id); post.likes.splice(index, 1); post.likesCount = post.likesCount - 1; await post.save(); } else { post.likes.push(req.user.id); post.likesCount = post.likesCount + 1; await post.save(); } res.status(200).json({ success: true, data: {} }); }); exports.addComment = asyncHandler(async (req, res, next) => { const post = await Post.findById(req.params.id); if (!post) { return next({ message: `No post found for id ${req.params.id}`, statusCode: 404, }); } let comment = await Comment.create({ user: req.user.id, post: req.params.id, text: req.body.text, }); post.comments.push(comment._id); post.commentsCount = post.commentsCount + 1; await post.save(); comment = await comment .populate({ path: "user", select: "avatar username fullname" }) .execPopulate(); res.status(200).json({ success: true, data: comment }); }); exports.deleteComment = asyncHandler(async (req, res, next) => { const post = await Post.findById(req.params.id); if (!post) { return next({ message: `No post found for id ${req.params.id}`, statusCode: 404, }); } const comment = await Comment.findOne({ _id: req.params.commentId, post: req.params.id, }); if (!comment) { return next({ message: `No comment found for id ${req.params.id}`, statusCode: 404, }); } if (comment.user.toString() !== req.user.id) { return next({ message: "You are not authorized to delete this comment", statusCode: 401, }); } // remove the comment from the post const index = post.comments.indexOf(comment._id); post.comments.splice(index, 1); post.commentsCount = post.commentsCount - 1; await post.save(); await comment.remove(); res.status(200).json({ success: true, data: {} }); }); exports.searchPost = asyncHandler(async (req, res, next) => { if (!req.query.caption && !req.query.tag) { return next({ message: "Please enter either caption or tag to search for", statusCode: 400, }); } let posts = []; if (req.query.caption) { const regex = new RegExp(req.query.caption, "i"); posts = await Post.find({ caption: regex }); } if (req.query.tag) { posts = posts.concat([await Post.find({ tags: req.query.tag })]); } res.status(200).json({ success: true, data: posts }); }); exports.toggleSave = asyncHandler(async (req, res, next) => { // make sure that the post exists const post = await Post.findById(req.params.id); if (!post) { return next({ message: `No post found for id ${req.params.id}`, statusCode: 404, }); } const { user } = req; if (user.savedPosts.includes(req.params.id)) { console.log("removing saved post"); await User.findByIdAndUpdate(user.id, { $pull: { savedPosts: req.params.id }, }); } else { console.log("saving post"); await User.findByIdAndUpdate(user.id, { $push: { savedPosts: req.params.id }, }); } res.status(200).json({ success: true, data: {} }); }); ================================================ FILE: src/controllers/user.js ================================================ const User = require("../models/User"); const Post = require("../models/Post"); const asyncHandler = require("../middlewares/asyncHandler"); exports.getUsers = asyncHandler(async (req, res, next) => { let users = await User.find().select("-password").lean().exec(); users.forEach((user) => { user.isFollowing = false; const followers = user.followers.map((follower) => follower._id.toString()); if (followers.includes(req.user.id)) { user.isFollowing = true; } }); users = users.filter((user) => user._id.toString() !== req.user.id); res.status(200).json({ success: true, data: users }); }); exports.getUser = asyncHandler(async (req, res, next) => { const user = await User.findOne({ username: req.params.username }) .select("-password") .populate({ path: "posts", select: "files commentsCount likesCount" }) .populate({ path: "savedPosts", select: "files commentsCount likesCount" }) .populate({ path: "followers", select: "avatar username fullname" }) .populate({ path: "following", select: "avatar username fullname" }) .lean() .exec(); if (!user) { return next({ message: `The user ${req.params.username} is not found`, statusCode: 404, }); } user.isFollowing = false; const followers = user.followers.map((follower) => follower._id.toString()); user.followers.forEach((follower) => { follower.isFollowing = false; if (req.user.following.includes(follower._id.toString())) { follower.isFollowing = true; } }); user.following.forEach((user) => { user.isFollowing = false; if (req.user.following.includes(user._id.toString())) { user.isFollowing = true; } }); if (followers.includes(req.user.id)) { user.isFollowing = true; } user.isMe = req.user.id === user._id.toString(); res.status(200).json({ success: true, data: user }); }); exports.follow = asyncHandler(async (req, res, next) => { // make sure the user exists const user = await User.findById(req.params.id); if (!user) { return next({ message: `No user found for id ${req.params.id}`, statusCode: 404, }); } // make the sure the user is not the logged in user if (req.params.id === req.user.id) { return next({ message: "You can't unfollow/follow yourself", status: 400 }); } // only follow if the user is not following already if (user.followers.includes(req.user.id)) { return next({ message: "You are already following him", status: 400 }); } await User.findByIdAndUpdate(req.params.id, { $push: { followers: req.user.id }, $inc: { followersCount: 1 }, }); await User.findByIdAndUpdate(req.user.id, { $push: { following: req.params.id }, $inc: { followingCount: 1 }, }); res.status(200).json({ success: true, data: {} }); }); exports.unfollow = asyncHandler(async (req, res, next) => { const user = await User.findById(req.params.id); if (!user) { return next({ message: `No user found for ID ${req.params.id}`, statusCode: 404, }); } // make the sure the user is not the logged in user if (req.params.id === req.user.id) { return next({ message: "You can't follow/unfollow yourself", status: 400 }); } await User.findByIdAndUpdate(req.params.id, { $pull: { followers: req.user.id }, $inc: { followersCount: -1 }, }); await User.findByIdAndUpdate(req.user.id, { $pull: { following: req.params.id }, $inc: { followingCount: -1 }, }); res.status(200).json({ success: true, data: {} }); }); exports.feed = asyncHandler(async (req, res, next) => { const following = req.user.following; const users = await User.find() .where("_id") .in(following.concat([req.user.id])) .exec(); const postIds = users.map((user) => user.posts).flat(); const posts = await Post.find() .populate({ path: "comments", select: "text", populate: { path: "user", select: "avatar fullname username" }, }) .populate({ path: "user", select: "avatar fullname username" }) .sort("-createdAt") .where("_id") .in(postIds) .lean() .exec(); posts.forEach((post) => { // is the loggedin user liked the post post.isLiked = false; const likes = post.likes.map((like) => like.toString()); if (likes.includes(req.user.id)) { post.isLiked = true; } // is the loggedin saved this post post.isSaved = false; const savedPosts = req.user.savedPosts.map((post) => post.toString()); if (savedPosts.includes(post._id)) { post.isSaved = true; } // is the post belongs to the loggedin user post.isMine = false; if (post.user._id.toString() === req.user.id) { post.isMine = true; } // is the comment belongs to the loggedin user post.comments.map((comment) => { comment.isCommentMine = false; if (comment.user._id.toString() === req.user.id) { comment.isCommentMine = true; } }); }); res.status(200).json({ success: true, data: posts }); }); exports.searchUser = asyncHandler(async (req, res, next) => { if (!req.query.username) { return next({ message: "The username cannot be empty", statusCode: 400 }); } const regex = new RegExp(req.query.username, "i"); const users = await User.find({ username: regex }); res.status(200).json({ success: true, data: users }); }); exports.editUser = asyncHandler(async (req, res, next) => { const { avatar, username, fullname, website, bio, email } = req.body; const fieldsToUpdate = {}; if (avatar) fieldsToUpdate.avatar = avatar; if (username) fieldsToUpdate.username = username; if (fullname) fieldsToUpdate.fullname = fullname; if (email) fieldsToUpdate.email = email; const user = await User.findByIdAndUpdate( req.user.id, { $set: { ...fieldsToUpdate, website, bio }, }, { new: true, runValidators: true, } ).select("avatar username fullname email bio website"); res.status(200).json({ success: true, data: user }); }); ================================================ FILE: src/middlewares/asyncHandler.js ================================================ const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); module.exports = asyncHandler; ================================================ FILE: src/middlewares/auth.js ================================================ const jwt = require("jsonwebtoken"); const User = require("../models/User"); exports.protect = async (req, res, next) => { let token; if ( req.headers.authorization && req.headers.authorization.startsWith("Bearer") ) { token = req.headers.authorization.split(" ")[1]; } if (!token) { return next({ message: "You need to be logged in to visit this route", statusCode: 403, }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); const user = await User.findById(decoded.id).select("-password"); if (!user) { return next({ message: `No user found for ID ${decoded.id}` }); } req.user = user; next(); } catch (err) { next({ message: "You need to be logged in to visit this route", statusCode: 403, }); } }; ================================================ FILE: src/middlewares/errorHandler.js ================================================ const errorHandler = (err, req, res, next) => { console.log(err); let message = err.message || "Internal server error"; let statusCode = err.statusCode || 500; if (err.code === 11000) { message = "Duplicate key"; if (err.keyValue.email) { message = "The email is already taken"; } if (err.keyValue.username) { message = "The username is already taken"; } statusCode = 400; } if (err.name === "ValidationError") { const fields = Object.keys(err.errors); fields.map((field) => { if (err.errors[field].kind === "maxlength") { message = "Password should be maximum of 12 characters"; } else { message = "Password should be minimum of 6 characters"; } }); statusCode = 400; } if (err.name === "CastError") { message = "The ObjectID is malformed"; statusCode = 400; } res.status(statusCode).json({ success: false, message }); }; module.exports = errorHandler; ================================================ FILE: src/models/Comment.js ================================================ const mongoose = require("mongoose"); const CommentSchema = new mongoose.Schema({ user: { type: mongoose.Schema.ObjectId, ref: "User", required: true, }, post: { type: mongoose.Schema.ObjectId, ref: "Post", required: true, }, text: { type: String, required: [true, "Please enter the comment"], trim: true, }, createdAt: { type: Date, default: Date.now, }, }); module.exports = mongoose.model("Comment", CommentSchema); ================================================ FILE: src/models/Post.js ================================================ const mongoose = require("mongoose"); const PostSchema = new mongoose.Schema({ user: { type: mongoose.Schema.ObjectId, ref: "User", required: true, }, caption: { type: String, required: [true, "Please enter the caption"], trim: true, }, tags: { type: [String], }, files: { type: [String], validate: (v) => v === null || v.length > 0, }, likes: [{ type: mongoose.Schema.ObjectId, ref: "User" }], likesCount: { type: Number, default: 0, }, comments: [{ type: mongoose.Schema.ObjectId, ref: "Comment" }], commentsCount: { type: Number, default: 0, }, createdAt: { type: Date, default: Date.now, }, }); module.exports = mongoose.model("Post", PostSchema); ================================================ FILE: src/models/User.js ================================================ const mongoose = require("mongoose"); const jwt = require("jsonwebtoken"); const bcrypt = require("bcryptjs"); const UserSchema = new mongoose.Schema({ fullname: { type: String, required: [true, "Please enter your fullname"], trim: true, }, username: { type: String, required: [true, "Please enter your username"], trim: true, unique: true, }, email: { type: String, required: [true, "Please enter your email"], trim: true, lowercase: true, unique: true, }, password: { type: String, required: [true, "Please enter your password"], minlength: [6, "Password should be atleast minimum of 6 characters"], maxlength: [12, "Password should be maximum of 12 characters"], }, avatar: { type: String, default: "https://res.cloudinary.com/tylerdurden/image/upload/v1602657481/random/pngfind.com-default-image-png-6764065_krremh.png", }, bio: String, website: String, followers: [{ type: mongoose.Schema.ObjectId, ref: "User" }], followersCount: { type: Number, default: 0, }, followingCount: { type: Number, default: 0, }, following: [{ type: mongoose.Schema.ObjectId, ref: "User" }], posts: [{ type: mongoose.Schema.ObjectId, ref: "Post" }], postCount: { type: Number, default: 0, }, savedPosts: [{ type: mongoose.Schema.ObjectId, ref: "Post" }], createdAt: { type: Date, default: Date.now, }, }); UserSchema.pre("save", async function (next) { const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); next(); }); UserSchema.methods.getJwtToken = function () { return jwt.sign({ id: this._id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRE, }); }; UserSchema.methods.checkPassword = async function (password) { return await bcrypt.compare(password, this.password); }; module.exports = mongoose.model("User", UserSchema); ================================================ FILE: src/routes/auth.js ================================================ const express = require("express"); const router = express.Router(); const { login, signup, me } = require("../controllers/auth"); const { protect } = require("../middlewares/auth"); router.route("/signup").post(signup); router.route("/login").post(login); router.route("/me").get(protect, me); module.exports = router; ================================================ FILE: src/routes/post.js ================================================ const express = require("express"); const router = express.Router(); const { getPosts, getPost, addPost, deletePost, toggleLike, toggleSave, addComment, deleteComment, searchPost, } = require("../controllers/post"); const { protect } = require("../middlewares/auth"); router.route("/").get(getPosts).post(protect, addPost); router.route("/search").get(searchPost); router.route("/:id").get(protect, getPost).delete(protect, deletePost); router.route("/:id/togglelike").get(protect, toggleLike); router.route("/:id/togglesave").get(protect, toggleSave); router.route("/:id/comments").post(protect, addComment); router.route("/:id/comments/:commentId").delete(protect, deleteComment); module.exports = router; ================================================ FILE: src/routes/user.js ================================================ const express = require("express"); const router = express.Router(); const { getUsers, getUser, follow, unfollow, feed, searchUser, editUser, } = require("../controllers/user"); const { protect } = require("../middlewares/auth"); router.route("/").get(protect, getUsers); router.route("/").put(protect, editUser); router.route("/feed").get(protect, feed); router.route("/search").get(searchUser); router.route("/:username").get(protect, getUser); router.route("/:id/follow").get(protect, follow); router.route("/:id/unfollow").get(protect, unfollow); module.exports = router; ================================================ FILE: src/seeder.js ================================================ require("dotenv").config(); const mongoose = require("mongoose"); const User = require("./models/User"); const Comment = require("./models/Comment"); const Post = require("./models/Post"); mongoose.connect(process.env.MONGOURI, { useCreateIndex: true, useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, }); const deleteData = async () => { try { await User.deleteMany(); await Comment.deleteMany(); await Post.deleteMany(); console.log("Deleted data..."); process.exit(); } catch (err) { console.error(err); } }; if (process.argv[2] === "-d") { deleteData(); } else { console.log("not enough arguments"); process.exit(1); } ================================================ FILE: src/server.js ================================================ require("dotenv").config(); const express = require("express"); const cors = require("cors"); const auth = require("./routes/auth"); const user = require("./routes/user"); const post = require("./routes/post"); const connectToDb = require("./utils/db"); const errorHandler = require("./middlewares/errorHandler"); const app = express(); connectToDb(); app.use(express.json()); app.use(cors()); app.use("/api/v1/auth", auth); app.use("/api/v1/users", user); app.use("/api/v1/posts", post); app.use(errorHandler); const PORT = process.env.PORT || 5000; app.listen( PORT, console.log(`server started in ${process.env.NODE_ENV} mode at port ${PORT}`) ); ================================================ FILE: src/utils/db.js ================================================ const mongoose = require("mongoose"); const connectToDb = async () => { try { const connection = await mongoose.connect(process.env.MONGOURI, { useCreateIndex: true, useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, }); console.log(`Connected to database ${connection.connections[0].name}`); } catch (err) { console.error(err); } }; module.exports = connectToDb;