├── Procfile ├── .gitignore ├── screenshots ├── chat.png ├── convo.png ├── notif.png ├── post.png ├── profile.png └── cretaePost.png ├── utils ├── catchAsync.js ├── profile.js ├── cloudinary.js ├── fileUpload.js ├── appError.js ├── multer.js ├── sendMail.js └── test.js ├── models ├── Bookmark.js ├── Otp.js ├── Group.js ├── Notification.js ├── Messages.js ├── User.js ├── Comment.js ├── Post.js └── Profile.js ├── routes ├── bookmark.js ├── Post.js ├── group.js ├── comment.js ├── profile.js └── user.js ├── package.json ├── README.md ├── controllers ├── bookmarkController.js ├── commentController.js ├── errorHandler │ └── error.js ├── postController.js ├── group.js ├── replyController.js ├── authController.js └── profileController.js ├── server.js └── app.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config.env/ -------------------------------------------------------------------------------- /screenshots/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charanpy/instagram-clone-API/HEAD/screenshots/chat.png -------------------------------------------------------------------------------- /screenshots/convo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charanpy/instagram-clone-API/HEAD/screenshots/convo.png -------------------------------------------------------------------------------- /screenshots/notif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charanpy/instagram-clone-API/HEAD/screenshots/notif.png -------------------------------------------------------------------------------- /screenshots/post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charanpy/instagram-clone-API/HEAD/screenshots/post.png -------------------------------------------------------------------------------- /screenshots/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charanpy/instagram-clone-API/HEAD/screenshots/profile.png -------------------------------------------------------------------------------- /screenshots/cretaePost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charanpy/instagram-clone-API/HEAD/screenshots/cretaePost.png -------------------------------------------------------------------------------- /utils/catchAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = fn => { 2 | return (req, res, next) => { 3 | fn(req, res, next).catch(next) 4 | } 5 | } -------------------------------------------------------------------------------- /utils/profile.js: -------------------------------------------------------------------------------- 1 | const Profile = require("../models/Profile"); 2 | 3 | const getProfileId = async (user) => { 4 | 5 | const { _id } = await Profile.findOne({ user: user }); 6 | return _id; 7 | } 8 | 9 | module.exports = getProfileId; -------------------------------------------------------------------------------- /utils/cloudinary.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require('cloudinary').v2; 2 | 3 | const cloudinaryConfig = () => { 4 | cloudinary.config({ 5 | cloud_name: process.env.CLOUDINARY_CLOUD, 6 | api_key: process.env.CLOUDINARY_KEY, 7 | api_secret: process.env.CLOUDINARY_SECRET, 8 | }); 9 | }; 10 | 11 | module.exports = cloudinaryConfig; 12 | -------------------------------------------------------------------------------- /utils/fileUpload.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require('cloudinary').v2; 2 | const fileUpload = async (file) => { 3 | if (file.mimetype.slice(0, 5) === 'video') { 4 | return await cloudinary.uploader.upload(file.path, { 5 | resource_type: 'video', 6 | end_offset: '30', 7 | video_codec: 'auto', 8 | }); 9 | } 10 | return await cloudinary.uploader.upload(file.path); 11 | }; 12 | 13 | module.exports = fileUpload; 14 | -------------------------------------------------------------------------------- /utils/appError.js: -------------------------------------------------------------------------------- 1 | class AppError extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | this.status = `${statusCode}`.startsWith('4') ? "fail" : "error" 6 | this.isOperational = true; 7 | 8 | Error.captureStackTrace(this, this.constructor) 9 | } 10 | } 11 | 12 | module.exports = AppError; -------------------------------------------------------------------------------- /models/Bookmark.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const bookmarkSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | default: 'Your Collection', 7 | }, 8 | user: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'User', 11 | }, 12 | post: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'Post', 15 | }, 16 | }); 17 | 18 | const Bookmark = new mongoose.model('Bookmark', bookmarkSchema); 19 | module.exports = Bookmark; 20 | -------------------------------------------------------------------------------- /utils/multer.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer'); 2 | const path = require('path'); 3 | const AppError = require('./appError'); 4 | module.exports = multer({ 5 | storage: multer.diskStorage({}), 6 | fileFilter: (req, file, cb) => { 7 | let ext = path.extname(file.originalname); 8 | console.log('file', ext, file); 9 | if (ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png' && ext !== '.mp4') { 10 | // cb(new Error(`File type ${ext} not supported`), false) 11 | return cb(new AppError(`File type ${ext} not supported`, 400)); 12 | } 13 | cb(null, true); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /routes/bookmark.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const { protect } = require("../controllers/authController") 5 | const { 6 | saveBookmark, 7 | deleteBookmark, 8 | deleteAll, 9 | getSavedPosts 10 | 11 | } = require("../controllers/bookmarkController") 12 | 13 | router.route("/:id") 14 | 15 | .post(protect, saveBookmark) 16 | .delete(protect, deleteBookmark) 17 | 18 | router.route("/") 19 | .delete(deleteAll) 20 | .get(protect, getSavedPosts) 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /models/Otp.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const validator = require("validator") 3 | 4 | const OtpSchema = new mongoose.Schema({ 5 | email: { 6 | type: String, 7 | required: [true, "Email is required"], 8 | trim: true, 9 | validate: validator.isEmail, 10 | 11 | }, 12 | OtpInsta: { 13 | type: Number, 14 | required: [true, 'OTP is required'], 15 | maxlength: 6, 16 | minlength: 6 17 | }, 18 | isAuthenticated: { 19 | type: Boolean, 20 | default: false 21 | } 22 | }) 23 | 24 | const Otp = mongoose.model("Otp", OtpSchema) 25 | 26 | module.exports = Otp; -------------------------------------------------------------------------------- /routes/Post.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { getProfileId } = require('../controllers/authController'); 5 | 6 | const { protect } = require('../controllers/authController'); 7 | const { 8 | createPost, 9 | getAllPost, 10 | getPostById, 11 | deletePost, 12 | likePost, 13 | } = require('../controllers/postController'); 14 | const upload = require('../utils/multer'); 15 | 16 | router 17 | .route('/') 18 | .get(protect, getProfileId, getAllPost) 19 | .post(protect, getProfileId, upload.array('image'), createPost); 20 | 21 | router.route('/:id').get(getPostById).delete(protect, deletePost); 22 | 23 | router.route('/like/:id').post(protect, likePost); 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /models/Group.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const GroupSchema = new mongoose.Schema({ 4 | users: [ 5 | { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'Profile', 8 | }, 9 | ], 10 | createdBy: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: 'Profile', 13 | }, 14 | date: { 15 | type: Date, 16 | default: Date.now, 17 | }, 18 | groupType: { 19 | type: String, 20 | required: [true, 'Group type is required'], 21 | enum: ['private', 'public'], 22 | }, 23 | }); 24 | 25 | GroupSchema.pre(/^find/, function (next) { 26 | this.find().populate({ 27 | path: 'users', 28 | select: 'username user name photo _id', 29 | }); 30 | next(); 31 | }); 32 | 33 | const Group = mongoose.model('Group', GroupSchema); 34 | module.exports = Group; 35 | -------------------------------------------------------------------------------- /utils/sendMail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | 4 | const sendEmail = async options => { 5 | console.log(options.email) 6 | const transporter = nodemailer.createTransport({ 7 | service: 'gmail', 8 | auth: { 9 | user: process.env.EMAIL_USERNAME, 10 | pass: process.env.EMAIL_PASSWORD 11 | } 12 | }); 13 | 14 | 15 | const mailOptions = { 16 | from: process.env.EMAIL_USERNAME, 17 | to: options.email, 18 | subject: options.subject, 19 | text: options.message 20 | }; 21 | 22 | await transporter.sendMail(mailOptions) 23 | } 24 | 25 | module.exports = sendEmail; -------------------------------------------------------------------------------- /models/Notification.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const NotificationSchema = new mongoose.Schema({ 4 | user: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: 'Profile', 7 | }, 8 | to: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'Profile', 11 | }, 12 | createdAt: { 13 | type: Date, 14 | default: Date.now, 15 | }, 16 | type: { 17 | type: String, 18 | enum: ['Follow', 'Like'], 19 | default: 'Like', 20 | }, 21 | post: { 22 | type: mongoose.Schema.Types.ObjectId, 23 | ref: 'Post', 24 | }, 25 | seen: { 26 | type: Boolean, 27 | default: false, 28 | }, 29 | }); 30 | 31 | NotificationSchema.pre(/^find/, function (next) { 32 | this.find().populate('user post'); 33 | 34 | next(); 35 | }); 36 | 37 | const Notification = mongoose.model('Notification', NotificationSchema); 38 | module.exports = Notification; 39 | -------------------------------------------------------------------------------- /models/Messages.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const MessageSchema = new mongoose.Schema({ 4 | groupId: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: 'Group', 7 | }, 8 | sender: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'Profile', 11 | }, 12 | to: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'Profile', 15 | }, 16 | message: { 17 | type: String, 18 | required: [true, 'Message should not be empty'], 19 | }, 20 | createdAt: { 21 | type: Date, 22 | default: Date.now, 23 | }, 24 | seen: [ 25 | { 26 | type: mongoose.Schema.Types.ObjectId, 27 | ref: 'Profile', 28 | }, 29 | ], 30 | }); 31 | 32 | MessageSchema.pre(/^find/, function (next) { 33 | this.find().populate('group'); 34 | next(); 35 | }); 36 | 37 | const Message = new mongoose.model('Message', MessageSchema); 38 | 39 | module.exports = Message; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insta-clone", 3 | "version": "1.0.0", 4 | "description": "instagram-clone", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js", 9 | "dev": "nodemon server.js" 10 | }, 11 | "author": "charan", 12 | "license": "ISC", 13 | "engines": { 14 | "node": "12.16.2" 15 | }, 16 | "nodemonConfig": { 17 | "ignore": [ 18 | "client/*" 19 | ] 20 | }, 21 | "dependencies": { 22 | "bcryptjs": "^2.4.3", 23 | "cloudinary": "^1.23.0", 24 | "cookie-parser": "^1.4.5", 25 | "cors": "^2.8.5", 26 | "dotenv": "^8.2.0", 27 | "express": "^4.17.1", 28 | "express-fileupload": "^1.2.0", 29 | "express-validator": "^6.6.1", 30 | "jsonwebtoken": "^8.5.1", 31 | "mongoose": "^5.10.9", 32 | "morgan": "^1.10.0", 33 | "multer": "^1.4.2", 34 | "nodemailer": "^6.4.14", 35 | "socket.io": "^4.0.0", 36 | "validator": "^13.1.17" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Instagram clone Backend-Rest Api 2 | 3 | ### FrontEnd 4 | [Instagram Frontend](https://github.com/charanpy/Instagram-react) 5 | 6 | 7 | ![Home](https://raw.github.com/charanpy/instagram-clone-API/master/screenshots/post.png) 8 | ## Feautures 9 | 10 | ### jwt authentication 11 | ### create,delete post 12 | ### upload,update.delete profile pic 13 | ### follow/unfollow,follow-request,accept-request 14 | ### comment,reply,like-comment,like-comment-reply 15 | ### save post to bookmark 16 | 17 | ## Notification 18 | ![Notification](https://raw.github.com/charanpy/instagram-clone-API/master/screenshots/notif.png) 19 | 20 | ## Chat Panel 21 | ![ChatPanel](https://raw.github.com/charanpy/instagram-clone-API/master/screenshots/chat.png) 22 | 23 | ## Chat 24 | ![Chat](https://raw.github.com/charanpy/instagram-clone-API/master/screenshots/convo.png) 25 | 26 | ## Profile 27 | ![Profile](https://raw.github.com/charanpy/instagram-clone-API/master/screenshots/profile.png) 28 | 29 | ## Post 30 | ![Post](https://raw.github.com/charanpy/instagram-clone-API/master/screenshots/cretaePost.png) 31 | -------------------------------------------------------------------------------- /routes/group.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const upload = require('../utils/multer'); 4 | 5 | const { getProfileId } = require('../controllers/authController'); 6 | const { 7 | createGroup, 8 | getGroup, 9 | createMessage, 10 | getGroupMessage, 11 | setSeenMessages, 12 | getUserGroup, 13 | getNotifications, 14 | } = require('../controllers/group'); 15 | 16 | const {messagePhoto} = require('../controllers/profileController'); 17 | const { protect } = require('../controllers/authController'); 18 | 19 | router 20 | .route('/') 21 | .get(protect, getProfileId, getUserGroup) 22 | .post(protect, getProfileId, createGroup); 23 | 24 | router.route('/notifications').get(protect, getProfileId, getNotifications); 25 | router.route('/:groupId').get(protect, getProfileId, getGroup); 26 | router 27 | .route('/:groupId/message') 28 | .post(protect, getProfileId, createMessage) 29 | .get(protect, getProfileId, getGroupMessage); 30 | 31 | router 32 | .route('/:groupId/message/photo/:to') 33 | .post(protect, upload.single('image'), messagePhoto) 34 | 35 | 36 | router.route('/:groupId/seen').patch(protect, getProfileId, setSeenMessages); 37 | // router.route('/like/:id').post(protect, likePost); 38 | module.exports = router; 39 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const validator = require('validator'); 3 | const bcrypt = require('bcryptjs'); 4 | 5 | const userSchema = new mongoose.Schema( 6 | { 7 | email: { 8 | type: String, 9 | trim: true, 10 | required: [true, 'Email is required'], 11 | validate: validator.isEmail, 12 | unique: [true, 'Email already exists'], 13 | }, 14 | password: { 15 | type: String, 16 | minlength: 8, 17 | required: [true, 'Password is required'], 18 | select: false, 19 | }, 20 | 21 | passwordChangedAt: { 22 | type: Date, 23 | }, 24 | passwordResetToken: String, 25 | passwordResetExpires: Date, 26 | active: { 27 | type: Boolean, 28 | default: true, 29 | select: false, 30 | }, 31 | }, 32 | { timestamps: true } 33 | ); 34 | 35 | //^Hash password 36 | userSchema.pre('save', async function (next) { 37 | if (!this.isModified('password')) return next(); 38 | 39 | this.password = await bcrypt.hash(this.password, 12); 40 | next(); 41 | }); 42 | 43 | //~Compare Password 44 | userSchema.methods.comparePassword = async function (dbPassword, userPassword) { 45 | return await bcrypt.compare(dbPassword, userPassword); 46 | }; 47 | 48 | const User = mongoose.model('User', userSchema); 49 | 50 | module.exports = User; 51 | -------------------------------------------------------------------------------- /routes/comment.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const { 4 | addComment, 5 | checkPost, 6 | likeComment, 7 | deleteComment 8 | } = require("../controllers/commentController"); 9 | 10 | const { getProfileId } = require("../controllers/authController") 11 | 12 | 13 | const { 14 | createComment, 15 | likeReply, 16 | checkComment, 17 | deleteReply 18 | 19 | } = require("../controllers/replyController") 20 | 21 | const { protect } = require("../controllers/authController") 22 | 23 | //router.route("/").get(getAllComments) 24 | 25 | //router.route("/:id").get(getComment) 26 | 27 | //comment 28 | router.route("/:id") 29 | .post(protect, checkPost,getProfileId,addComment) 30 | 31 | router.route("/like/:id") 32 | .patch(protect,getProfileId, likeComment) 33 | 34 | 35 | router.route("/:id/:commentId") 36 | .delete(protect, checkPost, deleteComment) 37 | 38 | 39 | //reply 40 | 41 | router.route("/reply/:id") 42 | .post(protect, checkComment,getProfileId,createComment) 43 | 44 | router.route("/reply/like/:id/:replyId") 45 | .patch(protect, checkComment,getProfileId, likeReply) 46 | .delete(protect, checkComment, deleteReply) 47 | 48 | module.exports = router; -------------------------------------------------------------------------------- /controllers/bookmarkController.js: -------------------------------------------------------------------------------- 1 | const Bookmark = require('../models/Bookmark'); 2 | const catchAsync = require('../utils/catchAsync'); 3 | 4 | const AppError = require('../utils/appError'); 5 | 6 | //save post 7 | exports.saveBookmark = catchAsync(async (req, res, next) => { 8 | const bookmark = await Bookmark.create({ 9 | user: req.user.id, 10 | post: req.params.id, //req.body.i, 11 | ...req.body, 12 | }); 13 | Bookmark.populate(bookmark, { path: 'post' }, function (err, save) { 14 | if (err) { 15 | console.log(err); 16 | } 17 | }); 18 | 19 | res.status(201).json({ 20 | status: 'success', 21 | bookmark, 22 | }); 23 | }); 24 | 25 | exports.deleteBookmark = catchAsync(async (req, res, next) => { 26 | const bookmark = await Bookmark.deleteOne({ 27 | user: req.user.id, 28 | post: req.params.id, 29 | }); 30 | 31 | if (!bookmark) { 32 | return next(new AppError('No posts found', 400)); 33 | } 34 | 35 | res.json({ 36 | message: 'success', 37 | }); 38 | }); 39 | 40 | exports.deleteAll = catchAsync(async (req, res, next) => { 41 | const bookmark = await Bookmark.deleteMany({}); 42 | res.json({ 43 | status: 'success', 44 | }); 45 | }); 46 | 47 | exports.getSavedPosts = catchAsync(async (req, res, next) => { 48 | const bookmark = await Bookmark.find({ 49 | user: req.user.id, 50 | }); 51 | 52 | res.status(200).json({ 53 | data: bookmark, 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /utils/test.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // exports.getComment = catchAsync(async (req, res, next) => { 4 | // const comment = await Comment.findById(req.params.id).populate({ 5 | // path: "profile", 6 | // select: "user username name photo " 7 | // }) 8 | // .populate({ 9 | // path: "likes", 10 | // select: "user username name photo " 11 | // }) 12 | // .populate("reply.like") 13 | // .populate("reply.profiles") 14 | // res.status(200).json({ 15 | // comment 16 | // }) 17 | // }) 18 | 19 | // exports.getAllComments = catchAsync(async (req, res, next) => { 20 | // const comments = await Comment.find().populate({ 21 | // path: "profile", 22 | // select: "user username name photo " 23 | // }) 24 | // .populate({ 25 | // path: "likes", 26 | // select: "user username name photo " 27 | // }) 28 | // .populate("reply.like") 29 | // .populate("reply.profiles") 30 | 31 | // // await Comment.deleteMany(); 32 | // res.status(200).json({ 33 | // data: comments.length, 34 | // comments 35 | // }) 36 | // }) 37 | 38 | -------------------------------------------------------------------------------- /routes/profile.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { protect } = require('../controllers/authController'); 4 | const { check } = require('express-validator'); 5 | const upload = require('../utils/multer'); 6 | const { getProfileId } = require('../controllers/authController'); 7 | const { 8 | updateProfile, 9 | getProfileById, 10 | getProfiles, 11 | uploadPhoto, 12 | deletePhoto, 13 | profileValidations, 14 | updatePhoto, 15 | search, 16 | follow, 17 | unfollow, 18 | userSettings, 19 | followRequest, 20 | getProfileByName, 21 | acceptRequest, 22 | getFollowRequest, 23 | getNotification 24 | } = require('../controllers/profileController'); 25 | 26 | router.route('/search').get(search); 27 | 28 | 29 | router 30 | .route('/') 31 | .get(getProfiles) 32 | .put([protect, profileValidations], updateProfile); 33 | 34 | router.route('/notifications').get(protect, getProfileId, getNotification) 35 | 36 | router 37 | .route('/profile-photo') 38 | .post(protect, upload.single('image'), uploadPhoto) 39 | .put(protect, upload.single('image'), updatePhoto) 40 | .delete(protect, deletePhoto); 41 | 42 | router.route('/:name').get(getProfileByName); 43 | 44 | router.route('/follow').post(protect, getProfileId, follow); 45 | router.route('/unfollow').post(protect, getProfileId, unfollow); 46 | 47 | router.route('/settings').put(protect, userSettings); 48 | 49 | router.route('/request-follow'); 50 | // .post(protect, getProfileId, followRequest) 51 | 52 | router 53 | .route('/request/accept-request') 54 | .get(protect, getFollowRequest) 55 | .post(protect, getProfileId, acceptRequest); 56 | module.exports = router; 57 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const dotenv = require('dotenv'); 3 | const Notification = require('./models/Notification'); 4 | 5 | dotenv.config({ 6 | path: './config.env', 7 | }); 8 | 9 | const app = require('./app'); 10 | 11 | mongoose 12 | .connect(process.env.DB, { 13 | useNewUrlParser: true, 14 | useCreateIndex: true, 15 | useFindAndModify: false, 16 | useUnifiedTopology: true, 17 | }) 18 | .then(() => { 19 | console.log('Db connected'); 20 | }) 21 | .catch((e) => { 22 | console.log(e, 'Failed to connect Db'); 23 | }); 24 | 25 | const server = app.listen(process.env.PORT || 3001, () => { 26 | console.log('Server started'); 27 | }); 28 | 29 | const io = require('socket.io')(server, { pingTimeout: 60000 }); 30 | 31 | io.on('connection', (socket) => { 32 | console.log('connected'); 33 | socket.on('authenticated', (userId) => { 34 | console.log(userId, 'auth'); 35 | socket.join(userId); 36 | }); 37 | socket.on('join room', (groupId) => { 38 | socket.join(groupId); 39 | }); 40 | socket.on('message', ({ groupId, message }) => { 41 | socket.broadcast.to(groupId).emit('message', { groupId, message }); 42 | }); 43 | socket.on('seen', (groupId) => { 44 | socket.broadcast.to(groupId).emit('seen', groupId); 45 | }); 46 | socket.on('render', (groupId) => { 47 | socket.broadcast.to(groupId).emit('render', groupId); 48 | }); 49 | socket.on('notification', async (msg) => { 50 | const notification = await Notification.create(msg); 51 | socket.broadcast.to(msg.to).emit('notification'); 52 | }); 53 | socket.on('disconnect', () => { 54 | console.log('disconnected'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const bodyParser = require('body-parser'); 4 | //const fileUpload = require("express-fileupload"); 5 | const cookieParser = require('cookie-parser'); 6 | const cors = require('cors'); 7 | 8 | const app = express(); 9 | 10 | const cloudinary = require('./utils/cloudinary'); 11 | 12 | const userRoute = require('./routes/user'); 13 | const profileRoute = require('./routes/profile'); 14 | const postRoute = require('./routes/Post'); 15 | const bookmarkRoute = require('./routes/bookmark'); 16 | const commentRoute = require('./routes/comment'); 17 | const groupRoute = require('./routes/group'); 18 | 19 | const AppError = require('./utils/appError'); 20 | const globalErrorHandler = require('./controllers/errorHandler/error'); 21 | 22 | //^morgan 23 | // if (process.env.NODE_ENV === 'development') { 24 | // app.use(morgan('dev')); 25 | // } 26 | 27 | app.use(express.json()); 28 | //app.use(fileUpload()) 29 | app.use(bodyParser.json()); 30 | app.use(bodyParser.urlencoded({ extended: true })); 31 | app.use(cookieParser()); 32 | app.use( 33 | cors({ 34 | origin: process.env.CLIENT, 35 | }) 36 | ); 37 | cloudinary(); 38 | 39 | //~routes 40 | app.use('/api/v1/users', userRoute); 41 | 42 | app.use('/api/v1/profile', profileRoute); 43 | app.use('/api/v1/post', postRoute); 44 | app.use('/api/v1/bookmark', bookmarkRoute); 45 | app.use('/api/v1/comment', commentRoute); 46 | app.use('/api/v1/group', groupRoute); 47 | 48 | app.all('*', (req, res, next) => { 49 | next(new AppError(`can't find ${req.originalUrl} on this server`, 404)); 50 | }); 51 | 52 | //^global error handler 53 | app.use(globalErrorHandler); 54 | 55 | module.exports = app; 56 | -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { check } = require('express-validator'); 4 | 5 | const { 6 | register, 7 | activateAccount, 8 | signUpValidations, 9 | login, 10 | getUsers, 11 | getUser, 12 | activate, 13 | myProfile, 14 | protect, 15 | signUpWeb, 16 | } = require('../controllers/authController'); 17 | 18 | router.route('/').get(getUsers); 19 | //outer.route("/").get(getUser) 20 | router.route('/me').get(protect, myProfile); 21 | 22 | router.post( 23 | '/register', 24 | signUpValidations, 25 | [check('email', 'Please enter valid email address').isEmail()], 26 | register 27 | ); 28 | 29 | router.post( 30 | '/activate', 31 | [ 32 | check('email', 'Please enter valid email address').isEmail(), 33 | 34 | check('password', 'Password should be minimum of 8 characters') 35 | .not() 36 | .isEmpty() 37 | .isLength({ min: 8 }), 38 | ], 39 | activate 40 | ); 41 | 42 | router.post( 43 | '/confirm/', 44 | [ 45 | check('Otp', 'Otp should be minimum of 6 number') 46 | .not() 47 | .isEmpty() 48 | .isInt({ min: 6, maz: 6 }), 49 | ], 50 | activateAccount 51 | ); 52 | router.post( 53 | '/login', 54 | [ 55 | check('email', 'Please enter valid email address').isEmail(), 56 | check('password', 'Password should be minimum of 8 characters') 57 | .not() 58 | .isEmpty(), 59 | ], 60 | login 61 | ); 62 | 63 | router.post( 64 | '/signup', 65 | [ 66 | check('email', 'Please enter valid email address').isEmail(), 67 | 68 | check('password', 'Password should be minimum of 8 characters') 69 | .not() 70 | .isEmpty() 71 | .isLength({ min: 8 }), 72 | ], 73 | signUpWeb 74 | ); 75 | 76 | module.exports = router; 77 | -------------------------------------------------------------------------------- /models/Comment.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const commentSchema = new mongoose.Schema( 4 | { 5 | user: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', 8 | }, 9 | profile: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'Profile', 12 | }, 13 | comment: { 14 | type: String, 15 | required: [true, 'Comment is required'], 16 | }, 17 | likes: [ 18 | { 19 | type: mongoose.Schema.Types.ObjectId, 20 | ref: 'Profile', 21 | }, 22 | ], 23 | post: { 24 | type: mongoose.Schema.Types.ObjectId, 25 | ref: 'Post', 26 | }, 27 | reply: [ 28 | { 29 | user: { 30 | type: mongoose.Schema.Types.ObjectId, 31 | ref: 'User', 32 | }, 33 | comment: { 34 | type: String, 35 | }, 36 | profiles: { 37 | type: mongoose.Schema.Types.ObjectId, 38 | ref: 'Profile', 39 | }, 40 | like: [ 41 | { 42 | type: mongoose.Schema.Types.ObjectId, 43 | ref: 'Profile', 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | { 50 | toJSON: { virtuals: true }, 51 | toObject: { virtuals: true }, 52 | } 53 | ); 54 | 55 | // commentSchema.virtual('replies', { 56 | // ref: 'Reply', 57 | // localField: '_id', 58 | // foreignField: 'comment' 59 | 60 | // }) 61 | 62 | commentSchema.pre(/^find/, function (next) { 63 | this.find() 64 | .populate('profile') 65 | .populate({ 66 | path: 'likes', 67 | select: 'username user name photo _id', 68 | }) 69 | .populate({ 70 | path: 'reply.like', 71 | select: 'username user name photo _id', 72 | }) 73 | .populate('reply.profiles'); 74 | 75 | next(); 76 | }); 77 | 78 | const Comment = mongoose.model('Comment', commentSchema); 79 | module.exports = Comment; 80 | -------------------------------------------------------------------------------- /controllers/commentController.js: -------------------------------------------------------------------------------- 1 | const Comment = require('../models/Comment'); 2 | const catchAsync = require('../utils/catchAsync'); 3 | const Post = require('../models/Post'); 4 | const AppError = require('../utils/appError'); 5 | //const getProfileId = require("../utils/profile"); 6 | 7 | //check post is in db 8 | exports.checkPost = async (req, res, next) => { 9 | const post = await Post.findById(req.params.id); 10 | 11 | if (!post) { 12 | return next(new AppError('Post not available', 400)); 13 | } 14 | next(); 15 | }; 16 | 17 | //comment to a post 18 | 19 | exports.addComment = catchAsync(async (req, res, next) => { 20 | const id = req.profile; 21 | 22 | const comment = await Comment.create({ 23 | user: req.user.id, 24 | profile: id, 25 | post: req.params.id, 26 | ...req.body, 27 | }); 28 | 29 | res.status(200).json({ 30 | status: 'success', 31 | comment, 32 | }); 33 | }); 34 | 35 | //like comment 36 | exports.likeComment = catchAsync(async (req, res, next) => { 37 | const id = req.profile; 38 | 39 | const comment = await Comment.findById(req.params.id); 40 | 41 | if (!comment) { 42 | return next(new AppError('Comment not found', 400)); 43 | } 44 | 45 | const checkLike = comment.likes.findIndex( 46 | (like) => like._id.toString() === id.toString() 47 | ); 48 | 49 | if (checkLike >= 0) { 50 | comment.likes.splice(checkLike, 1); 51 | await comment.save(); 52 | } else { 53 | comment.likes.unshift(id); 54 | await comment.save(); 55 | } 56 | 57 | res.status(200).json({ 58 | status: 'success', 59 | }); 60 | }); 61 | 62 | //delete comment 63 | 64 | exports.deleteComment = catchAsync(async (req, res, next) => { 65 | const comment = await Comment.findById(req.params.commentId); 66 | return comment.user.toString() === req.user.id.toString() 67 | ? (await comment.remove(), 68 | res.json({ 69 | status: 'success', 70 | })) 71 | : next(new AppError('You are not authorized to delete this comment', 401)); 72 | }); 73 | -------------------------------------------------------------------------------- /models/Post.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Profile = require('./Profile'); 3 | 4 | const postSchema = new mongoose.Schema( 5 | { 6 | user: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: 'Profile', 9 | }, 10 | createdAt: { 11 | type: Date, 12 | default: Date.now, 13 | }, 14 | profile: { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: 'Profile', 17 | }, 18 | caption: { 19 | type: String, 20 | trim: true, 21 | }, 22 | location: { 23 | type: String, 24 | }, 25 | hashtag: Array, 26 | likes: [ 27 | { 28 | type: mongoose.Schema.Types.ObjectId, 29 | ref: 'Profile', 30 | }, 31 | ], 32 | image: Array, 33 | comment: { 34 | type: mongoose.Schema.Types.ObjectId, 35 | ref: 'Comment', 36 | }, 37 | }, 38 | { 39 | toJSON: { virtuals: true }, 40 | toObject: { virtuals: true }, 41 | } 42 | ); 43 | 44 | postSchema.set('toObject', { virtuals: true }); 45 | postSchema.set('toJSON', { virtuals: true }); 46 | 47 | postSchema.virtual('commentsPost', { 48 | ref: 'Comment', 49 | localField: '_id', 50 | foreignField: 'post', 51 | }); 52 | 53 | postSchema.pre('save', function (next) { 54 | let caption = this.caption.replace(/\s/g, ''); 55 | console.log(caption); 56 | let hashTagIndex = caption.indexOf('#'); 57 | if (hashTagIndex === -1) { 58 | this.hashtag = undefined; 59 | return next(); 60 | } 61 | let hashTagSplice = caption.slice(hashTagIndex); 62 | //let res= hashTagSplice.replace(/#/, '').split('#'); 63 | 64 | this.hashtag = hashTagSplice.replace(/#/, '').split('#'); 65 | next(); 66 | }); 67 | 68 | // postSchema.pre("save", async function (next) { 69 | // const { _id } = await Profile.findOne({ user: this.user }); 70 | // this.profile = _id; 71 | // next(); 72 | // }) 73 | 74 | postSchema.methods.getProfileId = async function (id) { 75 | const { _id } = await Profile.findOne({ user: id }); 76 | return _id; 77 | }; 78 | 79 | //Todo 80 | postSchema.pre(/^find/, function (next) { 81 | this.find().populate('commentsPost'); 82 | 83 | next(); 84 | }); 85 | 86 | const Post = mongoose.model('Post', postSchema); 87 | 88 | module.exports = Post; 89 | -------------------------------------------------------------------------------- /controllers/errorHandler/error.js: -------------------------------------------------------------------------------- 1 | const AppError = require('../../utils/appError'); 2 | 3 | const handleValidationError = (err) => { 4 | const errors = Object.values(err.errors).map((el) => el.message); 5 | 6 | const message = `Invalid input data ${errors.join('.')}`; 7 | return new AppError(message, 400); 8 | }; 9 | 10 | const handleCastErrorDb = (err) => { 11 | const message = 12 | `Invalid ${err.path} : ${err.value}.` + 13 | 'The requested data is not available'; 14 | return new AppError(message, 400); 15 | }; 16 | 17 | const handleDuplicateFieldErrorDb = (err) => { 18 | const keyField = Object.keys(err.keyValue)[0]; 19 | return new AppError(`Please use different ${keyField}`, 400); 20 | }; 21 | 22 | const handleJwtTokenExpire = () => { 23 | return new AppError('Token has been expired!.Please register again', 401); 24 | }; 25 | 26 | const handleMulterError = () => { 27 | return new AppError('Please select only one image', 400); 28 | }; 29 | 30 | const handleWebTokenError = () => { 31 | return new AppError('Invalid Token.Please register again', 400); 32 | }; 33 | const sendError = (err, res) => { 34 | if (err.isOperational) { 35 | res.status(err.statusCode).json({ 36 | status: err.status, 37 | message: err.message, 38 | }); 39 | } else { 40 | res.status(500).json({ 41 | status: 'error', 42 | message: 'Something went wrong', 43 | }); 44 | } 45 | }; 46 | 47 | module.exports = (err, req, res, next) => { 48 | console.log(err); 49 | err.statusCode = err.statusCode || 500; 50 | 51 | err.status = err.status || 'error'; 52 | 53 | let error = { ...err, message: err.message }; 54 | // console.log(error.name, 5) 55 | 56 | if (error.name === 'ValidationError') { 57 | error = handleValidationError(error); 58 | } 59 | if (error.name === 'TokenExpiredError') { 60 | error = handleJwtTokenExpire(); 61 | } 62 | if (error.code === 11000) { 63 | error = handleDuplicateFieldErrorDb(error); 64 | } 65 | if (error.name === 'CastError' || error.kind === 'ObjectId') 66 | error = handleCastErrorDb(error); 67 | 68 | if (error.name === 'MulterError' && error.code === 'LIMIT_UNEXPECTED_FILE') 69 | error = handleMulterError(); 70 | 71 | if (error.name === 'JsonWebTokenError') error = handleWebTokenError(error); 72 | sendError(error, res); 73 | }; 74 | -------------------------------------------------------------------------------- /models/Profile.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const validator = require('validator'); 3 | 4 | const { isEmail } = validator; 5 | 6 | //Todo close friend,highlights 7 | const profileSchema = new mongoose.Schema( 8 | { 9 | user: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'User', 12 | }, 13 | bio: { 14 | type: String, 15 | trim: true, 16 | }, 17 | email: { 18 | type: String, 19 | trim: true, 20 | unique: true, 21 | validate: [isEmail, 'invalid email'], 22 | required: [true, 'email is required'], 23 | }, 24 | accountType: { 25 | type: String, 26 | enum: ['public', 'private'], 27 | default: 'public', 28 | }, 29 | website: { 30 | type: String, 31 | }, 32 | name: { 33 | type: String, 34 | }, 35 | username: { 36 | type: String, 37 | required: [true, 'username is required'], 38 | unique: true, 39 | }, 40 | gender: { 41 | type: String, 42 | select: false, 43 | }, 44 | birthday: { 45 | type: Date, 46 | select: false, 47 | }, 48 | closeFriends: { 49 | type: mongoose.Schema.Types.ObjectId, 50 | ref: 'User', 51 | select: false, 52 | }, 53 | photo: { 54 | type: Object, 55 | default: 56 | 'https://scontent-atl3-1.cdninstagram.com/v/t51.2885-19/44884218_345707102882519_2446069589734326272_n.jpg?_nc_ht=scontent-atl3-1.cdninstagram.com&_nc_ohc=MVEG8QTyVK4AX-E1QAd&oh=a2338939f84d05cff0d598c29ce23a6a&oe=5FB5D50F&ig_cache_key=YW5vbnltb3VzX3Byb2ZpbGVfcGlj.2', 57 | }, 58 | followers: { 59 | type: Map, 60 | of: { 61 | user: { 62 | type: mongoose.Schema.Types.ObjectId, 63 | ref: 'Profile', 64 | }, 65 | }, 66 | default: {}, 67 | }, 68 | 69 | following: { 70 | type: Map, 71 | of: { 72 | user: { 73 | type: mongoose.Schema.Types.ObjectId, 74 | ref: 'Profile', 75 | }, 76 | }, 77 | default: {}, 78 | }, 79 | 80 | requests: [ 81 | { 82 | type: mongoose.Schema.Types.ObjectId, 83 | ref: 'Profile', 84 | select: false, 85 | }, 86 | ], 87 | }, 88 | { 89 | toJSON: { virtuals: true }, 90 | toObject: { virtuals: true }, 91 | } 92 | ); 93 | 94 | profileSchema.set('toObject', { virtuals: true }); 95 | profileSchema.set('toJSON', { virtuals: true }); 96 | 97 | profileSchema.virtual('posts', { 98 | ref: 'Post', 99 | localField: '_id', 100 | foreignField: 'profile', 101 | }); 102 | 103 | profileSchema.pre(/^find/, function (next) { 104 | this.find().populate('posts'); 105 | 106 | next(); 107 | }); 108 | 109 | const Profile = mongoose.model('Profile', profileSchema); 110 | module.exports = Profile; 111 | -------------------------------------------------------------------------------- /controllers/postController.js: -------------------------------------------------------------------------------- 1 | const Post = require('../models/Post'); 2 | const getProfileId = require('../utils/profile'); 3 | const cloudinary = require('cloudinary').v2; 4 | const catchAsync = require('../utils/catchAsync'); 5 | const AppError = require('../utils/appError'); 6 | const fileUpload = require('../utils/fileUpload'); 7 | const Comment = require('../models/Comment'); 8 | const Notification = require('../models/Notification'); 9 | 10 | const deleteImageCloudinary = async (id) => { 11 | const { image } = await Post.findById(id); 12 | 13 | await image.map((img) => cloudinary.uploader.destroy(img.cloudinary_id)); 14 | }; 15 | 16 | exports.createPost = catchAsync(async (req, res, next) => { 17 | const files = req.files; 18 | console.log(req.files); 19 | let image = []; 20 | for (const file of files) { 21 | const newPath = await fileUpload(file); 22 | 23 | image.push({ 24 | cloudinary_id: newPath.public_id, 25 | url: newPath.secure_url, 26 | }); 27 | } 28 | 29 | const post = await Post.create({ 30 | user: req.user.id, 31 | image: image, 32 | ...req.body, 33 | profile: req.profile, 34 | }); 35 | 36 | res.status(201).json({ 37 | status: 'success', 38 | post, 39 | }); 40 | }); 41 | 42 | exports.getAllPost = catchAsync(async (req, res, next) => { 43 | const posts = await Post.find({}) 44 | .sort({ createdAt: 'descending' }) 45 | .populate('profile') 46 | .limit(20); 47 | // const populatedPost = await posts.populate('profile').execPopulate(); 48 | res.status(200).json({ 49 | status: 'success', 50 | data: posts.length, 51 | posts, 52 | }); 53 | }); 54 | 55 | exports.getPostById = catchAsync(async (req, res, next) => { 56 | const post = await Post.findById(req.params.id).populate({ 57 | path: 'profile', 58 | select: '-bio -website -user -_v', 59 | }); 60 | 61 | if (!post) { 62 | return next(new AppError('Post not found', 400)); 63 | } 64 | 65 | res.status(200).json({ 66 | status: 'success', 67 | post, 68 | }); 69 | }); 70 | 71 | exports.deletePost = catchAsync(async (req, res, next) => { 72 | //const post = await Post.deleteOne({ _id: req.params.id }); 73 | const post = await Post.findById(req.params.id); 74 | if (!post) { 75 | return next(new AppError('Post not found', 400)); 76 | } 77 | // console.log(post, post.user.toString() === req.user.id) 78 | if (post.user.toString() !== req.user.id) { 79 | return next( 80 | new AppError('You are not authorized to delete this post', 401) 81 | ); 82 | } 83 | 84 | post.commentsPost.length && 85 | (await Comment.findByIdAndDelete(post.commentsPost[0]._id)); 86 | 87 | await deleteImageCloudinary(req.params.id); 88 | await post.remove(); 89 | 90 | res.status(200).json({ 91 | message: 'deleted', 92 | }); 93 | }); 94 | 95 | exports.likePost = catchAsync(async (req, res, next) => { 96 | const post = await Post.findById(req.params.id).populate('profile'); 97 | 98 | if (!post) { 99 | return next(new AppError('Post not found', 400)); 100 | } 101 | const id = await post.getProfileId(req.user.id); 102 | 103 | if (post.likes.includes(id)) { 104 | const index = post.likes.indexOf(id); 105 | post.likes.splice(index, 1); 106 | await post.save((err) => { 107 | console.log(err); 108 | }); 109 | await Notification.deleteMany({ 110 | to: post.profile._id, 111 | user: id, 112 | type: 'Like', 113 | }); 114 | } else { 115 | post.likes.push(id); 116 | await post.save(); 117 | } 118 | 119 | res.status(200).json({ 120 | status: 'success', 121 | post, 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /controllers/group.js: -------------------------------------------------------------------------------- 1 | const catchAsync = require('../utils/catchAsync'); 2 | const AppError = require('../utils/appError'); 3 | const Group = require('../models/Group'); 4 | const Message = require('../models/Messages'); 5 | 6 | exports.createGroup = catchAsync(async (req, res, next) => { 7 | const { userId } = req.body; 8 | if (!userId) { 9 | return next(new AppError('Please select user', 400)); 10 | } 11 | const group = await Group.findOne({ 12 | $and: [{ users: { $in: req.profile } }, { users: { $in: userId } }], 13 | }); 14 | if (group) { 15 | return next(new AppError('Group already exist', 400)); 16 | } 17 | const createdGroup = await Group.create({ 18 | users: [req.profile, userId], 19 | createdBy: req.profile, 20 | groupType: 'public', 21 | }); 22 | const newGroup = await createdGroup 23 | .populate({ 24 | path: 'users', 25 | select: 'username user name photo _id', 26 | }) 27 | .execPopulate(); 28 | return res.status(201).json({ 29 | status: 'success', 30 | createdGroup: newGroup, 31 | }); 32 | }); 33 | 34 | exports.getGroup = catchAsync(async (req, res, next) => { 35 | const group = await Group.findOne({ 36 | _id: req.params.groupId, 37 | users: { 38 | $in: [req.profile], 39 | }, 40 | }); 41 | return res.status(200).json({ 42 | status: 'success', 43 | group, 44 | }); 45 | }); 46 | 47 | exports.getUserGroup = catchAsync(async (req, res, next) => { 48 | const groups = await Group.find({ 49 | users: { 50 | $in: [req.profile], 51 | }, 52 | }); 53 | 54 | return res.status(200).json({ 55 | status: 'success', 56 | groups, 57 | }); 58 | }); 59 | 60 | exports.createMessage = catchAsync(async (req, res, next) => { 61 | const { message, to } = req.body; 62 | if (!message) { 63 | return next(new AppError('Message should not empty', 400)); 64 | } 65 | const newMessage = await Message.create({ 66 | message, 67 | sender: req.profile, 68 | groupId: req.params.groupId, 69 | to, 70 | }); 71 | 72 | const populatedMessage = await newMessage 73 | .populate({ 74 | path: 'sender', 75 | select: 'username user name photo _id', 76 | }) 77 | .execPopulate(); 78 | return res.status(201).json({ 79 | status: 'success', 80 | message: populatedMessage, 81 | }); 82 | }); 83 | 84 | exports.getGroupMessage = catchAsync(async (req, res, next) => { 85 | const isMember = await Group.find({ 86 | _id: req.params.groupId, 87 | users: { 88 | $in: [req.profile], 89 | }, 90 | }); 91 | if (!isMember) { 92 | return next(new AppError('You are not authorized to view messages', 400)); 93 | } 94 | const messages = await Message.find({ groupId: req.params.groupId }); 95 | return res.status(200).json({ 96 | status: 'success', 97 | messages, 98 | }); 99 | }); 100 | 101 | exports.setSeenMessages = catchAsync(async (req, res, next) => { 102 | const seen = await Message.updateMany( 103 | { 104 | groupId: req.params.groupId, 105 | to: req.profile, 106 | seen: { $nin: [req.profile] }, 107 | }, 108 | { 109 | $push: { seen: req.profile }, 110 | }, 111 | { 112 | new: true, 113 | } 114 | ); 115 | return res.status(200).json({ 116 | status: 'success', 117 | }); 118 | }); 119 | 120 | exports.getNotifications = catchAsync(async (req, res, next) => { 121 | const notifications = await Message.aggregate([ 122 | { $match: { to: req.profile, seen: { $nin: [req.profile] } } }, 123 | { 124 | $group: { 125 | _id: '$groupId', 126 | message: { $push: '$$ROOT' }, 127 | }, 128 | }, 129 | ]); 130 | return res.status(200).json({ 131 | notifications, 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /controllers/replyController.js: -------------------------------------------------------------------------------- 1 | 2 | const catchAsync = require("../utils/catchAsync") 3 | const AppError = require("../utils/appError") 4 | //const getProfileId = require("../utils/profile"); 5 | const Comment = require("../models/Comment"); 6 | 7 | //check comment is in db 8 | exports.checkComment = async (req, res, next) => { 9 | const comment = await Comment.findById(req.params.id); 10 | if (!comment) { 11 | return next(new AppError("Comment not found", 400)) 12 | } 13 | next(); 14 | } 15 | 16 | //reply to a comment 17 | exports.createComment = catchAsync(async (req, res, next) => { 18 | const id = req.profile 19 | const comment = await Comment.findById(req.params.id); 20 | 21 | if (!comment) return next 22 | ( 23 | new AppError("Comment not found", 400) 24 | ) 25 | 26 | comment.reply.unshift({ 27 | profiles: id, 28 | reply: req.body.reply, 29 | user: req.user.id 30 | }) 31 | await comment.save(); 32 | 33 | res.status(201).json({ 34 | status: "success", 35 | comment 36 | }) 37 | }) 38 | 39 | //like/unlike a replied comment 40 | exports.likeReply = catchAsync(async (req, res, next) => { 41 | const id = req.profile; 42 | 43 | const comment = await Comment.findOne({ 44 | _id: req.params.id, 45 | reply: 46 | { 47 | $elemMatch: 48 | { _id: req.params.replyId } 49 | } 50 | }) 51 | // console.log(comment.reply[0].like) 52 | if (!comment) { 53 | return next(new AppError("Reply not found", 400)) 54 | } 55 | 56 | const checkLike = comment.reply[0].like.findIndex(like => like._id.toString() === id.toString()) 57 | 58 | if (checkLike >= 0) { 59 | comment.reply[0].like.splice(checkLike, 1) 60 | await comment.save(); 61 | } else { 62 | comment.reply[0].like.unshift(id); 63 | await comment.save(); 64 | } 65 | 66 | res.status(200).json({ 67 | status: "success", 68 | comment 69 | }) 70 | 71 | 72 | }) 73 | 74 | //delete reply 75 | exports.deleteReply = catchAsync(async (req, res, next) => { 76 | 77 | const post = await Comment.findOne({ 78 | _id: req.params.id 79 | , reply: { 80 | $elemMatch: 81 | { _id: req.params.replyId } 82 | } 83 | }) 84 | 85 | if (!post) return next 86 | ( 87 | new AppError("Cannot find comment", 400) 88 | ) 89 | 90 | return post.reply[0].user.toString() === req.user.id.toString() ? 91 | ( 92 | post.reply.splice(0, 1), 93 | await post.save(), 94 | res.json({ 95 | status: "success" 96 | }) 97 | ) : ( 98 | next(new AppError("You are not authorized to delete this reply comment", 401)) 99 | ) 100 | 101 | }) 102 | 103 | 104 | 105 | // if (comment.reply[0].like.includes(id)) { 106 | // const replyIndex = comment.reply[0].like.indexOf(id); 107 | // comment.reply[0].like.splice(replyIndex, 1) 108 | // await comment.save(); 109 | 110 | // } else { 111 | // console.log(true) 112 | // comment.reply[0].like.unshift(id) 113 | // await comment.save() 114 | // } 115 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const catchAsync = require('../utils/catchAsync'); 3 | const { validationResult } = require('express-validator'); 4 | const AppError = require('../utils/appError'); 5 | const jwt = require('jsonwebtoken'); 6 | const sendEmail = require('../utils/sendMail'); 7 | const { promisify } = require('util'); 8 | const Profile = require('../models/Profile'); 9 | const Otp = require('../models/Otp'); 10 | const Notification = require('../models/Notification'); 11 | 12 | //^middlewares 13 | exports.signUpValidations = (req, res, next) => { 14 | if ( 15 | req.body.passwordChangedAt || 16 | req.body.passwordResetToken || 17 | req.body.passwordResetExpires 18 | ) 19 | return next( 20 | new AppError('This is invalid route for changing password', 400) 21 | ); 22 | 23 | next(); 24 | }; 25 | 26 | //Authentication 27 | exports.protect = async (req, res, next) => { 28 | let token; 29 | if ( 30 | req.headers.authorization && 31 | req.headers.authorization.startsWith('Bearer') 32 | ) { 33 | token = req.headers.authorization.split(' ')[1]; 34 | } 35 | console.log(2, token); 36 | if (!token) { 37 | return next( 38 | new AppError('You are not logged in.Please login to get access', 401) 39 | ); 40 | } 41 | let decoded; 42 | try { 43 | decoded = await promisify(jwt.verify)( 44 | token, 45 | process.env.JWT_LOGIN_TOKEN 46 | ); 47 | } catch (e) { 48 | return next(new AppError('Please login to get access', 401)); 49 | } 50 | 51 | const freshUser = await User.findById(decoded.id); 52 | 53 | if (!freshUser) { 54 | return next(new AppError('User does not exist', 401)); 55 | } 56 | 57 | req.user = freshUser; 58 | 59 | next(); 60 | }; 61 | 62 | //create profile after login 63 | const createProfile = async (id, email) => { 64 | await Profile.updateMany({}, { followers: [], following:{}}) 65 | 66 | const profile = await Profile.findOne({ 67 | user: id, 68 | }); 69 | if (!profile) { 70 | const name = email.split('@')[0]; 71 | const profile = await Profile.create({ 72 | user: id, 73 | username: name, 74 | name, 75 | email, 76 | }); 77 | // await User.findByIdAndUpdate(id, { profile: _id }) 78 | return profile; 79 | } 80 | return profile; 81 | }; 82 | 83 | //~Generate jwt token 84 | 85 | const generateToken = (options, secret, expireTime) => { 86 | return jwt.sign(options, secret, { 87 | expiresIn: expireTime, 88 | }); 89 | }; 90 | 91 | //register 92 | exports.register = catchAsync(async (req, res, next) => { 93 | const errors = validationResult(req); 94 | 95 | //^Checking validation errors 96 | if (!errors.isEmpty()) { 97 | return next(new AppError(errors.array()[0].msg, 400)); 98 | } 99 | if (req.body.isAuthenticated) { 100 | return next(new AppError('Not Authorized', 400)); 101 | } 102 | //^Generating token 103 | const user = await User.findOne({ email: req.body.email }); 104 | if (user) { 105 | return next(new AppError('Email already taken', 400)); 106 | } 107 | 108 | let otpForEmailVerification = parseInt(Math.random() * 1000000); 109 | console.log(otpForEmailVerification); 110 | await Otp.create({ 111 | email: req.body.email, 112 | OtpInsta: otpForEmailVerification, 113 | }); 114 | 115 | const message = `Your verification code for Instagram-clone application 116 | is ${otpForEmailVerification}. 117 | `; 118 | try { 119 | await sendEmail({ 120 | email: req.body.email, 121 | subject: 'Email Verification for Instagram clone', 122 | message, 123 | }); 124 | 125 | res.status(200).json({ 126 | status: 'success', 127 | message: 'Token sent to email', 128 | }); 129 | } catch (e) { 130 | console.log(e); 131 | 132 | return next(new AppError('Error sending email.Try again later' + e, 500)); 133 | } 134 | }); 135 | 136 | //Activate account 137 | exports.activateAccount = catchAsync(async (req, res, next) => { 138 | const errors = validationResult(req); 139 | 140 | //^Checking validation errors 141 | if (!errors.isEmpty()) { 142 | return next(new AppError(errors.array()[0].msg, 400)); 143 | } 144 | 145 | const otp = req.body.Otp * 1; 146 | 147 | const checkOtpIsValid = await Otp.findOne({ 148 | OtpInsta: otp, 149 | isAuthenticated: false, 150 | }); 151 | if (!checkOtpIsValid) { 152 | return next(new AppError('Invalid Otp', 400)); 153 | } 154 | checkOtpIsValid.isAuthenticated = true; 155 | await checkOtpIsValid.save(); 156 | return res.status(200).json({ 157 | status: 'success', 158 | data: checkOtpIsValid, 159 | }); 160 | // const { 161 | // email, 162 | // password 163 | // } = await promisify(jwt.verify)(token, process.env.JWT_SIGNIN_TOKEN) 164 | 165 | // await User.create({ 166 | // email, 167 | // password, 168 | 169 | // }) 170 | 171 | // return res.status(201).json({ 172 | // "status": "success", 173 | // "data": "Verified.Please login" 174 | // }) 175 | }); 176 | 177 | exports.activate = catchAsync(async (req, res, next) => { 178 | const errors = validationResult(req); 179 | 180 | //^Checking validation errors 181 | if (!errors.isEmpty()) { 182 | return next(new AppError(errors.array()[0].msg, 400)); 183 | } 184 | const isEmailVerified = await Otp.findOne({ 185 | email: req.body.email, 186 | isAuthenticated: true, 187 | }); 188 | console.log(isEmailVerified); 189 | if (!isEmailVerified) { 190 | return next(new AppError('Email is not verified', 400)); 191 | } 192 | await Otp.deleteMany({ 193 | email: req.body.email, 194 | }); 195 | 196 | await User.create({ 197 | email: req.body.email, 198 | password: req.body.password, 199 | }); 200 | 201 | return res.status(201).json({ 202 | status: 'success', 203 | data: 'Verified.Please login', 204 | }); 205 | }); 206 | 207 | //login 208 | exports.login = catchAsync(async (req, res, next) => { 209 | const errors = validationResult(req); 210 | //^Checking validation errors 211 | if (!errors.isEmpty()) { 212 | return next(new AppError(errors.array()[0].msg, 400)); 213 | } 214 | const user = await User.findOne({ 215 | email: req.body.email, 216 | }).select('+password'); 217 | if ( 218 | !user || 219 | !(await user.comparePassword(req.body.password, user.password)) 220 | ) { 221 | return next(new AppError('Invalid email or password', 400)); 222 | } 223 | 224 | const profile = await createProfile(user._id, user.email); 225 | const token = generateToken( 226 | { 227 | id: user._id, 228 | }, 229 | process.env.JWT_LOGIN_TOKEN, 230 | '1d' 231 | ); 232 | 233 | res.status(200).json({ 234 | status: 'success', 235 | data: { 236 | token, 237 | profile, 238 | }, 239 | }); 240 | }); 241 | 242 | exports.getUsers = catchAsync(async (req, res, next) => { 243 | const users = await User.find(); 244 | 245 | res.status(200).json({ 246 | data: users.length, 247 | users, 248 | }); 249 | }); 250 | 251 | exports.getUser = catchAsync(async (req, res, next) => { 252 | const users = await User.find({}); 253 | 254 | res.status(200).json({ 255 | data: users.length, 256 | users, 257 | }); 258 | }); 259 | 260 | exports.getProfileId = async (req, res, next) => { 261 | const id = await Profile.findOne({ user: req.user.id }); 262 | 263 | req.profile = id._id; 264 | 265 | next(); 266 | }; 267 | 268 | exports.myProfile = catchAsync(async (req, res, next) => { 269 | const profile = await Profile.findOne({ user: req.user.id }); 270 | if (!profile) next(new AppError('No user found', 400)); 271 | const notification = await Notification.find({to: profile._id, seen: false}) 272 | res.status(200).json({ 273 | profile, 274 | notification: notification.length 275 | }); 276 | }); 277 | 278 | exports.signUpWeb = catchAsync(async (req, res, next) => { 279 | const errors = validationResult(req); 280 | 281 | //^Checking validation errors 282 | if (!errors.isEmpty()) { 283 | return next(new AppError(errors.array()[0].msg, 400)); 284 | } 285 | 286 | const user = await User.findOne({ email: req.body.email }); 287 | if (user) { 288 | return next(new AppError('Email already taken', 400)); 289 | } 290 | await User.create({ 291 | email: req.body.email, 292 | password: req.body.password, 293 | }); 294 | 295 | return res.status(201).json({ 296 | status: 'success', 297 | data: 'Verified.Please login', 298 | }); 299 | }); 300 | -------------------------------------------------------------------------------- /controllers/profileController.js: -------------------------------------------------------------------------------- 1 | const Profile = require('../models/Profile'); 2 | const catchAsync = require('../utils/catchAsync'); 3 | const { validationResult } = require('express-validator'); 4 | const AppError = require('../utils/appError'); 5 | const fileUpload = require('../utils/fileUpload'); 6 | const cloudinary = require('cloudinary').v2; 7 | const Post = require('../models/Post'); 8 | const Message = require('../models/Messages'); 9 | const Notification = require('../models/Notification'); 10 | 11 | exports.profileValidations = (req, res, next) => { 12 | if (req.body.accountType) 13 | return next( 14 | new AppError('This is invalid route for changing account type', 400) 15 | ); 16 | 17 | next(); 18 | }; 19 | 20 | const deletePhotoCloudinary = async (id) => 21 | await cloudinary.uploader.destroy(id); 22 | 23 | const uploadPhotoCloudinary = async (file) => { 24 | // if (file.mimetype.slice(0, 5) === 'video') { 25 | // return next(new AppError('Please upload valid image', 400)); 26 | // } 27 | const { public_id, secure_url } = await fileUpload(file); 28 | 29 | return { 30 | public_id, 31 | secure_url, 32 | }; 33 | }; 34 | 35 | exports.getProfiles = catchAsync(async (req, res, next) => { 36 | const profiles = await Profile.find({}) 37 | .limit(2); 38 | if (!profiles) { 39 | return next(new AppError('No profile found', 400)); 40 | } 41 | res.status(200).json({ 42 | status: 'success', 43 | docs: profiles.length, 44 | data: { 45 | profiles, 46 | }, 47 | }); 48 | }); 49 | 50 | exports.updateProfile = catchAsync(async (req, res, next) => { 51 | const updatedProfile = await Profile.findOneAndUpdate( 52 | { user: req.user.id }, 53 | req.body, 54 | { 55 | new: true, 56 | runValidators: true, 57 | } 58 | ); 59 | return res.status(200).json({ 60 | status: 'success', 61 | data: { 62 | profile: updatedProfile, 63 | }, 64 | }); 65 | }); 66 | 67 | exports.getProfileById = catchAsync(async (req, res, next) => { 68 | const profile = await Profile.findById(req.params.userId); 69 | 70 | if (!profile) { 71 | return next(new AppError('No profile found', 400)); 72 | } 73 | 74 | res.status(200).json({ 75 | status: 'success', 76 | data: { 77 | profile, 78 | }, 79 | }); 80 | }); 81 | 82 | exports.getProfileByName = catchAsync(async (req, res, next) => { 83 | const profile = await Profile.findOne({ username: req.params.name }); 84 | 85 | if (!profile) { 86 | return next(new AppError('No profile found', 400)); 87 | } 88 | 89 | res.status(200).json({ 90 | status: 'success', 91 | data: { 92 | profile, 93 | }, 94 | }); 95 | }); 96 | 97 | exports.uploadPhoto = catchAsync(async (req, res, next) => { 98 | const photo = await uploadPhotoCloudinary(req.file); 99 | 100 | const profile = await Profile.findOneAndUpdate( 101 | { user: req.user.id }, 102 | { photo }, 103 | { new: true } 104 | ); 105 | res.status(200).json({ 106 | success: 'true', 107 | profile, 108 | }); 109 | }); 110 | 111 | exports.messagePhoto = catchAsync(async (req, res, next) => { 112 | const photo = await uploadPhotoCloudinary(req.file); 113 | if (!message) { 114 | return next(new AppError('Message should not empty', 400)); 115 | } 116 | const newMessage = await Message.create({ 117 | message: photo, 118 | sender: req.profile, 119 | groupId: req.params.groupId, 120 | to: req.params.to, 121 | }); 122 | 123 | const populatedMessage = await newMessage 124 | .populate({ 125 | path: 'sender', 126 | select: 'username user name photo _id', 127 | }) 128 | .execPopulate(); 129 | return res.status(201).json({ 130 | status: 'success', 131 | message: populatedMessage, 132 | }); 133 | }); 134 | exports.updatePhoto = catchAsync(async (req, res, next) => { 135 | const profile = await Profile.findOne({ user: req.user.id }); 136 | await deletePhotoCloudinary(profile.photo.public_id); 137 | 138 | const photo = await uploadPhotoCloudinary(req.file); 139 | profile.photo = photo; 140 | profile.save(); 141 | res.status(200).json({ 142 | status: 'success', 143 | profile, 144 | }); 145 | }); 146 | 147 | exports.deletePhoto = catchAsync(async (req, res, next) => { 148 | const profile = await Profile.findOne({ user: req.user.id }); 149 | 150 | await deletePhotoCloudinary(profile.photo.public_id); 151 | profile.photo = process.env.DEFAULT_PROFILE_PHOTO; 152 | await profile.save(); 153 | 154 | res.status(200).json({ 155 | status: 'success', 156 | profile, 157 | }); 158 | }); 159 | 160 | const searchHashtag = async (query) => { 161 | const queryField = new RegExp('^' + query); 162 | const result = await Post.find({ hashtag: { $in: [queryField] } }); 163 | return result; 164 | }; 165 | 166 | exports.search = catchAsync(async (req, res, next) => { 167 | if (req.query.find[0] === '#') { 168 | const hashtag = await searchHashtag(req.query.find.slice(1)); 169 | return res.status(200).json({ 170 | status: 'success', 171 | data: hashtag.length, 172 | hashtag, 173 | }); 174 | } 175 | const queryField = new RegExp('^' + req.query.find); 176 | 177 | const result = await Profile.find({ 178 | username: { $regex: queryField }, 179 | }); 180 | 181 | res.status(200).json({ 182 | status: 'success', 183 | data: result.length, 184 | users: result, 185 | }); 186 | }); 187 | 188 | const followRequest = async (id, profile) => { 189 | const request = await Profile.findByIdAndUpdate( 190 | id, 191 | { 192 | $push: { requests: profile }, 193 | }, 194 | { new: true } 195 | ); 196 | if (!request) return next(new AppError('User not found', 400)); 197 | 198 | return request; 199 | }; 200 | 201 | exports.follow = catchAsync(async (req, res, next) => { 202 | req.profile.toString() === req.body.id.toString() && 203 | next(new AppError('You cant follow yourself', 400)); 204 | 205 | if (req.body.accountType === 'private') { 206 | const request = followRequest(req.body.id, req.profile); 207 | 208 | return res.status(200).json({ 209 | data: request, 210 | }); 211 | } 212 | 213 | const following = await Profile.findOne({ user: req.user.id }); 214 | 215 | await following.following.set(req.body.name, { 216 | user: req.body.id, 217 | }); 218 | await following.save(); 219 | 220 | const user = await Profile.findById(req.body.id); 221 | await user.followers.set(following.username, { 222 | user: req.user.id, 223 | }); 224 | await user.save(); 225 | res.status(200).json({ 226 | status: 'success', 227 | following, 228 | user: user.followers, 229 | }); 230 | }); 231 | //Todo unfollow 232 | exports.unfollow = catchAsync(async (req, res, next) => { 233 | req.user.id === req.params.id && 234 | next(new AppError('You cant unfollow yourself', 400)); 235 | const following = await Profile.findOne({ user: req.user.id }); 236 | if (!(await following.following.get(req.body.name))) { 237 | return res.status(200).json({ 238 | status: 'success', 239 | following, 240 | }); 241 | } 242 | await following.following.delete(req.body.name); 243 | await following.save(); 244 | 245 | const user = await Profile.findById(req.body.id); 246 | 247 | await user.followers.delete(following.username); 248 | await user.save(); 249 | 250 | await Notification.deleteMany({ 251 | to: req.body.id, 252 | user: following._id, 253 | type: 'Follow', 254 | }); 255 | 256 | res.status(200).json({ 257 | status: 'success', 258 | 259 | following, 260 | user: user.followers, 261 | }); 262 | }); 263 | 264 | exports.userSettings = catchAsync(async (req, res, next) => { 265 | const user = await Profile.findOneAndUpdate( 266 | { user: req.user.id }, 267 | { accountType: req.body.account }, 268 | { new: true } 269 | ); 270 | 271 | if (!user) return next(new AppError('No user found', 400)); 272 | 273 | res.status(200).json({ 274 | status: 'success', 275 | user, 276 | }); 277 | }); 278 | 279 | exports.getFollowRequest = catchAsync(async (req, res, next) => { 280 | const user = await Profile.findOne({ user: req.user.id }).select( 281 | '+accountType' 282 | ); 283 | if (!user || user.accountType === 'public') { 284 | return next(new AppError('Not authorized to access this route', 400)); 285 | } 286 | if (user.requests.length === 0) 287 | return res.status(200).json({ data: 'No follow requests' }); 288 | res.status(200).json({ 289 | data: user.requests, 290 | }); 291 | }); 292 | 293 | exports.acceptRequest = catchAsync(async (req, res, next) => { 294 | const user = await Profile.findOneAndUpdate( 295 | { user: req.user.id }, 296 | { $pull: { requests: req.body.id } } 297 | ); 298 | user.followers.unshift(req.body.id); 299 | await user.save(); 300 | 301 | const updateUser = await Profile.findByIdAndUpdate(req.body.id, { 302 | $push: { following: req.profile }, 303 | }); 304 | res.status(200).json({ 305 | data: updateUser, 306 | }); 307 | }); 308 | 309 | exports.getNotification = catchAsync(async (req, res, next) => { 310 | const notifications = await Notification.find({ to: req.profile }) 311 | .sort({ createdAt: 'descending' }) 312 | .exec(); 313 | await Notification.updateMany( 314 | { 315 | to: req.profile, 316 | seen: false, 317 | }, 318 | { 319 | seen: true, 320 | }, 321 | { 322 | new: true, 323 | } 324 | ); 325 | 326 | return res.status(200).json({ 327 | notifications, 328 | }); 329 | }); 330 | --------------------------------------------------------------------------------