diff --git a/Readme.md b/Readme.md index 62f1fb4b..e770ac4e 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,13 @@ -# chai aur backend series +# chai aur backend series This is a video series on backend with javascript + - [Model link](https://app.eraser.io/workspace/YtPqZ1VogxGy1jzIDkzj?origin=share) - [Video playlist](https://www.youtube.com/watch?v=EH3vGeqeIAo&list=PLu71SKxNbfoBGh_8p_NS-ZAh6v7HhYqHW) --- + # Summary of this project 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. @@ -14,11 +16,15 @@ We are building a complete video hosting website similar to youtube with all the 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. --- + Top Contributer to complete all TODOs -1. Spiderman (just sample) [Link to Repo](https://www.youtube.com/@chaiaurcode) +1. Spiderman (just sample) [Link to Repo](https://www.youtube.com/@chaiaurcode) + +--- ---- ## How to contribute in this open source Project -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. \ No newline at end of file +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. + +#171 diff --git a/src/controllers/comment.controller.js b/src/controllers/comment.controller.js index 47b5a821..16d07d2b 100644 --- a/src/controllers/comment.controller.js +++ b/src/controllers/comment.controller.js @@ -1,31 +1,155 @@ -import mongoose from "mongoose" -import {Comment} from "../models/comment.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" +import mongoose from "mongoose"; +import { Comment } from "../models/comment.model.js"; +import { Video } from "../models/video.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const getVideoComments = asyncHandler(async (req, res) => { - //TODO: get all comments for a video - const {videoId} = req.params - const {page = 1, limit = 10} = req.query + //TODO: get all comments for a video + const { videoId } = req.params; + const { page = 1, limit = 10 } = req.query; + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } + const options = { + page, + limit, + }; + const comments = await Comment.aggregate([ + { + $match: { + video: new mongoose.Types.ObjectId(videoId), + }, + }, + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "createdBy", + pipeline: [ + { + $project: { + username: 1, + fullName: 1, + avatar: 1, + }, + }, + ], + }, + }, + { + $addFields: { + createdBy: { + $first: "$createdBy", + }, + }, + }, + { + $unwind: "$createdBy", + }, + { + $project: { + content: 1, + createdBy: 1, + }, + }, + { + $skip: (page - 1) * limit, + }, + { + $limit: parseInt(limit), + }, + ]); -}) + return res + .status(200) + .json(new ApiResponse(200, comments, "Comments Fetched")); +}); const addComment = asyncHandler(async (req, res) => { - // TODO: add a comment to a video -}) + // TODO: add a comment to a video + const { videoId } = req.params; + const { content } = req.body; + const user = req.user._id; + if (!content) { + throw new ApiError(400, "Comment content is missing"); + } + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } + const comment = await Comment.create({ + content, + video: videoId, + owner: user, + }); + if (!comment) { + throw new ApiError(500, "Error while saving the comment"); + } + + return response + .status(200) + .json(new ApiResponse(200, comment, "Comment Saved")); +}); const updateComment = asyncHandler(async (req, res) => { - // TODO: update a comment -}) + // TODO: update a comment + const { content } = req.body; + if (!content) { + throw new ApiError(400, "Comment content is required"); + } + const { commentId } = req.params; + const user = req.user._id; + const originalComment = await Comment.findById(commentId); + if (!originalComment) { + throw new ApiError(404, "Comment not found"); + } + if (originalComment.owner !== user) { + throw new ApiError(403, "You don't have permission to update this comment"); + } + + const updateComment = await Comment.findByIdAndUpdate( + commentId, + { + $set: { + content, + }, + }, + { new: true } + ); + + if (!updateComment) { + throw new ApiError(500, "Error while updating comment"); + } + + return res + .status(200) + .json(new ApiResponse(200, updateComment, "Comment updated")); +}); const deleteComment = asyncHandler(async (req, res) => { - // TODO: delete a comment -}) - -export { - getVideoComments, - addComment, - updateComment, - deleteComment - } + // TODO: delete a comment + const { commentId } = req.params; + const user = req.user._id; + const comment = await Comment(findById(commentId)); + if (!comment) { + throw new ApiError(404, "Comment not found"); + } + if (comment.owner !== user) { + throw new ApiError(404, "You don't have permission to delete this comment"); + } + + const deleteComment = await Comment.findByIdAndDelete(commentId); + if (!deleteComment) { + throw new ApiError(500, "Error while deleting comment"); + } + + return res + .status(200) + .json(new ApiResponse(200, deleteComment, "Comment deleted")); +}); + +export { getVideoComments, addComment, updateComment, deleteComment }; diff --git a/src/controllers/dashboard.controller.js b/src/controllers/dashboard.controller.js index dd1748e4..0d556891 100644 --- a/src/controllers/dashboard.controller.js +++ b/src/controllers/dashboard.controller.js @@ -1,20 +1,155 @@ -import mongoose from "mongoose" -import {Video} from "../models/video.model.js" -import {Subscription} from "../models/subscription.model.js" -import {Like} from "../models/like.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" +import mongoose from "mongoose"; +import { Video } from "../models/video.model.js"; +import { Subscription } from "../models/subscription.model.js"; +import { Like } from "../models/like.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const getChannelStats = asyncHandler(async (req, res) => { - // TODO: Get the channel stats like total video views, total subscribers, total videos, total likes etc. -}) + // TODO: Get the channel stats like total video views, total subscribers, total videos, total likes etc. + const userId = req.user._id; + const videoCount = await Video.aggregate([ + { + $match: { + owner: new mongoose.Types.ObjectId(userId), + }, + }, + { + $group: { + _id: "$videoFile", + totalViews: { + $sum: "$views", + }, + totalVideos: { + $sum: 1, + }, + }, + }, + { + $project: { + _id: 0, + totalViews: 1, + totalVideos: 1, + }, + }, + ]); + + const subsCount = await Subscription.aggregate([ + { + $match: { + channel: new mongoose.Types.ObjectId(userId), + }, + }, + { + $group: { + _id: null, + totalSubscribers: { + $sum: 1, + }, + }, + }, + { + $project: { + _id: 0, + totalSubscribers: 1, + }, + }, + ]); + + const likeCount = await Like.aggregate([ + { + $lookup: { + from: "videos", + localField: "video", + foreignField: "_id", + as: "videoInfo", + }, + }, + { + $lookup: { + from: "tweets", + localField: "tweet", + foreignField: "_id", + as: "tweetInfo", + }, + }, + { + $lookup: { + from: "comments", + localField: "comment", + foreignField: "_id", + as: "commentInfo", + }, + }, + { + $match: { + $or: [ + { + "videoInfo.owner": userId, + }, + { + "tweetInfo.owner": userId, + }, + { + "commentInfo.owner": userId, + }, + ], + }, + }, + { + $group: { + _id: null, + totalLikes: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + totalLikes: 1, + }, + }, + ]); + + const info = { + totalViews: videoCount[0].totalViews, + totalVideos: videoCount[0].totalVideos, + totalSubscribers: subsCount[0].totalSubscribers, + totalLikes: likeCount[0].totalLikes, + }; + + return res + .status(200) + .json(new ApiResponse(200, info, "Channel Stats Fetched")); +}); const getChannelVideos = asyncHandler(async (req, res) => { - // TODO: Get all the videos uploaded by the channel -}) + // TODO: Get all the videos uploaded by the channel + const userId = req.user._id; + const videos = await Video.aggregate([ + { + $match: { + owner: new mongoose.Types.ObjectId(userId), + }, + }, + { + $project: { + videoFile: 1, + thumbnail: 1, + title: 1, + duration: 1, + views: 1, + isPublished: 1, + owner: 1, + createdAt: 1, + updatedAt: 1, + }, + }, + ]); + + return res + .status(200) + .json(new ApiResponse(200, videos, "Channel Videos Fetched")); +}); -export { - getChannelStats, - getChannelVideos - } \ No newline at end of file +export { getChannelStats, getChannelVideos }; diff --git a/src/controllers/healthcheck.controller.js b/src/controllers/healthcheck.controller.js index ed5921de..9a871070 100644 --- a/src/controllers/healthcheck.controller.js +++ b/src/controllers/healthcheck.controller.js @@ -1,13 +1,10 @@ -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" - +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const healthcheck = asyncHandler(async (req, res) => { - //TODO: build a healthcheck response that simply returns the OK status as json with a message -}) + //TODO: build a healthcheck response that simply returns the OK status as json with a message + return res.status(200).json(new ApiResponse(200, {}, "OK")); +}); -export { - healthcheck - } - \ No newline at end of file +export { healthcheck }; diff --git a/src/controllers/like.controller.js b/src/controllers/like.controller.js index de76ee15..bd7fa0e3 100644 --- a/src/controllers/like.controller.js +++ b/src/controllers/like.controller.js @@ -1,33 +1,173 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {Like} from "../models/like.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" +import mongoose, { isValidObjectId } from "mongoose"; +import { Like } from "../models/like.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const toggleVideoLike = asyncHandler(async (req, res) => { - const {videoId} = req.params - //TODO: toggle like on video -}) + const { videoId } = req.params; + //TODO: toggle like on video + if (!isValidObjectId(videoId)) { + throw new ApiError(400, "Invalid Video ID"); + } + const user = req.user._id; + const likedVideo = await Like.findOne({ + $and: [{ video: videoId }, { likedBy: user }], + }); + + if (!videoLike) { + const like = await Like.create({ + video: videoId, + likedBy: user, + }); + + if (!like) { + throw new ApiError(500, "Error while liking the video"); + } + + return res + .status(200) + .json(new ApiResponse(200, like, "User Liked the video")); + } + + const unlikeVideo = await Like.findByIdAndDelete(likedVideo._id); + if (!unlikeVideo) { + throw new ApiError(500, "Error while unliking the video"); + } + return res + .status(200) + .json(new ApiResponse(200, unlikeVideo, "User Unliked the video")); +}); const toggleCommentLike = asyncHandler(async (req, res) => { - const {commentId} = req.params - //TODO: toggle like on comment + const { commentId } = req.params; + //TODO: toggle like on comment + if (!isValidObjectId(commentId)) { + throw new ApiError(400, "Invalid Comment ID"); + } + const user = req.user._id; + const likeComment = await Like.findOne({ + $and: [{ comment: commentId }, { likedBy: user }], + }); + if (!likeComment) { + const comment = await Like.create({ + comment: commentId, + likedBy: user, + }); + + if (!comment) { + throw new ApiError(500, "Error while liking comment"); + } -}) + return res.status(200).json(new ApiResponse(200, comment, "Comment Liked")); + } + const unlikeComment = await Like.findByIdAndDelete(likeComment._id); + if (!unlikeComment) { + throw new ApiError(500, "Error while unliking comment"); + } + + return res + .status(200) + .json(new ApiResponse(200, unlikeComment, "Comment deleted")); +}); const toggleTweetLike = asyncHandler(async (req, res) => { - const {tweetId} = req.params - //TODO: toggle like on tweet -} -) + const { tweetId } = req.params; + //TODO: toggle like on tweet + if (!isValidObjectId(tweetId)) { + throw new ApiError(400, "Invalid Tweet ID"); + } + const user = req.user._id; + const likeTweet = await Like.findOne({ + $and: [{ tweet: tweetId }, { likedBy: user }], + }); + if (!likeTweet) { + const like = await Like.create({ + tweet: tweetId, + likedBy: user, + }); + if (!like) { + throw new ApiError(500, "Error while Liking the Tweet"); + } + return res.status(200).json(new ApiResponse(200, like, "Tweet Liked")); + } + const unlikeTweet = await Like.findByIdAndDelete(likeTweet._id); + if (!unlikeTweet) { + throw new ApiError(500, "Error while unliking the Tweet"); + } + + return res + .status(200) + .json(new ApiResponse(200, unlikeTweet, "Tweet Unliked")); +}); const getLikedVideos = asyncHandler(async (req, res) => { - //TODO: get all liked videos -}) - -export { - toggleCommentLike, - toggleTweetLike, - toggleVideoLike, - getLikedVideos -} \ No newline at end of file + //TODO: get all liked videos + const likedVideos = await Like.aggregate([ + { + $match: { + likedBy: new mongoose.Types.ObjectId(req.user._id), + video: { $exists: true, $ne: null }, + }, + }, + { + $lookup: { + from: "videos", + localField: "video", + foreignField: "_id", + as: "video", + pipeline: [ + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "owner", + pipeline: [ + { + $project: { + avatar: 1, + username: 1, + fullName: 1, + }, + }, + ], + }, + }, + { + $addFields: { + owner: { + $first: "$owner", + }, + }, + }, + { + $project: { + videoFile: 1, + thumbnail: 1, + title: 1, + duration: 1, + views: 1, + owner: 1, + }, + }, + ], + }, + }, + { + $unwind: "$video", + }, + { + $project: { + video: 1, + likedBy: 1, + }, + }, + ]); + + return res + .status(200) + .json(new ApiResponse(200, likedVideos, "Fetched Liked Videos")); +}); + +export { toggleCommentLike, toggleTweetLike, toggleVideoLike, getLikedVideos }; diff --git a/src/controllers/playlist.controller.js b/src/controllers/playlist.controller.js index 18d8c2dd..8852fbb9 100644 --- a/src/controllers/playlist.controller.js +++ b/src/controllers/playlist.controller.js @@ -1,53 +1,398 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {Playlist} from "../models/playlist.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" - +import mongoose, { isValidObjectId } from "mongoose"; +import { Playlist } from "../models/playlist.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const createPlaylist = asyncHandler(async (req, res) => { - const {name, description} = req.body + const { name, description } = req.body; + + //TODO: create playlist + if (!name || !description) { + throw new ApiError(400, "All Fields are required"); + } + + const existingPlaylist = await Playlist.findOne({ + $and: [{ name: name }, { owner: req.user._id }], + }); + + if (existingPlaylist) { + throw new ApiError(400, "Playlist with this name already exists"); + } + + const playlist = await Playlist.create({ + name, + description, + owner: req.user._id, + }); - //TODO: create playlist -}) + if (!playlist) { + throw new ApiError(500, "Error while creating playlist"); + } + + return res + .status(200) + .json(new ApiResponse(200, playlist, "Playlist Created")); +}); const getUserPlaylists = asyncHandler(async (req, res) => { - const {userId} = req.params - //TODO: get user playlists -}) + const { userId } = req.params; + //TODO: get user playlists + if (!isValidObjectId(userId)) { + throw new ApiError(400, "Invalid User ID"); + } + + const userPlaylists = await Playlist.aggregate([ + { + $match: { + owner: new mongoose.Types.ObjectId(userId), + }, + }, + { + $lookup: { + from: "videos", + localField: "videos", + foreignField: "_id", + as: "videos", + pipeline: [ + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "owner", + pipeline: [ + { + $project: { + fullName: 1, + username: 1, + avatar: 1, + }, + }, + ], + }, + }, + { + $addFields: { + owner: { + $first: "$owner", + }, + }, + }, + { + $project: { + title: 1, + thumbnail: 1, + description: 1, + owner: 1, + }, + }, + ], + }, + }, + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "createdBy", + pipeline: [ + { + $project: { + avatar: 1, + fullName: 1, + username: 1, + }, + }, + ], + }, + }, + { + $addFields: { + createdBy: { + $first: "$createdBy", + }, + }, + }, + { + $project: { + videos: 1, + createdBy: 1, + name: 1, + description: 1, + }, + }, + ]).toArray(); + if (userPlaylists.length === 0) { + throw new ApiError(504, "No Playlists found"); + } + + return res + .status(200) + .json(new ApiResponse(200, userPlaylists, "Playlists Fetched")); +}); const getPlaylistById = asyncHandler(async (req, res) => { - const {playlistId} = req.params - //TODO: get playlist by id -}) + const { playlistId } = req.params; + //TODO: get playlist by id + if (!isValidObjectId(playlistId)) { + throw new ApiError(400, "Invalid playlist ID"); + } + + const playlist = await Playlist.aggregate([ + { + $match: { + _id: new mongoose.Types.ObjectId(playlistId), + }, + }, + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "createdBy", + pipeline: [ + { + $project: { + fullName: 1, + username: 1, + avatar: 1, + }, + }, + ], + }, + }, + { + $addFields: { + createdBy: { + $first: "$createdBy", + }, + }, + }, + { + $lookup: { + from: "videos", + localField: "videos", + foreignField: "_id", + as: "videos", + pipeline: [ + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "owner", + pipeline: [ + { + $project: { + fullName: 1, + username: 1, + avatar: 1, + }, + }, + ], + }, + }, + { + $addFields: { + owner: { + $first: "$owner", + }, + }, + }, + { + $project: { + thumbnail: 1, + title: 1, + duration: 1, + views: 1, + owner: 1, + createdAt: 1, + updatedAt: 1, + }, + }, + ], + }, + }, + { + $project: { + videos: 1, + description: 1, + name: 1, + createdBy: 1, + }, + }, + ]); + + if (!playlist) { + throw new ApiError(500, "Error fetching playlist"); + } + + return res + .status(200) + .json(new ApiResponse(200, playlist, "Playlist Fetched")); +}); const addVideoToPlaylist = asyncHandler(async (req, res) => { - const {playlistId, videoId} = req.params -}) + const { playlistId, videoId } = req.params; + if (!isValidObjectId(playlistId) || !isValidObjectId(videoId)) { + throw new ApiError(400, "Invalid Playlist or Video ID"); + } + + const playlist = await Playlist.findById(playlistId); + if (!playlist) { + throw new ApiError(400, "No Playlist found"); + } + if (playlist.owner.toString() !== req.user._id) { + throw new ApiError(403, "You are not allowed to modify this playlist"); + } + + const videoExists = playlist.videos.filter( + (video) => video.toString() === videoId + ); + + if (videoExists.length > 0) { + throw new ApiError(400, "Video already in the Playlist"); + } + + const addVideo = await Playlist.findByIdAndUpdate( + playlistId, + { + $set: { + videos: [...playlist.videos, videoId], + }, + }, + { new: true } + ); + + if (!addVideo) { + throw new ApiError(500, "Error while adding video to playlist"); + } + + return res + .status(200) + .json(new ApiResponse(200, addVideo, "Video Added to Playlist")); +}); const removeVideoFromPlaylist = asyncHandler(async (req, res) => { - const {playlistId, videoId} = req.params - // TODO: remove video from playlist + const { playlistId, videoId } = req.params; + // TODO: remove video from playlist + if (!isValidObjectId(videoId) || !isValidObjectId(playlistId)) { + throw new ApiError(400, "Invalid Video or Playlist ID"); + } + + const playlist = await Playlist.findById(playlistId); + if (!playlist) { + throw new ApiError(400, "No Playlist found with the ID"); + } -}) + if (playlist.owner.toString() !== req.user._id) { + throw new ApiError(403, "You are not allowed to modify this playlist"); + } + + const videoExists = playlist.videos.find( + (video) => video.toString() === videoId + ); + + if (!videoExists) { + throw new ApiError(400, "No video found with the ID in the playlist"); + } + + const modifiedPlaylistVideos = playlist.videos.filter( + (video) => video.toString() !== videoId + ); + + const removeVideo = await Playlist.findByIdAndUpdate( + playlistId, + { + $set: { + videos: modifiedPlaylistVideos, + }, + }, + { new: true } + ); + + if (!removeVideo) { + throw new ApiError(500, "Error while removing video"); + } + + return res + .status(200) + .json(new ApiResponse(200, removeVideo, "Video removed from playlist")); +}); const deletePlaylist = asyncHandler(async (req, res) => { - const {playlistId} = req.params - // TODO: delete playlist -}) + const { playlistId } = req.params; + // TODO: delete playlist + if (!isValidObjectId(playlistId)) { + throw new ApiError(400, "Invalid Playlist ID"); + } + + const playlist = await Playlist.findById(playlistId); + if (!playlist) { + throw new ApiError(400, "No Playlist found with this ID"); + } + + if (playlist.owner.toString() !== req.user._id) { + throw new ApiError(403, "You are not allowed to delete this playlist"); + } + + const deletePlaylist = await Playlist.findByIdAndDelete(playlist._id); + + if (!deletePlaylist) { + throw new ApiError(500, "Error while deleting playlist"); + } + + return res.status(200).json(new ApiResponse(200, {}, "Playlist Deleted")); +}); const updatePlaylist = asyncHandler(async (req, res) => { - const {playlistId} = req.params - const {name, description} = req.body - //TODO: update playlist -}) + const { playlistId } = req.params; + const { name, description } = req.body; + //TODO: update playlist + + if (!name || !description) { + throw new ApiError(400, "All Fields are required"); + } + if (!isValidObjectId(playlistId)) { + throw new ApiError(400, "Invalid Playlist ID"); + } + + const playlist = await Playlist.findById(playlistId); + if (!playlist) { + throw new ApiError(400, "No Playlist found with this ID"); + } + + if (playlist.owner.toString() !== req.user._id) { + throw new ApiError(403, "You are not allowed to modify this playlist"); + } + + const updatePlaylist = await Playlist.findByIdAndUpdate( + playlistId, + { + $set: { + name, + description, + }, + }, + { new: true } + ); + + if (!updatePlaylist) { + throw new ApiError(500, "Error while updating playlist"); + } + + return res + .status(200) + .json(new ApiResponse(200, updatePlaylist, "Playlist Updated")); +}); export { - createPlaylist, - getUserPlaylists, - getPlaylistById, - addVideoToPlaylist, - removeVideoFromPlaylist, - deletePlaylist, - updatePlaylist -} + createPlaylist, + getUserPlaylists, + getPlaylistById, + addVideoToPlaylist, + removeVideoFromPlaylist, + deletePlaylist, + updatePlaylist, +}; diff --git a/src/controllers/subscription.controller.js b/src/controllers/subscription.controller.js index c89d9956..b28e1516 100644 --- a/src/controllers/subscription.controller.js +++ b/src/controllers/subscription.controller.js @@ -1,28 +1,148 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {User} from "../models/user.model.js" -import { Subscription } from "../models/subscription.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" - +import mongoose, { isValidObjectId } from "mongoose"; +import { User } from "../models/user.model.js"; +import { Subscription } from "../models/subscription.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const toggleSubscription = asyncHandler(async (req, res) => { - const {channelId} = req.params - // TODO: toggle subscription -}) + const { channelId } = req.params; + // TODO: toggle subscription + if (!isValidObjectId(channelId)) { + throw new ApiError(400, "Invalid Channel ID"); + } + const subscribed = await Subscription.findOne({ + $and: [{ channel: channelId }, { subscriber: req.user._id }], + }); + if (!subscribed) { + const subscribe = await Subscription.create({ + subscriber: req.user._id, + channel: channelId, + }); + if (!subscribe) { + throw new ApiError(500, "Error while Subscribing"); + } + + return res + .status(200) + .json(new ApiResponse(200, subscribe, "Channel Subscribed")); + } + + const unsubscribe = await Subscription.findByIdAndDelete(subscribed._id); + if (!unsubscribe) { + throw new ApiError(500, "Error while Unsubscribing"); + } + + return res.status(200).json(new ApiResponse(200, {}, "Channel Unsubscribed")); +}); // controller to return subscriber list of a channel const getUserChannelSubscribers = asyncHandler(async (req, res) => { - const {channelId} = req.params -}) + const { subscriberId } = req.params; + if (!isValidObjectId(subscriberId)) { + throw new ApiError(400, "Invalid Subscriber ID"); + } + const subscribersList = await Subscription.aggregate([ + { + $match: { + channel: new mongoose.Types.ObjectId(subscriberId), + }, + }, + { + $lookup: { + from: "users", + localField: "subscriber", + foreignField: "_id", + as: "subscriber", + pipeline: [ + { + $project: { + username: 1, + fullName: 1, + avatar: 1, + }, + }, + ], + }, + }, + { + $addFields: { + subscriber: { + $first: "$subscriber", + }, + }, + }, + { + $project: { + subscriber: 1, + createdAt: 1, + }, + }, + ]); + + if (!subscribersList) { + throw new ApiError(400, "Error Fetching Subscribers List"); + } + + return res + .status(200) + .json( + new ApiResponse(200, subscribersList, "Subscribers Fetched Successfully") + ); +}); // controller to return channel list to which user has subscribed const getSubscribedChannels = asyncHandler(async (req, res) => { - const { subscriberId } = req.params -}) - -export { - toggleSubscription, - getUserChannelSubscribers, - getSubscribedChannels -} \ No newline at end of file + const { channelId } = req.params; + if (!isValidObjectId(channelId)) { + throw new ApiError(400, "Invalid Channel ID"); + } + + const channelList = await Subscription.aggregate([ + { + $match: { + subscriber: channelId, + }, + }, + { + $lookup: { + from: "users", + localField: "channel", + foreignField: "_id", + as: "channel", + pipeline: [ + { + $project: { + fullName: 1, + username: 1, + avatar: 1, + }, + }, + ], + }, + }, + { + $addFields: { + channel: { + $first: "$channel", + }, + }, + }, + { + $project: { + channel: 1, + createdAt: 1, + }, + }, + ]); + + if (!channelList) { + throw new ApiError(400, "Error Fetching Subscribed Channels"); + } + + return res + .status(200) + .json(new ApiResponse(200, channelList, "Subscribed Channels Fetched")); +}); + +export { toggleSubscription, getUserChannelSubscribers, getSubscribedChannels }; diff --git a/src/controllers/tweet.controller.js b/src/controllers/tweet.controller.js index 21b001d4..1f39509c 100644 --- a/src/controllers/tweet.controller.js +++ b/src/controllers/tweet.controller.js @@ -1,29 +1,102 @@ -import mongoose, { isValidObjectId } from "mongoose" -import {Tweet} from "../models/tweet.model.js" -import {User} from "../models/user.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" +import mongoose, { isValidObjectId } from "mongoose"; +import { Tweet } from "../models/tweet.model.js"; +import { User } from "../models/user.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; const createTweet = asyncHandler(async (req, res) => { - //TODO: create tweet -}) + //TODO: create tweet + const { content } = req.body; + + if (!content) { + throw new ApiError(400, "Tweet Content is missing!"); + } + + const tweet = await Tweet.create({ + content, + owner: req.user._id, + }); + + if (!tweet) { + throw new ApiError(400, "Error while creating the tweet"); + } + + return res + .status(200) + .json(new ApiResponse(200, tweet, "Tweet created successfully")); +}); const getUserTweets = asyncHandler(async (req, res) => { - // TODO: get user tweets -}) + // TODO: get user tweets + const { userId } = req.params; + if (!isValidObjectId(userId)) { + throw new ApiError(400, "Invalid user id"); + } + const tweets = await Tweet.find({ owner: userId }); + + if (tweets.length === 0) { + throw new ApiError(404, "No tweets found"); + } + + return res + .status(200) + .json(new ApiResponse(200, tweets, "Tweets fetched successfully")); +}); const updateTweet = asyncHandler(async (req, res) => { - //TODO: update tweet -}) + //TODO: update tweet + const { content } = req.body; + if (!content) { + throw new ApiError(400, "Content field is missing"); + } + const { tweetId } = req.params; + const tweet = await Tweet.findById(tweetId); + if (!tweet) { + throw new ApiError(400, "Tweet ID is invalid"); + } + const owner = req.user._id; + if (tweet?.owner !== owner) { + throw new ApiError(400, "You are not allowed to modify this tweet"); + } + + const modifiedTweet = await Tweet.findByIdAndUpdate( + tweetId, + { + $set: { + content, + }, + }, + { new: true } + ); + + return res + .status(200) + .json(new ApiResponse(201, modifiedTweet, "Tweet updated successfully")); +}); const deleteTweet = asyncHandler(async (req, res) => { - //TODO: delete tweet -}) - -export { - createTweet, - getUserTweets, - updateTweet, - deleteTweet -} + //TODO: delete tweet + const { tweetId } = req.params; + if (!isValidObjectId(tweetId)) { + throw new ApiError(400, "Invalid Tweet ID"); + } + const tweet = await Tweet.findById(tweetId); + if (!tweet) { + throw new ApiError(404, "Tweet not found"); + } + if (tweet.owner !== req.user._id) { + throw new ApiError(403, "You are not allowed to delete this tweet"); + } + + const response = await Tweet.findByIdAndDelete(tweetId); + if (!response) { + throw new ApiError(400, "Something went wrong while deleting the tweet"); + } + + return response + .status(200) + .json(new ApiResponse(200, {}, "Tweet deleted successfully")); +}); + +export { createTweet, getUserTweets, updateTweet, deleteTweet }; diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index db473568..8bcd2b2b 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -1,496 +1,491 @@ import { asyncHandler } from "../utils/asyncHandler.js"; -import {ApiError} from "../utils/ApiError.js" -import { User} from "../models/user.model.js" -import {uploadOnCloudinary} from "../utils/cloudinary.js" +import { ApiError } from "../utils/ApiError.js"; +import { User } from "../models/user.model.js"; +import { + uploadOnCloudinary, + deleteFromCloudinary, +} from "../utils/cloudinary.js"; import { ApiResponse } from "../utils/ApiResponse.js"; -import jwt from "jsonwebtoken" +import jwt from "jsonwebtoken"; import mongoose from "mongoose"; +const generateAccessAndRefereshTokens = async (userId) => { + try { + const user = await User.findById(userId); + const accessToken = user.generateAccessToken(); + const refreshToken = user.generateRefreshToken(); + + user.refreshToken = refreshToken; + await user.save({ validateBeforeSave: false }); + + return { accessToken, refreshToken }; + } catch (error) { + throw new ApiError( + 500, + "Something went wrong while generating referesh and access token" + ); + } +}; + +const registerUser = asyncHandler(async (req, res) => { + // get user details from frontend + // validation - not empty + // check if user already exists: username, email + // check for images, check for avatar + // upload them to cloudinary, avatar + // create user object - create entry in db + // remove password and refresh token field from response + // check for user creation + // return res + + const { fullName, email, username, password } = req.body; + //console.log("email: ", email); + + if ( + [fullName, email, username, password].some((field) => field?.trim() === "") + ) { + throw new ApiError(400, "All fields are required"); + } + + const existedUser = await User.findOne({ + $or: [{ username }, { email }], + }); + + if (existedUser) { + throw new ApiError(409, "User with email or username already exists"); + } + //console.log(req.files); + + const avatarLocalPath = req.files?.avatar[0]?.path; + //const coverImageLocalPath = req.files?.coverImage[0]?.path; + + let coverImageLocalPath; + if ( + req.files && + Array.isArray(req.files.coverImage) && + req.files.coverImage.length > 0 + ) { + coverImageLocalPath = req.files.coverImage[0].path; + } + + if (!avatarLocalPath) { + throw new ApiError(400, "Avatar file is required"); + } + + const avatar = await uploadOnCloudinary(avatarLocalPath); + const coverImage = await uploadOnCloudinary(coverImageLocalPath); + + if (!avatar) { + throw new ApiError(400, "Avatar file is required"); + } + + const user = await User.create({ + fullName, + avatar: avatar.url, + coverImage: coverImage?.url || "", + email, + password, + username: username.toLowerCase(), + }); + + const createdUser = await User.findById(user._id).select( + "-password -refreshToken" + ); + + if (!createdUser) { + throw new ApiError(500, "Something went wrong while registering the user"); + } + + return res + .status(201) + .json(new ApiResponse(200, createdUser, "User registered Successfully")); +}); -const generateAccessAndRefereshTokens = async(userId) =>{ - try { - const user = await User.findById(userId) - const accessToken = user.generateAccessToken() - const refreshToken = user.generateRefreshToken() - - user.refreshToken = refreshToken - await user.save({ validateBeforeSave: false }) - - return {accessToken, refreshToken} - - - } catch (error) { - throw new ApiError(500, "Something went wrong while generating referesh and access token") - } -} - -const registerUser = asyncHandler( async (req, res) => { - // get user details from frontend - // validation - not empty - // check if user already exists: username, email - // check for images, check for avatar - // upload them to cloudinary, avatar - // create user object - create entry in db - // remove password and refresh token field from response - // check for user creation - // return res - - - const {fullName, email, username, password } = req.body - //console.log("email: ", email); - - if ( - [fullName, email, username, password].some((field) => field?.trim() === "") - ) { - throw new ApiError(400, "All fields are required") - } - - const existedUser = await User.findOne({ - $or: [{ username }, { email }] - }) - - if (existedUser) { - throw new ApiError(409, "User with email or username already exists") - } - //console.log(req.files); - - const avatarLocalPath = req.files?.avatar[0]?.path; - //const coverImageLocalPath = req.files?.coverImage[0]?.path; - - let coverImageLocalPath; - if (req.files && Array.isArray(req.files.coverImage) && req.files.coverImage.length > 0) { - coverImageLocalPath = req.files.coverImage[0].path - } - - - if (!avatarLocalPath) { - throw new ApiError(400, "Avatar file is required") - } - - const avatar = await uploadOnCloudinary(avatarLocalPath) - const coverImage = await uploadOnCloudinary(coverImageLocalPath) - - if (!avatar) { - throw new ApiError(400, "Avatar file is required") - } - - - const user = await User.create({ - fullName, - avatar: avatar.url, - coverImage: coverImage?.url || "", - email, - password, - username: username.toLowerCase() - }) - - const createdUser = await User.findById(user._id).select( - "-password -refreshToken" - ) - - if (!createdUser) { - throw new ApiError(500, "Something went wrong while registering the user") - } - - return res.status(201).json( - new ApiResponse(200, createdUser, "User registered Successfully") - ) +const loginUser = asyncHandler(async (req, res) => { + // req body -> data + // username or email + //find the user + //password check + //access and referesh token + //send cookie -} ) + const { email, username, password } = req.body; + console.log(email); -const loginUser = asyncHandler(async (req, res) =>{ - // req body -> data - // username or email - //find the user - //password check - //access and referesh token - //send cookie + if (!username && !email) { + throw new ApiError(400, "username or email is required"); + } - const {email, username, password} = req.body - console.log(email); + // Here is an alternative of above code based on logic discussed in video: + // if (!(username || email)) { + // throw new ApiError(400, "username or email is required") - if (!username && !email) { - throw new ApiError(400, "username or email is required") - } - - // Here is an alternative of above code based on logic discussed in video: - // if (!(username || email)) { - // throw new ApiError(400, "username or email is required") - - // } + // } - const user = await User.findOne({ - $or: [{username}, {email}] - }) + const user = await User.findOne({ + $or: [{ username }, { email }], + }); - if (!user) { - throw new ApiError(404, "User does not exist") - } + if (!user) { + throw new ApiError(404, "User does not exist"); + } - const isPasswordValid = await user.isPasswordCorrect(password) + const isPasswordValid = await user.isPasswordCorrect(password); - if (!isPasswordValid) { - throw new ApiError(401, "Invalid user credentials") - } + if (!isPasswordValid) { + throw new ApiError(401, "Invalid user credentials"); + } - const {accessToken, refreshToken} = await generateAccessAndRefereshTokens(user._id) + const { accessToken, refreshToken } = await generateAccessAndRefereshTokens( + user._id + ); - const loggedInUser = await User.findById(user._id).select("-password -refreshToken") + const loggedInUser = await User.findById(user._id).select( + "-password -refreshToken" + ); - const options = { - httpOnly: true, - secure: true - } + const options = { + httpOnly: true, + secure: true, + }; - return res + return res .status(200) .cookie("accessToken", accessToken, options) .cookie("refreshToken", refreshToken, options) .json( - new ApiResponse( - 200, - { - user: loggedInUser, accessToken, refreshToken - }, - "User logged In Successfully" - ) - ) - -}) - -const logoutUser = asyncHandler(async(req, res) => { - await User.findByIdAndUpdate( - req.user._id, + new ApiResponse( + 200, { - $unset: { - refreshToken: 1 // this removes the field from document - } + user: loggedInUser, + accessToken, + refreshToken, }, - { - new: true - } - ) + "User logged In Successfully" + ) + ); +}); - const options = { - httpOnly: true, - secure: true +const logoutUser = asyncHandler(async (req, res) => { + await User.findByIdAndUpdate( + req.user._id, + { + $unset: { + refreshToken: 1, // this removes the field from document + }, + }, + { + new: true, } + ); - return res + const options = { + httpOnly: true, + secure: true, + }; + + return res .status(200) .clearCookie("accessToken", options) .clearCookie("refreshToken", options) - .json(new ApiResponse(200, {}, "User logged Out")) -}) + .json(new ApiResponse(200, {}, "User logged Out")); +}); const refreshAccessToken = asyncHandler(async (req, res) => { - const incomingRefreshToken = req.cookies.refreshToken || req.body.refreshToken + const incomingRefreshToken = + req.cookies.refreshToken || req.body.refreshToken; + + if (!incomingRefreshToken) { + throw new ApiError(401, "unauthorized request"); + } - if (!incomingRefreshToken) { - throw new ApiError(401, "unauthorized request") + try { + const decodedToken = jwt.verify( + incomingRefreshToken, + process.env.REFRESH_TOKEN_SECRET + ); + + const user = await User.findById(decodedToken?._id); + + if (!user) { + throw new ApiError(401, "Invalid refresh token"); } - try { - const decodedToken = jwt.verify( - incomingRefreshToken, - process.env.REFRESH_TOKEN_SECRET - ) - - const user = await User.findById(decodedToken?._id) - - if (!user) { - throw new ApiError(401, "Invalid refresh token") - } - - if (incomingRefreshToken !== user?.refreshToken) { - throw new ApiError(401, "Refresh token is expired or used") - - } - - const options = { - httpOnly: true, - secure: true - } - - const {accessToken, newRefreshToken} = await generateAccessAndRefereshTokens(user._id) - - return res - .status(200) - .cookie("accessToken", accessToken, options) - .cookie("refreshToken", newRefreshToken, options) - .json( - new ApiResponse( - 200, - {accessToken, refreshToken: newRefreshToken}, - "Access token refreshed" - ) - ) - } catch (error) { - throw new ApiError(401, error?.message || "Invalid refresh token") + if (incomingRefreshToken !== user?.refreshToken) { + throw new ApiError(401, "Refresh token is expired or used"); } -}) + const options = { + httpOnly: true, + secure: true, + }; + + const { accessToken, newRefreshToken } = + await generateAccessAndRefereshTokens(user._id); -const changeCurrentPassword = asyncHandler(async(req, res) => { - const {oldPassword, newPassword} = req.body + return res + .status(200) + .cookie("accessToken", accessToken, options) + .cookie("refreshToken", newRefreshToken, options) + .json( + new ApiResponse( + 200, + { accessToken, refreshToken: newRefreshToken }, + "Access token refreshed" + ) + ); + } catch (error) { + throw new ApiError(401, error?.message || "Invalid refresh token"); + } +}); - +const changeCurrentPassword = asyncHandler(async (req, res) => { + const { oldPassword, newPassword } = req.body; - const user = await User.findById(req.user?._id) - const isPasswordCorrect = await user.isPasswordCorrect(oldPassword) + const user = await User.findById(req.user?._id); + const isPasswordCorrect = await user.isPasswordCorrect(oldPassword); - if (!isPasswordCorrect) { - throw new ApiError(400, "Invalid old password") - } + if (!isPasswordCorrect) { + throw new ApiError(400, "Invalid old password"); + } - user.password = newPassword - await user.save({validateBeforeSave: false}) + user.password = newPassword; + await user.save({ validateBeforeSave: false }); - return res + return res .status(200) - .json(new ApiResponse(200, {}, "Password changed successfully")) -}) - + .json(new ApiResponse(200, {}, "Password changed successfully")); +}); -const getCurrentUser = asyncHandler(async(req, res) => { - return res +const getCurrentUser = asyncHandler(async (req, res) => { + return res .status(200) - .json(new ApiResponse( - 200, - req.user, - "User fetched successfully" - )) -}) + .json(new ApiResponse(200, req.user, "User fetched successfully")); +}); -const updateAccountDetails = asyncHandler(async(req, res) => { - const {fullName, email} = req.body +const updateAccountDetails = asyncHandler(async (req, res) => { + const { fullName, email } = req.body; - if (!fullName || !email) { - throw new ApiError(400, "All fields are required") - } + if (!fullName || !email) { + throw new ApiError(400, "All fields are required"); + } - const user = await User.findByIdAndUpdate( - req.user?._id, - { - $set: { - fullName, - email: email - } - }, - {new: true} - - ).select("-password") + const user = await User.findByIdAndUpdate( + req.user?._id, + { + $set: { + fullName, + email: email, + }, + }, + { new: true } + ).select("-password"); - return res + return res .status(200) - .json(new ApiResponse(200, user, "Account details updated successfully")) + .json(new ApiResponse(200, user, "Account details updated successfully")); }); -const updateUserAvatar = asyncHandler(async(req, res) => { - const avatarLocalPath = req.file?.path +const updateUserAvatar = asyncHandler(async (req, res) => { + const avatarLocalPath = req.file?.path; - if (!avatarLocalPath) { - throw new ApiError(400, "Avatar file is missing") - } + if (!avatarLocalPath) { + throw new ApiError(400, "Avatar file is missing"); + } - //TODO: delete old image - assignment + //TODO: delete old image - assignment + const oldAvatar = req.user?.avatar; + oldAvatar && (await deleteFromCloudinary(oldAvatar)); - const avatar = await uploadOnCloudinary(avatarLocalPath) + const avatar = await uploadOnCloudinary(avatarLocalPath); - if (!avatar.url) { - throw new ApiError(400, "Error while uploading on avatar") - - } + if (!avatar.url) { + throw new ApiError(400, "Error while uploading on avatar"); + } - const user = await User.findByIdAndUpdate( - req.user?._id, - { - $set:{ - avatar: avatar.url - } - }, - {new: true} - ).select("-password") + const user = await User.findByIdAndUpdate( + req.user?._id, + { + $set: { + avatar: avatar.url, + }, + }, + { new: true } + ).select("-password"); - return res + return res .status(200) - .json( - new ApiResponse(200, user, "Avatar image updated successfully") - ) -}) + .json(new ApiResponse(200, user, "Avatar image updated successfully")); +}); -const updateUserCoverImage = asyncHandler(async(req, res) => { - const coverImageLocalPath = req.file?.path +const updateUserCoverImage = asyncHandler(async (req, res) => { + const coverImageLocalPath = req.file?.path; - if (!coverImageLocalPath) { - throw new ApiError(400, "Cover image file is missing") - } + if (!coverImageLocalPath) { + throw new ApiError(400, "Cover image file is missing"); + } - //TODO: delete old image - assignment + //TODO: delete old image - assignment + const oldCoverImage = req.user?.coverImage; + oldCoverImage !== "" && (await deleteFromCloudinary(oldCoverImage)); - const coverImage = await uploadOnCloudinary(coverImageLocalPath) + const coverImage = await uploadOnCloudinary(coverImageLocalPath); - if (!coverImage.url) { - throw new ApiError(400, "Error while uploading on avatar") - - } + if (!coverImage.url) { + throw new ApiError(400, "Error while uploading on avatar"); + } - const user = await User.findByIdAndUpdate( - req.user?._id, - { - $set:{ - coverImage: coverImage.url - } - }, - {new: true} - ).select("-password") + const user = await User.findByIdAndUpdate( + req.user?._id, + { + $set: { + coverImage: coverImage.url, + }, + }, + { new: true } + ).select("-password"); - return res + return res .status(200) - .json( - new ApiResponse(200, user, "Cover image updated successfully") - ) -}) - - -const getUserChannelProfile = asyncHandler(async(req, res) => { - const {username} = req.params - - if (!username?.trim()) { - throw new ApiError(400, "username is missing") - } + .json(new ApiResponse(200, user, "Cover image updated successfully")); +}); - const channel = await User.aggregate([ - { - $match: { - username: username?.toLowerCase() - } +const getUserChannelProfile = asyncHandler(async (req, res) => { + const { username } = req.params; + + if (!username?.trim()) { + throw new ApiError(400, "username is missing"); + } + + const channel = await User.aggregate([ + { + $match: { + username: username?.toLowerCase(), + }, + }, + { + $lookup: { + from: "subscriptions", + localField: "_id", + foreignField: "channel", + as: "subscribers", + }, + }, + { + $lookup: { + from: "subscriptions", + localField: "_id", + foreignField: "subscriber", + as: "subscribedTo", + }, + }, + { + $addFields: { + subscribersCount: { + $size: "$subscribers", }, - { - $lookup: { - from: "subscriptions", - localField: "_id", - foreignField: "channel", - as: "subscribers" - } + channelsSubscribedToCount: { + $size: "$subscribedTo", }, - { - $lookup: { - from: "subscriptions", - localField: "_id", - foreignField: "subscriber", - as: "subscribedTo" - } - }, - { - $addFields: { - subscribersCount: { - $size: "$subscribers" - }, - channelsSubscribedToCount: { - $size: "$subscribedTo" - }, - isSubscribed: { - $cond: { - if: {$in: [req.user?._id, "$subscribers.subscriber"]}, - then: true, - else: false - } - } - } + isSubscribed: { + $cond: { + if: { $in: [req.user?._id, "$subscribers.subscriber"] }, + then: true, + else: false, + }, }, - { - $project: { - fullName: 1, - username: 1, - subscribersCount: 1, - channelsSubscribedToCount: 1, - isSubscribed: 1, - avatar: 1, - coverImage: 1, - email: 1 - - } - } - ]) - - if (!channel?.length) { - throw new ApiError(404, "channel does not exists") - } - - return res + }, + }, + { + $project: { + fullName: 1, + username: 1, + subscribersCount: 1, + channelsSubscribedToCount: 1, + isSubscribed: 1, + avatar: 1, + coverImage: 1, + email: 1, + }, + }, + ]); + + if (!channel?.length) { + throw new ApiError(404, "channel does not exists"); + } + + return res .status(200) .json( - new ApiResponse(200, channel[0], "User channel fetched successfully") - ) -}) + new ApiResponse(200, channel[0], "User channel fetched successfully") + ); +}); -const getWatchHistory = asyncHandler(async(req, res) => { - const user = await User.aggregate([ - { - $match: { - _id: new mongoose.Types.ObjectId(req.user._id) - } - }, - { +const getWatchHistory = asyncHandler(async (req, res) => { + const user = await User.aggregate([ + { + $match: { + _id: new mongoose.Types.ObjectId(req.user._id), + }, + }, + { + $lookup: { + from: "videos", + localField: "watchHistory", + foreignField: "_id", + as: "watchHistory", + pipeline: [ + { $lookup: { - from: "videos", - localField: "watchHistory", - foreignField: "_id", - as: "watchHistory", - pipeline: [ - { - $lookup: { - from: "users", - localField: "owner", - foreignField: "_id", - as: "owner", - pipeline: [ - { - $project: { - fullName: 1, - username: 1, - avatar: 1 - } - } - ] - } - }, - { - $addFields:{ - owner:{ - $first: "$owner" - } - } - } - ] - } - } - ]) + from: "users", + localField: "owner", + foreignField: "_id", + as: "owner", + pipeline: [ + { + $project: { + fullName: 1, + username: 1, + avatar: 1, + }, + }, + ], + }, + }, + { + $addFields: { + owner: { + $first: "$owner", + }, + }, + }, + ], + }, + }, + ]); - return res + return res .status(200) .json( - new ApiResponse( - 200, - user[0].watchHistory, - "Watch history fetched successfully" - ) - ) -}) - + new ApiResponse( + 200, + user[0].watchHistory, + "Watch history fetched successfully" + ) + ); +}); export { - registerUser, - loginUser, - logoutUser, - refreshAccessToken, - changeCurrentPassword, - getCurrentUser, - updateAccountDetails, - updateUserAvatar, - updateUserCoverImage, - getUserChannelProfile, - getWatchHistory -} \ No newline at end of file + registerUser, + loginUser, + logoutUser, + refreshAccessToken, + changeCurrentPassword, + getCurrentUser, + updateAccountDetails, + updateUserAvatar, + updateUserCoverImage, + getUserChannelProfile, + getWatchHistory, +}; diff --git a/src/controllers/video.controller.js b/src/controllers/video.controller.js index 78a52627..1f4cf64e 100644 --- a/src/controllers/video.controller.js +++ b/src/controllers/video.controller.js @@ -1,47 +1,274 @@ -import mongoose, {isValidObjectId} from "mongoose" -import {Video} from "../models/video.model.js" -import {User} from "../models/user.model.js" -import {ApiError} from "../utils/ApiError.js" -import {ApiResponse} from "../utils/ApiResponse.js" -import {asyncHandler} from "../utils/asyncHandler.js" -import {uploadOnCloudinary} from "../utils/cloudinary.js" - +import mongoose, { isValidObjectId } from "mongoose"; +import { Video } from "../models/video.model.js"; +import { User } from "../models/user.model.js"; +import { ApiError } from "../utils/ApiError.js"; +import { ApiResponse } from "../utils/ApiResponse.js"; +import { asyncHandler } from "../utils/asyncHandler.js"; +import { + uploadOnCloudinary, + deleteFromCloudinary, +} from "../utils/cloudinary.js"; const getAllVideos = asyncHandler(async (req, res) => { - const { page = 1, limit = 10, query, sortBy, sortType, userId } = req.query - //TODO: get all videos based on query, sort, pagination -}) + const { + page = 1, + limit = 10, + query = " ", + sortBy, + sortType, + userId, + } = req.query; + //TODO: get all videos based on query, sort, pagination + const videos = await Video.aggregate([ + { + $match: { + $or: [ + { + title: { $regex: query, $options: "i" }, + }, + { + description: { $regex: query, $options: "i" }, + }, + ], + }, + }, + { + $lookup: { + from: "users", + localField: "owner", + foreignField: "_id", + as: "createdBy", + }, + }, + { + $unwind: "$createdBy", + }, + { + $project: { + thumbnail: 1, + videoFile: 1, + title: 1, + description: 1, + createdBy: { + fullName: 1, + username: 1, + avatar: 1, + }, + }, + }, + { + $sort: { + [sortBy]: sortType === "asc" ? 1 : -1, + }, + }, + { + $skip: (page - 1) * limit, + }, + { + $limit: parseInt(limit), + }, + ]); + + return res + .status(200) + .json(new ApiResponse(200, videos, "Fetched All Videos")); +}); const publishAVideo = asyncHandler(async (req, res) => { - const { title, description} = req.body - // TODO: get video, upload to cloudinary, create video -}) + const { title, description } = req.body; + // TODO: get video, upload to cloudinary, create video + if (!title || !description) { + throw new ApiError(400, "All Fields are required"); + } + + const videoFileLocalPath = req.files?.videoFile[0]?.path; + if (!videoFileLocalPath) { + throw new ApiError(400, "No video file found"); + } + const videoFile = await uploadOnCloudinary(videoFileLocalPath); + + if (!videoFile.url) { + throw new ApiError(500, "Error while uploading video file"); + } + + const thumbnailLocalPath = req.files?.thumbnail[0]?.path; + if (!thumbnailLocalPath) { + throw new ApiError(400, "No thumbnail file found"); + } + + const thumbnail = await uploadOnCloudinary(thumbnailLocalPath); + + if (!thumbnail.url) { + throw new ApiError(400, "Error while uploading thumbnail file"); + } + + const video = await Video.create({ + videoFile: videoFile.url, + thumbnail: thumbnail.url, + title, + description, + duration: videoFile.duration, + owner: req.user._id, + }); + + if (!video) { + throw new ApiError(500, "Error while publishing the video"); + } + + return res.status(200).json(new ApiResponse(200, video, "Video Published")); +}); const getVideoById = asyncHandler(async (req, res) => { - const { videoId } = req.params - //TODO: get video by id -}) + const { videoId } = req.params; + //TODO: get video by id + if (!isValidObjectId(videoId)) { + throw new ApiError(400, "Invalid Video ID"); + } + + const video = await Video.findById(videoId); + + if (!video) { + throw new ApiError(404, "No video found"); + } + + return res.status(200).json(new ApiResponse(200, video, "Video Fetched")); +}); const updateVideo = asyncHandler(async (req, res) => { - const { videoId } = req.params - //TODO: update video details like title, description, thumbnail + const { videoId } = req.params; + //TODO: update video details like title, description, thumbnail + const { title, description } = req.body; + const newThumbnailLocalPath = req.file?.path; + + if (!isValidObjectId(videoId)) { + throw new ApiError(400, "Invalid Video ID"); + } + if (!title || !description) { + throw new ApiError(400, "Provide updated Title and Description"); + } + if (!newThumbnailLocalPath) { + throw new ApiError(400, "Provide Thumbnail file"); + } + + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } -}) + if (video.owner !== req.user._id) { + throw new ApiError(403, "You are not allowed to update this video"); + } + + const deleteThumbnailResponse = await deleteFromCloudinary(video.thumbnail); + if (deleteThumbnailResponse.result !== "ok") { + throw new ApiError( + 500, + "Error while deleting old thumbnail from cloudinary" + ); + } + + const newThumbnail = await uploadOnCloudinary(newThumbnailLocalPath); + if (!newThumbnail.url) { + throw new ApiError(500, "Error while uploading new thumbnail"); + } + + const updateVideo = await Video.findByIdAndUpdate( + videoId, + { + $set: { + title, + description, + thumbnail: newThumbnail.url, + }, + }, + { new: true } + ); + + return res + .status(200) + .json(new ApiResponse(200, updateVideo, "Video details updated")); +}); const deleteVideo = asyncHandler(async (req, res) => { - const { videoId } = req.params - //TODO: delete video -}) + const { videoId } = req.params; + //TODO: delete video + if (!isValidObjectId(videoId)) { + throw new ApiError(400, "Invalid Video ID"); + } + + const video = await Video.findById(videoId); + if (!video) { + throw new ApiError(404, "Video not found"); + } + + if (video.owner !== req.user._id) { + throw new ApiError(403, "You are not allowed to delete this video"); + } + + const cloudinaryDeleteVideoResponse = await deleteFromCloudinary( + video.videoFile + ); + if (cloudinaryDeleteVideoResponse.result !== "ok") { + throw new ApiError(500, "Error while deleting video from cloudinary"); + } + + const cloudinaryDeleteThumbnailResponse = await deleteFromCloudinary( + video.thumbnail + ); + if (cloudinaryDeleteThumbnailResponse.result !== "ok") { + throw new ApiError(500, "Error while deleting thumbnail from cloudinary"); + } + + const deleteVideo = await Video.findByIdAndDelete(videoId); + if (!deleteVideo) { + throw new ApiError(500, "Error while deleting video"); + } + + return res.status(200).json(new ApiResponse(200, {}, "Video Deleted")); +}); const togglePublishStatus = asyncHandler(async (req, res) => { - const { videoId } = req.params -}) + const { videoId } = req.params; + if (!isValidObjectId(videoId)) { + throw new ApiError(400, "Invalid Video ID"); + } + + const video = await Video.findById(videoId); + + if (!video) { + throw new ApiError(404, "Video Not Found"); + } + + if (video.owner !== req.user._id) { + throw new ApiError(403, "You are not allowed to modify this video status"); + } + + const modifyVideoPublishStatus = await Video.findByIdAndUpdate( + videoId, + { + $set: { + isPublished: !video.isPublished, + }, + }, + { new: true } + ); + + return res + .status(200) + .json( + new ApiResponse( + 200, + modifyVideoPublishStatus, + "Video Publish status modified" + ) + ); +}); export { - getAllVideos, - publishAVideo, - getVideoById, - updateVideo, - deleteVideo, - togglePublishStatus -} + getAllVideos, + publishAVideo, + getVideoById, + updateVideo, + deleteVideo, + togglePublishStatus, +}; diff --git a/src/utils/cloudinary.js b/src/utils/cloudinary.js index 7b67fdc6..d14b2f33 100644 --- a/src/utils/cloudinary.js +++ b/src/utils/cloudinary.js @@ -1,31 +1,39 @@ -import {v2 as cloudinary} from "cloudinary" -import fs from "fs" +import { v2 as cloudinary } from "cloudinary"; +import fs from "fs"; - -cloudinary.config({ - cloud_name: process.env.CLOUDINARY_CLOUD_NAME, - api_key: process.env.CLOUDINARY_API_KEY, - api_secret: process.env.CLOUDINARY_API_SECRET +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, }); const uploadOnCloudinary = async (localFilePath) => { - try { - if (!localFilePath) return null - //upload the file on cloudinary - const response = await cloudinary.uploader.upload(localFilePath, { - resource_type: "auto" - }) - // file has been uploaded successfull - //console.log("file is uploaded on cloudinary ", response.url); - fs.unlinkSync(localFilePath) - return response; - - } catch (error) { - fs.unlinkSync(localFilePath) // remove the locally saved temporary file as the upload operation got failed - return null; - } -} - + try { + if (!localFilePath) return null; + //upload the file on cloudinary + const response = await cloudinary.uploader.upload(localFilePath, { + resource_type: "auto", + }); + // file has been uploaded successfully + //console.log("file is uploaded on cloudinary ", response.url); + fs.unlinkSync(localFilePath); + return response; + } catch (error) { + fs.unlinkSync(localFilePath); // remove the locally saved temporary file as the upload operation got failed + return null; + } +}; +const deleteFromCloudinary = async (cloudinaryFilepath) => { + try { + if (!cloudinaryFilepath) return null; + const fileName = cloudinaryFilepath.split("/").pop().split(".")[0]; + const response = await cloudinary.uploader.destroy(fileName); + return response; + } catch (error) { + console.log("Error while deleting file from cloudinary : ", error); + return null; + } +}; -export {uploadOnCloudinary} \ No newline at end of file +export { uploadOnCloudinary, deleteFromCloudinary };