├── public └── temp │ └── .gitkeep ├── src ├── constants.js ├── routes │ ├── healthcheck.routes.js │ ├── dashboard.routes.js │ ├── comment.routes.js │ ├── tweet.routes.js │ ├── subscription.routes.js │ ├── like.routes.js │ ├── playlist.routes.js │ ├── video.routes.js │ └── user.routes.js ├── utils │ ├── ApiResponse.js │ ├── ApiError.js │ ├── asyncHandler.js │ └── cloudinary.js ├── models │ ├── tweet.model.js │ ├── subscription.model.js │ ├── like.model.js │ ├── playlist.model.js │ ├── comment.model.js │ ├── video.model.js │ └── user.model.js ├── middlewares │ ├── multer.middleware.js │ └── auth.middleware.js ├── controllers │ ├── healthcheck.controller.js │ ├── dashboard.controller.js │ ├── tweet.controller.js │ ├── comment.controller.js │ ├── like.controller.js │ ├── subscription.controller.js │ ├── video.controller.js │ ├── playlist.controller.js │ └── user.controller.js ├── db │ └── index.js ├── index.js └── app.js ├── .prettierignore ├── .prettierrc ├── .env.sample ├── package.json ├── Readme.md └── .gitignore /public/temp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const DB_NAME = "videotube" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /node_modules 3 | ./dist 4 | 5 | *.env 6 | .env 7 | .env.* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "bracketSpacing": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "semi": true 7 | } -------------------------------------------------------------------------------- /src/routes/healthcheck.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { healthcheck } from "../controllers/healthcheck.controller.js" 3 | 4 | const router = Router(); 5 | 6 | router.route('/').get(healthcheck); 7 | 8 | export default router -------------------------------------------------------------------------------- /src/utils/ApiResponse.js: -------------------------------------------------------------------------------- 1 | class ApiResponse { 2 | constructor(statusCode, data, message = "Success"){ 3 | this.statusCode = statusCode 4 | this.data = data 5 | this.message = message 6 | this.success = statusCode < 400 7 | } 8 | } 9 | 10 | export { ApiResponse } -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | PORT=8000 2 | MONGODB_URI=mongodb+srv://hitesh:your-password@cluster0.lxl3fsq.mongodb.net 3 | CORS_ORIGIN=* 4 | ACCESS_TOKEN_SECRET=chai-aur-code 5 | ACCESS_TOKEN_EXPIRY=1d 6 | REFRESH_TOKEN_SECRET=chai-aur-backend 7 | REFRESH_TOKEN_EXPIRY=10d 8 | 9 | CLOUDINARY_CLOUD_NAME= 10 | CLOUDINARY_API_KEY= 11 | CLOUDINARY_API_SECRET= -------------------------------------------------------------------------------- /src/models/tweet.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema} from "mongoose"; 2 | 3 | const tweetSchema = new Schema({ 4 | content: { 5 | type: String, 6 | required: true 7 | }, 8 | owner: { 9 | type: Schema.Types.ObjectId, 10 | ref: "User" 11 | } 12 | }, {timestamps: true}) 13 | 14 | 15 | export const Tweet = mongoose.model("Tweet", tweetSchema) -------------------------------------------------------------------------------- /src/middlewares/multer.middleware.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.diskStorage({ 4 | destination: function (req, file, cb) { 5 | cb(null, "./public/temp") 6 | }, 7 | filename: function (req, file, cb) { 8 | 9 | cb(null, file.originalname) 10 | } 11 | }) 12 | 13 | export const upload = multer({ 14 | storage, 15 | }) -------------------------------------------------------------------------------- /src/controllers/healthcheck.controller.js: -------------------------------------------------------------------------------- 1 | import {ApiError} from "../utils/ApiError.js" 2 | import {ApiResponse} from "../utils/ApiResponse.js" 3 | import {asyncHandler} from "../utils/asyncHandler.js" 4 | 5 | 6 | const healthcheck = asyncHandler(async (req, res) => { 7 | //TODO: build a healthcheck response that simply returns the OK status as json with a message 8 | }) 9 | 10 | export { 11 | healthcheck 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/dashboard.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getChannelStats, 4 | getChannelVideos, 5 | } from "../controllers/dashboard.controller.js" 6 | import {verifyJWT} from "../middlewares/auth.middleware.js" 7 | 8 | const router = Router(); 9 | 10 | router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file 11 | 12 | router.route("/stats").get(getChannelStats); 13 | router.route("/videos").get(getChannelVideos); 14 | 15 | export default router -------------------------------------------------------------------------------- /src/models/subscription.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema} from "mongoose" 2 | 3 | const subscriptionSchema = new Schema({ 4 | subscriber: { 5 | type: Schema.Types.ObjectId, // one who is subscribing 6 | ref: "User" 7 | }, 8 | channel: { 9 | type: Schema.Types.ObjectId, // one to whom 'subscriber' is subscribing 10 | ref: "User" 11 | } 12 | }, {timestamps: true}) 13 | 14 | 15 | 16 | export const Subscription = mongoose.model("Subscription", subscriptionSchema) -------------------------------------------------------------------------------- /src/db/index.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { DB_NAME } from "../constants.js"; 3 | 4 | 5 | const connectDB = async () => { 6 | try { 7 | const connectionInstance = await mongoose.connect(`${process.env.MONGODB_URI}/${DB_NAME}`) 8 | console.log(`\n MongoDB connected !! DB HOST: ${connectionInstance.connection.host}`); 9 | } catch (error) { 10 | console.log("MONGODB connection FAILED ", error); 11 | process.exit(1) 12 | } 13 | } 14 | 15 | export default connectDB -------------------------------------------------------------------------------- /src/routes/comment.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | addComment, 4 | deleteComment, 5 | getVideoComments, 6 | updateComment, 7 | } from "../controllers/comment.controller.js" 8 | import {verifyJWT} from "../middlewares/auth.middleware.js" 9 | 10 | const router = Router(); 11 | 12 | router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file 13 | 14 | router.route("/:videoId").get(getVideoComments).post(addComment); 15 | router.route("/c/:commentId").delete(deleteComment).patch(updateComment); 16 | 17 | export default router -------------------------------------------------------------------------------- /src/routes/tweet.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | createTweet, 4 | deleteTweet, 5 | getUserTweets, 6 | updateTweet, 7 | } from "../controllers/tweet.controller.js" 8 | import {verifyJWT} from "../middlewares/auth.middleware.js" 9 | 10 | const router = Router(); 11 | router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file 12 | 13 | router.route("/").post(createTweet); 14 | router.route("/user/:userId").get(getUserTweets); 15 | router.route("/:tweetId").patch(updateTweet).delete(deleteTweet); 16 | 17 | export default router -------------------------------------------------------------------------------- /src/models/like.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema} from "mongoose"; 2 | 3 | 4 | const likeSchema = new Schema({ 5 | video: { 6 | type: Schema.Types.ObjectId, 7 | ref: "Video" 8 | }, 9 | comment: { 10 | type: Schema.Types.ObjectId, 11 | ref: "Comment" 12 | }, 13 | tweet: { 14 | type: Schema.Types.ObjectId, 15 | ref: "Tweet" 16 | }, 17 | likedBy: { 18 | type: Schema.Types.ObjectId, 19 | ref: "User" 20 | }, 21 | 22 | }, {timestamps: true}) 23 | 24 | export const Like = mongoose.model("Like", likeSchema) -------------------------------------------------------------------------------- /src/utils/ApiError.js: -------------------------------------------------------------------------------- 1 | class ApiError extends Error { 2 | constructor( 3 | statusCode, 4 | message= "Something went wrong", 5 | errors = [], 6 | stack = "" 7 | ){ 8 | super(message) 9 | this.statusCode = statusCode 10 | this.data = null 11 | this.message = message 12 | this.success = false; 13 | this.errors = errors 14 | 15 | if (stack) { 16 | this.stack = stack 17 | } else{ 18 | Error.captureStackTrace(this, this.constructor) 19 | } 20 | 21 | } 22 | } 23 | 24 | export {ApiError} -------------------------------------------------------------------------------- /src/models/playlist.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema} from "mongoose"; 2 | 3 | const playlistSchema = new Schema({ 4 | name: { 5 | type: String, 6 | required: true 7 | }, 8 | description: { 9 | type: String, 10 | required: true 11 | }, 12 | videos: [ 13 | { 14 | type: Schema.Types.ObjectId, 15 | ref: "Video" 16 | } 17 | ], 18 | owner: { 19 | type: Schema.Types.ObjectId, 20 | ref: "User" 21 | }, 22 | }, {timestamps: true}) 23 | 24 | 25 | 26 | export const Playlist = mongoose.model("Playlist", playlistSchema) -------------------------------------------------------------------------------- /src/routes/subscription.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getSubscribedChannels, 4 | getUserChannelSubscribers, 5 | toggleSubscription, 6 | } from "../controllers/subscription.controller.js" 7 | import {verifyJWT} from "../middlewares/auth.middleware.js" 8 | 9 | const router = Router(); 10 | router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file 11 | 12 | router 13 | .route("/c/:channelId") 14 | .get(getSubscribedChannels) 15 | .post(toggleSubscription); 16 | 17 | router.route("/u/:subscriberId").get(getUserChannelSubscribers); 18 | 19 | export default router -------------------------------------------------------------------------------- /src/routes/like.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getLikedVideos, 4 | toggleCommentLike, 5 | toggleVideoLike, 6 | toggleTweetLike, 7 | } from "../controllers/like.controller.js" 8 | import {verifyJWT} from "../middlewares/auth.middleware.js" 9 | 10 | const router = Router(); 11 | router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file 12 | 13 | router.route("/toggle/v/:videoId").post(toggleVideoLike); 14 | router.route("/toggle/c/:commentId").post(toggleCommentLike); 15 | router.route("/toggle/t/:tweetId").post(toggleTweetLike); 16 | router.route("/videos").get(getLikedVideos); 17 | 18 | export default router -------------------------------------------------------------------------------- /src/models/comment.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema} from "mongoose"; 2 | import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2"; 3 | 4 | const commentSchema = new Schema( 5 | { 6 | content: { 7 | type: String, 8 | required: true 9 | }, 10 | video: { 11 | type: Schema.Types.ObjectId, 12 | ref: "Video" 13 | }, 14 | owner: { 15 | type: Schema.Types.ObjectId, 16 | ref: "User" 17 | } 18 | }, 19 | { 20 | timestamps: true 21 | } 22 | ) 23 | 24 | 25 | commentSchema.plugin(mongooseAggregatePaginate) 26 | 27 | export const Comment = mongoose.model("Comment", commentSchema) -------------------------------------------------------------------------------- /src/utils/asyncHandler.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (requestHandler) => { 2 | return (req, res, next) => { 3 | Promise.resolve(requestHandler(req, res, next)).catch((err) => next(err)) 4 | } 5 | } 6 | 7 | 8 | export { asyncHandler } 9 | 10 | 11 | 12 | 13 | // const asyncHandler = () => {} 14 | // const asyncHandler = (func) => () => {} 15 | // const asyncHandler = (func) => async () => {} 16 | 17 | 18 | // const asyncHandler = (fn) => async (req, res, next) => { 19 | // try { 20 | // await fn(req, res, next) 21 | // } catch (error) { 22 | // res.status(err.code || 500).json({ 23 | // success: false, 24 | // message: err.message 25 | // }) 26 | // } 27 | // } -------------------------------------------------------------------------------- /src/controllers/dashboard.controller.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | import {Video} from "../models/video.model.js" 3 | import {Subscription} from "../models/subscription.model.js" 4 | import {Like} from "../models/like.model.js" 5 | import {ApiError} from "../utils/ApiError.js" 6 | import {ApiResponse} from "../utils/ApiResponse.js" 7 | import {asyncHandler} from "../utils/asyncHandler.js" 8 | 9 | const getChannelStats = asyncHandler(async (req, res) => { 10 | // TODO: Get the channel stats like total video views, total subscribers, total videos, total likes etc. 11 | }) 12 | 13 | const getChannelVideos = asyncHandler(async (req, res) => { 14 | // TODO: Get all the videos uploaded by the channel 15 | }) 16 | 17 | export { 18 | getChannelStats, 19 | getChannelVideos 20 | } -------------------------------------------------------------------------------- /src/controllers/tweet.controller.js: -------------------------------------------------------------------------------- 1 | import mongoose, { isValidObjectId } from "mongoose" 2 | import {Tweet} from "../models/tweet.model.js" 3 | import {User} from "../models/user.model.js" 4 | import {ApiError} from "../utils/ApiError.js" 5 | import {ApiResponse} from "../utils/ApiResponse.js" 6 | import {asyncHandler} from "../utils/asyncHandler.js" 7 | 8 | const createTweet = asyncHandler(async (req, res) => { 9 | //TODO: create tweet 10 | }) 11 | 12 | const getUserTweets = asyncHandler(async (req, res) => { 13 | // TODO: get user tweets 14 | }) 15 | 16 | const updateTweet = asyncHandler(async (req, res) => { 17 | //TODO: update tweet 18 | }) 19 | 20 | const deleteTweet = asyncHandler(async (req, res) => { 21 | //TODO: delete tweet 22 | }) 23 | 24 | export { 25 | createTweet, 26 | getUserTweets, 27 | updateTweet, 28 | deleteTweet 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chai-backend", 3 | "version": "1.0.0", 4 | "description": "a backend at chai aur code channel - youtube", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "nodemon -r dotenv/config --experimental-json-modules src/index.js" 9 | }, 10 | "keywords": [ 11 | "javascript", 12 | "backend", 13 | "chai" 14 | ], 15 | "author": "Hitesh Choudhary", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "nodemon": "^3.0.1", 19 | "prettier": "^3.0.3" 20 | }, 21 | "dependencies": { 22 | "bcrypt": "^5.1.1", 23 | "cloudinary": "^1.41.0", 24 | "cookie-parser": "^1.4.6", 25 | "cors": "^2.8.5", 26 | "dotenv": "^16.3.1", 27 | "express": "^4.18.2", 28 | "jsonwebtoken": "^9.0.2", 29 | "mongoose": "^8.0.0", 30 | "mongoose-aggregate-paginate-v2": "^1.0.6", 31 | "multer": "^1.4.5-lts.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/playlist.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | addVideoToPlaylist, 4 | createPlaylist, 5 | deletePlaylist, 6 | getPlaylistById, 7 | getUserPlaylists, 8 | removeVideoFromPlaylist, 9 | updatePlaylist, 10 | } from "../controllers/playlist.controller.js" 11 | import {verifyJWT} from "../middlewares/auth.middleware.js" 12 | 13 | const router = Router(); 14 | 15 | router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file 16 | 17 | router.route("/").post(createPlaylist) 18 | 19 | router 20 | .route("/:playlistId") 21 | .get(getPlaylistById) 22 | .patch(updatePlaylist) 23 | .delete(deletePlaylist); 24 | 25 | router.route("/add/:videoId/:playlistId").patch(addVideoToPlaylist); 26 | router.route("/remove/:videoId/:playlistId").patch(removeVideoFromPlaylist); 27 | 28 | router.route("/user/:userId").get(getUserPlaylists); 29 | 30 | export default router -------------------------------------------------------------------------------- /src/controllers/comment.controller.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | import {Comment} from "../models/comment.model.js" 3 | import {ApiError} from "../utils/ApiError.js" 4 | import {ApiResponse} from "../utils/ApiResponse.js" 5 | import {asyncHandler} from "../utils/asyncHandler.js" 6 | 7 | const getVideoComments = asyncHandler(async (req, res) => { 8 | //TODO: get all comments for a video 9 | const {videoId} = req.params 10 | const {page = 1, limit = 10} = req.query 11 | 12 | }) 13 | 14 | const addComment = asyncHandler(async (req, res) => { 15 | // TODO: add a comment to a video 16 | }) 17 | 18 | const updateComment = asyncHandler(async (req, res) => { 19 | // TODO: update a comment 20 | }) 21 | 22 | const deleteComment = asyncHandler(async (req, res) => { 23 | // TODO: delete a comment 24 | }) 25 | 26 | export { 27 | getVideoComments, 28 | addComment, 29 | updateComment, 30 | deleteComment 31 | } 32 | -------------------------------------------------------------------------------- /src/controllers/like.controller.js: -------------------------------------------------------------------------------- 1 | import mongoose, {isValidObjectId} from "mongoose" 2 | import {Like} from "../models/like.model.js" 3 | import {ApiError} from "../utils/ApiError.js" 4 | import {ApiResponse} from "../utils/ApiResponse.js" 5 | import {asyncHandler} from "../utils/asyncHandler.js" 6 | 7 | const toggleVideoLike = asyncHandler(async (req, res) => { 8 | const {videoId} = req.params 9 | //TODO: toggle like on video 10 | }) 11 | 12 | const toggleCommentLike = asyncHandler(async (req, res) => { 13 | const {commentId} = req.params 14 | //TODO: toggle like on comment 15 | 16 | }) 17 | 18 | const toggleTweetLike = asyncHandler(async (req, res) => { 19 | const {tweetId} = req.params 20 | //TODO: toggle like on tweet 21 | } 22 | ) 23 | 24 | const getLikedVideos = asyncHandler(async (req, res) => { 25 | //TODO: get all liked videos 26 | }) 27 | 28 | export { 29 | toggleCommentLike, 30 | toggleTweetLike, 31 | toggleVideoLike, 32 | getLikedVideos 33 | } -------------------------------------------------------------------------------- /src/controllers/subscription.controller.js: -------------------------------------------------------------------------------- 1 | import mongoose, {isValidObjectId} from "mongoose" 2 | import {User} from "../models/user.model.js" 3 | import { Subscription } from "../models/subscription.model.js" 4 | import {ApiError} from "../utils/ApiError.js" 5 | import {ApiResponse} from "../utils/ApiResponse.js" 6 | import {asyncHandler} from "../utils/asyncHandler.js" 7 | 8 | 9 | const toggleSubscription = asyncHandler(async (req, res) => { 10 | const {channelId} = req.params 11 | // TODO: toggle subscription 12 | }) 13 | 14 | // controller to return subscriber list of a channel 15 | const getUserChannelSubscribers = asyncHandler(async (req, res) => { 16 | const {channelId} = req.params 17 | }) 18 | 19 | // controller to return channel list to which user has subscribed 20 | const getSubscribedChannels = asyncHandler(async (req, res) => { 21 | const { subscriberId } = req.params 22 | }) 23 | 24 | export { 25 | toggleSubscription, 26 | getUserChannelSubscribers, 27 | getSubscribedChannels 28 | } -------------------------------------------------------------------------------- /src/utils/cloudinary.js: -------------------------------------------------------------------------------- 1 | import {v2 as cloudinary} from "cloudinary" 2 | import fs from "fs" 3 | 4 | 5 | cloudinary.config({ 6 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 7 | api_key: process.env.CLOUDINARY_API_KEY, 8 | api_secret: process.env.CLOUDINARY_API_SECRET 9 | }); 10 | 11 | const uploadOnCloudinary = async (localFilePath) => { 12 | try { 13 | if (!localFilePath) return null 14 | //upload the file on cloudinary 15 | const response = await cloudinary.uploader.upload(localFilePath, { 16 | resource_type: "auto" 17 | }) 18 | // file has been uploaded successfull 19 | //console.log("file is uploaded on cloudinary ", response.url); 20 | fs.unlinkSync(localFilePath) 21 | return response; 22 | 23 | } catch (error) { 24 | fs.unlinkSync(localFilePath) // remove the locally saved temporary file as the upload operation got failed 25 | return null; 26 | } 27 | } 28 | 29 | 30 | 31 | export {uploadOnCloudinary} -------------------------------------------------------------------------------- /src/middlewares/auth.middleware.js: -------------------------------------------------------------------------------- 1 | import { ApiError } from "../utils/ApiError.js"; 2 | import { asyncHandler } from "../utils/asyncHandler.js"; 3 | import jwt from "jsonwebtoken" 4 | import { User } from "../models/user.model.js"; 5 | 6 | export const verifyJWT = asyncHandler(async(req, _, next) => { 7 | try { 8 | const token = req.cookies?.accessToken || req.header("Authorization")?.replace("Bearer ", "") 9 | 10 | // console.log(token); 11 | if (!token) { 12 | throw new ApiError(401, "Unauthorized request") 13 | } 14 | 15 | const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET) 16 | 17 | const user = await User.findById(decodedToken?._id).select("-password -refreshToken") 18 | 19 | if (!user) { 20 | 21 | throw new ApiError(401, "Invalid Access Token") 22 | } 23 | 24 | req.user = user; 25 | next() 26 | } catch (error) { 27 | throw new ApiError(401, error?.message || "Invalid access token") 28 | } 29 | 30 | }) -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // require('dotenv').config({path: './env'}) 2 | import dotenv from "dotenv" 3 | import connectDB from "./db/index.js"; 4 | import {app} from './app.js' 5 | dotenv.config({ 6 | path: './.env' 7 | }) 8 | 9 | 10 | 11 | connectDB() 12 | .then(() => { 13 | app.listen(process.env.PORT || 8000, () => { 14 | console.log(`⚙️ Server is running at port : ${process.env.PORT}`); 15 | }) 16 | }) 17 | .catch((err) => { 18 | console.log("MONGO db connection failed !!! ", err); 19 | }) 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | /* 31 | import express from "express" 32 | const app = express() 33 | ( async () => { 34 | try { 35 | await mongoose.connect(`${process.env.MONGODB_URI}/${DB_NAME}`) 36 | app.on("errror", (error) => { 37 | console.log("ERRR: ", error); 38 | throw error 39 | }) 40 | 41 | app.listen(process.env.PORT, () => { 42 | console.log(`App is listening on port ${process.env.PORT}`); 43 | }) 44 | 45 | } catch (error) { 46 | console.error("ERROR: ", error) 47 | throw err 48 | } 49 | })() 50 | 51 | */ -------------------------------------------------------------------------------- /src/routes/video.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | deleteVideo, 4 | getAllVideos, 5 | getVideoById, 6 | publishAVideo, 7 | togglePublishStatus, 8 | updateVideo, 9 | } from "../controllers/video.controller.js" 10 | import {verifyJWT} from "../middlewares/auth.middleware.js" 11 | import {upload} from "../middlewares/multer.middleware.js" 12 | 13 | const router = Router(); 14 | router.use(verifyJWT); // Apply verifyJWT middleware to all routes in this file 15 | 16 | router 17 | .route("/") 18 | .get(getAllVideos) 19 | .post( 20 | upload.fields([ 21 | { 22 | name: "videoFile", 23 | maxCount: 1, 24 | }, 25 | { 26 | name: "thumbnail", 27 | maxCount: 1, 28 | }, 29 | 30 | ]), 31 | publishAVideo 32 | ); 33 | 34 | router 35 | .route("/:videoId") 36 | .get(getVideoById) 37 | .delete(deleteVideo) 38 | .patch(upload.single("thumbnail"), updateVideo); 39 | 40 | router.route("/toggle/publish/:videoId").patch(togglePublishStatus); 41 | 42 | export default router -------------------------------------------------------------------------------- /src/models/video.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema} from "mongoose"; 2 | import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2"; 3 | 4 | const videoSchema = new Schema( 5 | { 6 | videoFile: { 7 | type: String, //cloudinary url 8 | required: true 9 | }, 10 | thumbnail: { 11 | type: String, //cloudinary url 12 | required: true 13 | }, 14 | title: { 15 | type: String, 16 | required: true 17 | }, 18 | description: { 19 | type: String, 20 | required: true 21 | }, 22 | duration: { 23 | type: Number, 24 | required: true 25 | }, 26 | views: { 27 | type: Number, 28 | default: 0 29 | }, 30 | isPublished: { 31 | type: Boolean, 32 | default: true 33 | }, 34 | owner: { 35 | type: Schema.Types.ObjectId, 36 | ref: "User" 37 | } 38 | 39 | }, 40 | { 41 | timestamps: true 42 | } 43 | ) 44 | 45 | videoSchema.plugin(mongooseAggregatePaginate) 46 | 47 | export const Video = mongoose.model("Video", videoSchema) -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import cors from "cors" 3 | import cookieParser from "cookie-parser" 4 | 5 | const app = express() 6 | 7 | app.use(cors({ 8 | origin: process.env.CORS_ORIGIN, 9 | credentials: true 10 | })) 11 | 12 | app.use(express.json({limit: "16kb"})) 13 | app.use(express.urlencoded({extended: true, limit: "16kb"})) 14 | app.use(express.static("public")) 15 | app.use(cookieParser()) 16 | 17 | 18 | //routes import 19 | import userRouter from './routes/user.routes.js' 20 | import healthcheckRouter from "./routes/healthcheck.routes.js" 21 | import tweetRouter from "./routes/tweet.routes.js" 22 | import subscriptionRouter from "./routes/subscription.routes.js" 23 | import videoRouter from "./routes/video.routes.js" 24 | import commentRouter from "./routes/comment.routes.js" 25 | import likeRouter from "./routes/like.routes.js" 26 | import playlistRouter from "./routes/playlist.routes.js" 27 | import dashboardRouter from "./routes/dashboard.routes.js" 28 | 29 | //routes declaration 30 | app.use("/api/v1/healthcheck", healthcheckRouter) 31 | app.use("/api/v1/users", userRouter) 32 | app.use("/api/v1/tweets", tweetRouter) 33 | app.use("/api/v1/subscriptions", subscriptionRouter) 34 | app.use("/api/v1/videos", videoRouter) 35 | app.use("/api/v1/comments", commentRouter) 36 | app.use("/api/v1/likes", likeRouter) 37 | app.use("/api/v1/playlist", playlistRouter) 38 | app.use("/api/v1/dashboard", dashboardRouter) 39 | 40 | // http://localhost:8000/api/v1/users/register 41 | 42 | export { app } -------------------------------------------------------------------------------- /src/controllers/video.controller.js: -------------------------------------------------------------------------------- 1 | import mongoose, {isValidObjectId} from "mongoose" 2 | import {Video} from "../models/video.model.js" 3 | import {User} from "../models/user.model.js" 4 | import {ApiError} from "../utils/ApiError.js" 5 | import {ApiResponse} from "../utils/ApiResponse.js" 6 | import {asyncHandler} from "../utils/asyncHandler.js" 7 | import {uploadOnCloudinary} from "../utils/cloudinary.js" 8 | 9 | 10 | const getAllVideos = asyncHandler(async (req, res) => { 11 | const { page = 1, limit = 10, query, sortBy, sortType, userId } = req.query 12 | //TODO: get all videos based on query, sort, pagination 13 | }) 14 | 15 | const publishAVideo = asyncHandler(async (req, res) => { 16 | const { title, description} = req.body 17 | // TODO: get video, upload to cloudinary, create video 18 | }) 19 | 20 | const getVideoById = asyncHandler(async (req, res) => { 21 | const { videoId } = req.params 22 | //TODO: get video by id 23 | }) 24 | 25 | const updateVideo = asyncHandler(async (req, res) => { 26 | const { videoId } = req.params 27 | //TODO: update video details like title, description, thumbnail 28 | 29 | }) 30 | 31 | const deleteVideo = asyncHandler(async (req, res) => { 32 | const { videoId } = req.params 33 | //TODO: delete video 34 | }) 35 | 36 | const togglePublishStatus = asyncHandler(async (req, res) => { 37 | const { videoId } = req.params 38 | }) 39 | 40 | export { 41 | getAllVideos, 42 | publishAVideo, 43 | getVideoById, 44 | updateVideo, 45 | deleteVideo, 46 | togglePublishStatus 47 | } 48 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # chai aur backend series 2 | 3 | This is a video series on backend with javascript 4 | - [Model link](https://app.eraser.io/workspace/YtPqZ1VogxGy1jzIDkzj?origin=share) 5 | 6 | - [Video playlist](https://www.youtube.com/watch?v=EH3vGeqeIAo&list=PLu71SKxNbfoBGh_8p_NS-ZAh6v7HhYqHW) 7 | 8 | --- 9 | # Summary of this project 10 | 11 | This project is a complex backend project that is built with nodejs, expressjs, mongodb, mongoose, jwt, bcrypt, and many more. This project is a complete backend project that has all the features that a backend project should have. 12 | We are building a complete video hosting website similar to youtube with all the features like login, signup, upload video, like, dislike, comment, reply, subscribe, unsubscribe, and many more. 13 | 14 | Project uses all standard practices like JWT, bcrypt, access tokens, refresh Tokens and many more. We have spent a lot of time in building this project and we are sure that you will learn a lot from this project. 15 | 16 | --- 17 | Top Contributer to complete all TODOs 18 | 19 | 1. Spiderman (just sample) [Link to Repo](https://www.youtube.com/@chaiaurcode) 20 | 21 | --- 22 | ## How to contribute in this open source Project 23 | 24 | First, please understand that this is not your regular project to merge your PR. This repo requires you to finish all assignments that are in controller folder. We don't accept half work, please finish all controllers and then reach us out on [Discord](https://hitesh.ai/discord) or [Twitter](https://twitter.com/@hiteshdotcom) and after checking your repo, I will add link to your repo in this readme. -------------------------------------------------------------------------------- /src/controllers/playlist.controller.js: -------------------------------------------------------------------------------- 1 | import mongoose, {isValidObjectId} from "mongoose" 2 | import {Playlist} from "../models/playlist.model.js" 3 | import {ApiError} from "../utils/ApiError.js" 4 | import {ApiResponse} from "../utils/ApiResponse.js" 5 | import {asyncHandler} from "../utils/asyncHandler.js" 6 | 7 | 8 | const createPlaylist = asyncHandler(async (req, res) => { 9 | const {name, description} = req.body 10 | 11 | //TODO: create playlist 12 | }) 13 | 14 | const getUserPlaylists = asyncHandler(async (req, res) => { 15 | const {userId} = req.params 16 | //TODO: get user playlists 17 | }) 18 | 19 | const getPlaylistById = asyncHandler(async (req, res) => { 20 | const {playlistId} = req.params 21 | //TODO: get playlist by id 22 | }) 23 | 24 | const addVideoToPlaylist = asyncHandler(async (req, res) => { 25 | const {playlistId, videoId} = req.params 26 | }) 27 | 28 | const removeVideoFromPlaylist = asyncHandler(async (req, res) => { 29 | const {playlistId, videoId} = req.params 30 | // TODO: remove video from playlist 31 | 32 | }) 33 | 34 | const deletePlaylist = asyncHandler(async (req, res) => { 35 | const {playlistId} = req.params 36 | // TODO: delete playlist 37 | }) 38 | 39 | const updatePlaylist = asyncHandler(async (req, res) => { 40 | const {playlistId} = req.params 41 | const {name, description} = req.body 42 | //TODO: update playlist 43 | }) 44 | 45 | export { 46 | createPlaylist, 47 | getUserPlaylists, 48 | getPlaylistById, 49 | addVideoToPlaylist, 50 | removeVideoFromPlaylist, 51 | deletePlaylist, 52 | updatePlaylist 53 | } 54 | -------------------------------------------------------------------------------- /src/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { 3 | loginUser, 4 | logoutUser, 5 | registerUser, 6 | refreshAccessToken, 7 | changeCurrentPassword, 8 | getCurrentUser, 9 | updateUserAvatar, 10 | updateUserCoverImage, 11 | getUserChannelProfile, 12 | getWatchHistory, 13 | updateAccountDetails 14 | } from "../controllers/user.controller.js"; 15 | import {upload} from "../middlewares/multer.middleware.js" 16 | import { verifyJWT } from "../middlewares/auth.middleware.js"; 17 | 18 | 19 | const router = Router() 20 | 21 | router.route("/register").post( 22 | upload.fields([ 23 | { 24 | name: "avatar", 25 | maxCount: 1 26 | }, 27 | { 28 | name: "coverImage", 29 | maxCount: 1 30 | } 31 | ]), 32 | registerUser 33 | ) 34 | 35 | router.route("/login").post(loginUser) 36 | 37 | //secured routes 38 | router.route("/logout").post(verifyJWT, logoutUser) 39 | router.route("/refresh-token").post(refreshAccessToken) 40 | router.route("/change-password").post(verifyJWT, changeCurrentPassword) 41 | router.route("/current-user").get(verifyJWT, getCurrentUser) 42 | router.route("/update-account").patch(verifyJWT, updateAccountDetails) 43 | 44 | router.route("/avatar").patch(verifyJWT, upload.single("avatar"), updateUserAvatar) 45 | router.route("/cover-image").patch(verifyJWT, upload.single("coverImage"), updateUserCoverImage) 46 | 47 | router.route("/c/:username").get(verifyJWT, getUserChannelProfile) 48 | router.route("/history").get(verifyJWT, getWatchHistory) 49 | 50 | export default router -------------------------------------------------------------------------------- /src/models/user.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, {Schema} from "mongoose"; 2 | import jwt from "jsonwebtoken" 3 | import bcrypt from "bcrypt" 4 | 5 | const userSchema = new Schema( 6 | { 7 | username: { 8 | type: String, 9 | required: true, 10 | unique: true, 11 | lowercase: true, 12 | trim: true, 13 | index: true 14 | }, 15 | email: { 16 | type: String, 17 | required: true, 18 | unique: true, 19 | lowecase: true, 20 | trim: true, 21 | }, 22 | fullName: { 23 | type: String, 24 | required: true, 25 | trim: true, 26 | index: true 27 | }, 28 | avatar: { 29 | type: String, // cloudinary url 30 | required: true, 31 | }, 32 | coverImage: { 33 | type: String, // cloudinary url 34 | }, 35 | watchHistory: [ 36 | { 37 | type: Schema.Types.ObjectId, 38 | ref: "Video" 39 | } 40 | ], 41 | password: { 42 | type: String, 43 | required: [true, 'Password is required'] 44 | }, 45 | refreshToken: { 46 | type: String 47 | } 48 | 49 | }, 50 | { 51 | timestamps: true 52 | } 53 | ) 54 | 55 | userSchema.pre("save", async function (next) { 56 | if(!this.isModified("password")) return next(); 57 | 58 | this.password = await bcrypt.hash(this.password, 10) 59 | next() 60 | }) 61 | 62 | userSchema.methods.isPasswordCorrect = async function(password){ 63 | return await bcrypt.compare(password, this.password) 64 | } 65 | 66 | userSchema.methods.generateAccessToken = function(){ 67 | return jwt.sign( 68 | { 69 | _id: this._id, 70 | email: this.email, 71 | username: this.username, 72 | fullName: this.fullName 73 | }, 74 | process.env.ACCESS_TOKEN_SECRET, 75 | { 76 | expiresIn: process.env.ACCESS_TOKEN_EXPIRY 77 | } 78 | ) 79 | } 80 | userSchema.methods.generateRefreshToken = function(){ 81 | return jwt.sign( 82 | { 83 | _id: this._id, 84 | 85 | }, 86 | process.env.REFRESH_TOKEN_SECRET, 87 | { 88 | expiresIn: process.env.REFRESH_TOKEN_EXPIRY 89 | } 90 | ) 91 | } 92 | 93 | export const User = mongoose.model("User", userSchema) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | # End of https://mrkandreev.name/snippets/gitignore-generator/#Node 121 | .DS_Store 122 | -------------------------------------------------------------------------------- /src/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | import { asyncHandler } from "../utils/asyncHandler.js"; 2 | import {ApiError} from "../utils/ApiError.js" 3 | import { User} from "../models/user.model.js" 4 | import {uploadOnCloudinary} from "../utils/cloudinary.js" 5 | import { ApiResponse } from "../utils/ApiResponse.js"; 6 | import jwt from "jsonwebtoken" 7 | import mongoose from "mongoose"; 8 | 9 | 10 | const generateAccessAndRefereshTokens = async(userId) =>{ 11 | try { 12 | const user = await User.findById(userId) 13 | const accessToken = user.generateAccessToken() 14 | const refreshToken = user.generateRefreshToken() 15 | 16 | user.refreshToken = refreshToken 17 | await user.save({ validateBeforeSave: false }) 18 | 19 | return {accessToken, refreshToken} 20 | 21 | 22 | } catch (error) { 23 | throw new ApiError(500, "Something went wrong while generating referesh and access token") 24 | } 25 | } 26 | 27 | const registerUser = asyncHandler( async (req, res) => { 28 | // get user details from frontend 29 | // validation - not empty 30 | // check if user already exists: username, email 31 | // check for images, check for avatar 32 | // upload them to cloudinary, avatar 33 | // create user object - create entry in db 34 | // remove password and refresh token field from response 35 | // check for user creation 36 | // return res 37 | 38 | 39 | const {fullName, email, username, password } = req.body 40 | //console.log("email: ", email); 41 | 42 | if ( 43 | [fullName, email, username, password].some((field) => field?.trim() === "") 44 | ) { 45 | throw new ApiError(400, "All fields are required") 46 | } 47 | 48 | const existedUser = await User.findOne({ 49 | $or: [{ username }, { email }] 50 | }) 51 | 52 | if (existedUser) { 53 | throw new ApiError(409, "User with email or username already exists") 54 | } 55 | //console.log(req.files); 56 | 57 | const avatarLocalPath = req.files?.avatar[0]?.path; 58 | //const coverImageLocalPath = req.files?.coverImage[0]?.path; 59 | 60 | let coverImageLocalPath; 61 | if (req.files && Array.isArray(req.files.coverImage) && req.files.coverImage.length > 0) { 62 | coverImageLocalPath = req.files.coverImage[0].path 63 | } 64 | 65 | 66 | if (!avatarLocalPath) { 67 | throw new ApiError(400, "Avatar file is required") 68 | } 69 | 70 | const avatar = await uploadOnCloudinary(avatarLocalPath) 71 | const coverImage = await uploadOnCloudinary(coverImageLocalPath) 72 | 73 | if (!avatar) { 74 | throw new ApiError(400, "Avatar file is required") 75 | } 76 | 77 | 78 | const user = await User.create({ 79 | fullName, 80 | avatar: avatar.url, 81 | coverImage: coverImage?.url || "", 82 | email, 83 | password, 84 | username: username.toLowerCase() 85 | }) 86 | 87 | const createdUser = await User.findById(user._id).select( 88 | "-password -refreshToken" 89 | ) 90 | 91 | if (!createdUser) { 92 | throw new ApiError(500, "Something went wrong while registering the user") 93 | } 94 | 95 | return res.status(201).json( 96 | new ApiResponse(200, createdUser, "User registered Successfully") 97 | ) 98 | 99 | } ) 100 | 101 | const loginUser = asyncHandler(async (req, res) =>{ 102 | // req body -> data 103 | // username or email 104 | //find the user 105 | //password check 106 | //access and referesh token 107 | //send cookie 108 | 109 | const {email, username, password} = req.body 110 | console.log(email); 111 | 112 | if (!username && !email) { 113 | throw new ApiError(400, "username or email is required") 114 | } 115 | 116 | // Here is an alternative of above code based on logic discussed in video: 117 | // if (!(username || email)) { 118 | // throw new ApiError(400, "username or email is required") 119 | 120 | // } 121 | 122 | const user = await User.findOne({ 123 | $or: [{username}, {email}] 124 | }) 125 | 126 | if (!user) { 127 | throw new ApiError(404, "User does not exist") 128 | } 129 | 130 | const isPasswordValid = await user.isPasswordCorrect(password) 131 | 132 | if (!isPasswordValid) { 133 | throw new ApiError(401, "Invalid user credentials") 134 | } 135 | 136 | const {accessToken, refreshToken} = await generateAccessAndRefereshTokens(user._id) 137 | 138 | const loggedInUser = await User.findById(user._id).select("-password -refreshToken") 139 | 140 | const options = { 141 | httpOnly: true, 142 | secure: true 143 | } 144 | 145 | return res 146 | .status(200) 147 | .cookie("accessToken", accessToken, options) 148 | .cookie("refreshToken", refreshToken, options) 149 | .json( 150 | new ApiResponse( 151 | 200, 152 | { 153 | user: loggedInUser, accessToken, refreshToken 154 | }, 155 | "User logged In Successfully" 156 | ) 157 | ) 158 | 159 | }) 160 | 161 | const logoutUser = asyncHandler(async(req, res) => { 162 | await User.findByIdAndUpdate( 163 | req.user._id, 164 | { 165 | $unset: { 166 | refreshToken: 1 // this removes the field from document 167 | } 168 | }, 169 | { 170 | new: true 171 | } 172 | ) 173 | 174 | const options = { 175 | httpOnly: true, 176 | secure: true 177 | } 178 | 179 | return res 180 | .status(200) 181 | .clearCookie("accessToken", options) 182 | .clearCookie("refreshToken", options) 183 | .json(new ApiResponse(200, {}, "User logged Out")) 184 | }) 185 | 186 | const refreshAccessToken = asyncHandler(async (req, res) => { 187 | const incomingRefreshToken = req.cookies.refreshToken || req.body.refreshToken 188 | 189 | if (!incomingRefreshToken) { 190 | throw new ApiError(401, "unauthorized request") 191 | } 192 | 193 | try { 194 | const decodedToken = jwt.verify( 195 | incomingRefreshToken, 196 | process.env.REFRESH_TOKEN_SECRET 197 | ) 198 | 199 | const user = await User.findById(decodedToken?._id) 200 | 201 | if (!user) { 202 | throw new ApiError(401, "Invalid refresh token") 203 | } 204 | 205 | if (incomingRefreshToken !== user?.refreshToken) { 206 | throw new ApiError(401, "Refresh token is expired or used") 207 | 208 | } 209 | 210 | const options = { 211 | httpOnly: true, 212 | secure: true 213 | } 214 | 215 | const {accessToken, newRefreshToken} = await generateAccessAndRefereshTokens(user._id) 216 | 217 | return res 218 | .status(200) 219 | .cookie("accessToken", accessToken, options) 220 | .cookie("refreshToken", newRefreshToken, options) 221 | .json( 222 | new ApiResponse( 223 | 200, 224 | {accessToken, refreshToken: newRefreshToken}, 225 | "Access token refreshed" 226 | ) 227 | ) 228 | } catch (error) { 229 | throw new ApiError(401, error?.message || "Invalid refresh token") 230 | } 231 | 232 | }) 233 | 234 | const changeCurrentPassword = asyncHandler(async(req, res) => { 235 | const {oldPassword, newPassword} = req.body 236 | 237 | 238 | 239 | const user = await User.findById(req.user?._id) 240 | const isPasswordCorrect = await user.isPasswordCorrect(oldPassword) 241 | 242 | if (!isPasswordCorrect) { 243 | throw new ApiError(400, "Invalid old password") 244 | } 245 | 246 | user.password = newPassword 247 | await user.save({validateBeforeSave: false}) 248 | 249 | return res 250 | .status(200) 251 | .json(new ApiResponse(200, {}, "Password changed successfully")) 252 | }) 253 | 254 | 255 | const getCurrentUser = asyncHandler(async(req, res) => { 256 | return res 257 | .status(200) 258 | .json(new ApiResponse( 259 | 200, 260 | req.user, 261 | "User fetched successfully" 262 | )) 263 | }) 264 | 265 | const updateAccountDetails = asyncHandler(async(req, res) => { 266 | const {fullName, email} = req.body 267 | 268 | if (!fullName || !email) { 269 | throw new ApiError(400, "All fields are required") 270 | } 271 | 272 | const user = await User.findByIdAndUpdate( 273 | req.user?._id, 274 | { 275 | $set: { 276 | fullName, 277 | email: email 278 | } 279 | }, 280 | {new: true} 281 | 282 | ).select("-password") 283 | 284 | return res 285 | .status(200) 286 | .json(new ApiResponse(200, user, "Account details updated successfully")) 287 | }); 288 | 289 | const updateUserAvatar = asyncHandler(async(req, res) => { 290 | const avatarLocalPath = req.file?.path 291 | 292 | if (!avatarLocalPath) { 293 | throw new ApiError(400, "Avatar file is missing") 294 | } 295 | 296 | //TODO: delete old image - assignment 297 | 298 | const avatar = await uploadOnCloudinary(avatarLocalPath) 299 | 300 | if (!avatar.url) { 301 | throw new ApiError(400, "Error while uploading on avatar") 302 | 303 | } 304 | 305 | const user = await User.findByIdAndUpdate( 306 | req.user?._id, 307 | { 308 | $set:{ 309 | avatar: avatar.url 310 | } 311 | }, 312 | {new: true} 313 | ).select("-password") 314 | 315 | return res 316 | .status(200) 317 | .json( 318 | new ApiResponse(200, user, "Avatar image updated successfully") 319 | ) 320 | }) 321 | 322 | const updateUserCoverImage = asyncHandler(async(req, res) => { 323 | const coverImageLocalPath = req.file?.path 324 | 325 | if (!coverImageLocalPath) { 326 | throw new ApiError(400, "Cover image file is missing") 327 | } 328 | 329 | //TODO: delete old image - assignment 330 | 331 | 332 | const coverImage = await uploadOnCloudinary(coverImageLocalPath) 333 | 334 | if (!coverImage.url) { 335 | throw new ApiError(400, "Error while uploading on avatar") 336 | 337 | } 338 | 339 | const user = await User.findByIdAndUpdate( 340 | req.user?._id, 341 | { 342 | $set:{ 343 | coverImage: coverImage.url 344 | } 345 | }, 346 | {new: true} 347 | ).select("-password") 348 | 349 | return res 350 | .status(200) 351 | .json( 352 | new ApiResponse(200, user, "Cover image updated successfully") 353 | ) 354 | }) 355 | 356 | 357 | const getUserChannelProfile = asyncHandler(async(req, res) => { 358 | const {username} = req.params 359 | 360 | if (!username?.trim()) { 361 | throw new ApiError(400, "username is missing") 362 | } 363 | 364 | const channel = await User.aggregate([ 365 | { 366 | $match: { 367 | username: username?.toLowerCase() 368 | } 369 | }, 370 | { 371 | $lookup: { 372 | from: "subscriptions", 373 | localField: "_id", 374 | foreignField: "channel", 375 | as: "subscribers" 376 | } 377 | }, 378 | { 379 | $lookup: { 380 | from: "subscriptions", 381 | localField: "_id", 382 | foreignField: "subscriber", 383 | as: "subscribedTo" 384 | } 385 | }, 386 | { 387 | $addFields: { 388 | subscribersCount: { 389 | $size: "$subscribers" 390 | }, 391 | channelsSubscribedToCount: { 392 | $size: "$subscribedTo" 393 | }, 394 | isSubscribed: { 395 | $cond: { 396 | if: {$in: [req.user?._id, "$subscribers.subscriber"]}, 397 | then: true, 398 | else: false 399 | } 400 | } 401 | } 402 | }, 403 | { 404 | $project: { 405 | fullName: 1, 406 | username: 1, 407 | subscribersCount: 1, 408 | channelsSubscribedToCount: 1, 409 | isSubscribed: 1, 410 | avatar: 1, 411 | coverImage: 1, 412 | email: 1 413 | 414 | } 415 | } 416 | ]) 417 | 418 | if (!channel?.length) { 419 | throw new ApiError(404, "channel does not exists") 420 | } 421 | 422 | return res 423 | .status(200) 424 | .json( 425 | new ApiResponse(200, channel[0], "User channel fetched successfully") 426 | ) 427 | }) 428 | 429 | const getWatchHistory = asyncHandler(async(req, res) => { 430 | const user = await User.aggregate([ 431 | { 432 | $match: { 433 | _id: new mongoose.Types.ObjectId(req.user._id) 434 | } 435 | }, 436 | { 437 | $lookup: { 438 | from: "videos", 439 | localField: "watchHistory", 440 | foreignField: "_id", 441 | as: "watchHistory", 442 | pipeline: [ 443 | { 444 | $lookup: { 445 | from: "users", 446 | localField: "owner", 447 | foreignField: "_id", 448 | as: "owner", 449 | pipeline: [ 450 | { 451 | $project: { 452 | fullName: 1, 453 | username: 1, 454 | avatar: 1 455 | } 456 | } 457 | ] 458 | } 459 | }, 460 | { 461 | $addFields:{ 462 | owner:{ 463 | $first: "$owner" 464 | } 465 | } 466 | } 467 | ] 468 | } 469 | } 470 | ]) 471 | 472 | return res 473 | .status(200) 474 | .json( 475 | new ApiResponse( 476 | 200, 477 | user[0].watchHistory, 478 | "Watch history fetched successfully" 479 | ) 480 | ) 481 | }) 482 | 483 | 484 | export { 485 | registerUser, 486 | loginUser, 487 | logoutUser, 488 | refreshAccessToken, 489 | changeCurrentPassword, 490 | getCurrentUser, 491 | updateAccountDetails, 492 | updateUserAvatar, 493 | updateUserCoverImage, 494 | getUserChannelProfile, 495 | getWatchHistory 496 | } --------------------------------------------------------------------------------