├── .env.sample
├── .gitignore
├── README.md
├── backend
├── config
│ ├── db.js
│ └── envVars.js
├── controllers
│ ├── auth.controller.js
│ ├── movie.controller.js
│ ├── search.controller.js
│ └── tv.controller.js
├── middleware
│ └── protectRoute.js
├── models
│ └── user.model.js
├── routes
│ ├── auth.route.js
│ ├── movie.route.js
│ ├── search.route.js
│ └── tv.route.js
├── server.js
├── services
│ └── tmdb.service.js
└── utils
│ └── generateToken.js
├── frontend
├── .eslintrc.cjs
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── 404.png
│ ├── avatar1.png
│ ├── avatar2.png
│ ├── avatar3.png
│ ├── device-pile.png
│ ├── download-icon.gif
│ ├── extraction.jpg
│ ├── favicon.png
│ ├── hero-vid.m4v
│ ├── hero.png
│ ├── kids.png
│ ├── netflix-logo.png
│ ├── screenshot-for-readme.png
│ ├── stranger-things-lg.png
│ ├── stranger-things-sm.png
│ ├── tv.png
│ └── video-devices.m4v
├── src
│ ├── App.jsx
│ ├── components
│ │ ├── Footer.jsx
│ │ ├── MovieSlider.jsx
│ │ ├── Navbar.jsx
│ │ └── skeletons
│ │ │ └── WatchPageSkeleton.jsx
│ ├── hooks
│ │ └── useGetTrendingContent.jsx
│ ├── index.css
│ ├── main.jsx
│ ├── pages
│ │ ├── 404.jsx
│ │ ├── LoginPage.jsx
│ │ ├── SearchHistoryPage.jsx
│ │ ├── SearchPage.jsx
│ │ ├── SignUpPage.jsx
│ │ ├── WatchPage.jsx
│ │ └── home
│ │ │ ├── AuthScreen.jsx
│ │ │ ├── HomePage.jsx
│ │ │ └── HomeScreen.jsx
│ ├── store
│ │ ├── authUser.js
│ │ └── content.js
│ └── utils
│ │ ├── constants.js
│ │ └── dateFunction.js
├── tailwind.config.js
└── vite.config.js
├── package-lock.json
└── package.json
/.env.sample:
--------------------------------------------------------------------------------
1 | PORT=5000
2 | MONGO_URI=your_mongo_uri
3 | NODE_ENV=development
4 | JWT_SECRET=your_jwt_secret
5 | TMDB_API_KEY=your_tmdb_api_key
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | .env
16 |
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
MERN Netflix Clone 🎬
2 |
3 |
4 | About This Course:
5 |
6 | - ⚛️ Tech Stack: React.js, Node.js, Express.js, MongoDB, Tailwind
7 | - 🔐 Authentication with JWT
8 | - 📱 Responsive UI
9 | - 🎬 Fetch Movies and Tv Show
10 | - 🔎 Search for Actors and Movies
11 | - 🎥 Watch Trailers
12 | - 🔥 Fetch Search History
13 | - 🐱👤 Get Similar Movies/Tv Shows
14 | - 💙 Awesome Landing Page
15 | - 🌐 Deployment
16 |
17 |
18 | ### Setup .env file
19 |
20 | ```bash
21 | PORT=5000
22 | MONGO_URI=your_mongo_uri
23 | NODE_ENV=development
24 | JWT_SECRET=your_jwt_secre
25 | TMDB_API_KEY=your_tmdb_api_key
26 | ```
27 |
28 | ### Run this app locally
29 |
30 | ```shell
31 | npm run build
32 | ```
33 |
34 | ### Start the app
35 |
36 | ```shell
37 | npm run start
38 | ```
39 |
--------------------------------------------------------------------------------
/backend/config/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import { ENV_VARS } from "./envVars.js";
3 |
4 | export const connectDB = async () => {
5 | try {
6 | const conn = await mongoose.connect(ENV_VARS.MONGO_URI);
7 | console.log("MongoDB connected: " + conn.connection.host);
8 | } catch (error) {
9 | console.error("Error connecting to MONGODB: " + error.message);
10 | process.exit(1); // 1 means there was an error, 0 means success
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/backend/config/envVars.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 |
3 | dotenv.config();
4 |
5 | export const ENV_VARS = {
6 | MONGO_URI: process.env.MONGO_URI,
7 | PORT: process.env.PORT || 5000,
8 | JWT_SECRET: process.env.JWT_SECRET,
9 | NODE_ENV: process.env.NODE_ENV,
10 | TMDB_API_KEY: process.env.TMDB_API_KEY,
11 | };
12 |
--------------------------------------------------------------------------------
/backend/controllers/auth.controller.js:
--------------------------------------------------------------------------------
1 | import { User } from "../models/user.model.js";
2 | import bcryptjs from "bcryptjs";
3 | import { generateTokenAndSetCookie } from "../utils/generateToken.js";
4 |
5 | export async function signup(req, res) {
6 | try {
7 | const { email, password, username } = req.body;
8 |
9 | if (!email || !password || !username) {
10 | return res.status(400).json({ success: false, message: "All fields are required" });
11 | }
12 |
13 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
14 |
15 | if (!emailRegex.test(email)) {
16 | return res.status(400).json({ success: false, message: "Invalid email" });
17 | }
18 |
19 | if (password.length < 6) {
20 | return res.status(400).json({ success: false, message: "Password must be at least 6 characters" });
21 | }
22 |
23 | const existingUserByEmail = await User.findOne({ email: email });
24 |
25 | if (existingUserByEmail) {
26 | return res.status(400).json({ success: false, message: "Email already exists" });
27 | }
28 |
29 | const existingUserByUsername = await User.findOne({ username: username });
30 |
31 | if (existingUserByUsername) {
32 | return res.status(400).json({ success: false, message: "Username already exists" });
33 | }
34 |
35 | const salt = await bcryptjs.genSalt(10);
36 | const hashedPassword = await bcryptjs.hash(password, salt);
37 |
38 | const PROFILE_PICS = ["/avatar1.png", "/avatar2.png", "/avatar3.png"];
39 |
40 | const image = PROFILE_PICS[Math.floor(Math.random() * PROFILE_PICS.length)];
41 |
42 | const newUser = new User({
43 | email,
44 | password: hashedPassword,
45 | username,
46 | image,
47 | });
48 |
49 | generateTokenAndSetCookie(newUser._id, res);
50 | await newUser.save();
51 |
52 | res.status(201).json({
53 | success: true,
54 | user: {
55 | ...newUser._doc,
56 | password: "",
57 | },
58 | });
59 | } catch (error) {
60 | console.log("Error in signup controller", error.message);
61 | res.status(500).json({ success: false, message: "Internal server error" });
62 | }
63 | }
64 |
65 | export async function login(req, res) {
66 | try {
67 | const { email, password } = req.body;
68 |
69 | if (!email || !password) {
70 | return res.status(400).json({ success: false, message: "All fields are required" });
71 | }
72 |
73 | const user = await User.findOne({ email: email });
74 | if (!user) {
75 | return res.status(404).json({ success: false, message: "Invalid credentials" });
76 | }
77 |
78 | const isPasswordCorrect = await bcryptjs.compare(password, user.password);
79 |
80 | if (!isPasswordCorrect) {
81 | return res.status(400).json({ success: false, message: "Invalid credentials" });
82 | }
83 |
84 | generateTokenAndSetCookie(user._id, res);
85 |
86 | res.status(200).json({
87 | success: true,
88 | user: {
89 | ...user._doc,
90 | password: "",
91 | },
92 | });
93 | } catch (error) {
94 | console.log("Error in login controller", error.message);
95 | res.status(500).json({ success: false, message: "Internal server error" });
96 | }
97 | }
98 |
99 | export async function logout(req, res) {
100 | try {
101 | res.clearCookie("jwt-netflix");
102 | res.status(200).json({ success: true, message: "Logged out successfully" });
103 | } catch (error) {
104 | console.log("Error in logout controller", error.message);
105 | res.status(500).json({ success: false, message: "Internal server error" });
106 | }
107 | }
108 |
109 | export async function authCheck(req, res) {
110 | try {
111 | console.log("req.user:", req.user);
112 | res.status(200).json({ success: true, user: req.user });
113 | } catch (error) {
114 | console.log("Error in authCheck controller", error.message);
115 | res.status(500).json({ success: false, message: "Internal server error" });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/backend/controllers/movie.controller.js:
--------------------------------------------------------------------------------
1 | import { fetchFromTMDB } from "../services/tmdb.service.js";
2 |
3 | export async function getTrendingMovie(req, res) {
4 | try {
5 | const data = await fetchFromTMDB("https://api.themoviedb.org/3/trending/movie/day?language=en-US");
6 | const randomMovie = data.results[Math.floor(Math.random() * data.results?.length)];
7 |
8 | res.json({ success: true, content: randomMovie });
9 | } catch (error) {
10 | res.status(500).json({ success: false, message: "Internal Server Error" });
11 | }
12 | }
13 |
14 | export async function getMovieTrailers(req, res) {
15 | const { id } = req.params;
16 | try {
17 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${id}/videos?language=en-US`);
18 | res.json({ success: true, trailers: data.results });
19 | } catch (error) {
20 | if (error.message.includes("404")) {
21 | return res.status(404).send(null);
22 | }
23 |
24 | res.status(500).json({ success: false, message: "Internal Server Error" });
25 | }
26 | }
27 |
28 | export async function getMovieDetails(req, res) {
29 | const { id } = req.params;
30 | try {
31 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${id}?language=en-US`);
32 | res.status(200).json({ success: true, content: data });
33 | } catch (error) {
34 | if (error.message.includes("404")) {
35 | return res.status(404).send(null);
36 | }
37 |
38 | res.status(500).json({ success: false, message: "Internal Server Error" });
39 | }
40 | }
41 |
42 | export async function getSimilarMovies(req, res) {
43 | const { id } = req.params;
44 | try {
45 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${id}/similar?language=en-US&page=1`);
46 | res.status(200).json({ success: true, similar: data.results });
47 | } catch (error) {
48 | res.status(500).json({ success: false, message: "Internal Server Error" });
49 | }
50 | }
51 |
52 | export async function getMoviesByCategory(req, res) {
53 | const { category } = req.params;
54 | try {
55 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/movie/${category}?language=en-US&page=1`);
56 | res.status(200).json({ success: true, content: data.results });
57 | } catch (error) {
58 | res.status(500).json({ success: false, message: "Internal Server Error" });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/backend/controllers/search.controller.js:
--------------------------------------------------------------------------------
1 | import { User } from "../models/user.model.js";
2 | import { fetchFromTMDB } from "../services/tmdb.service.js";
3 |
4 | export async function searchPerson(req, res) {
5 | const { query } = req.params;
6 | try {
7 | const response = await fetchFromTMDB(
8 | `https://api.themoviedb.org/3/search/person?query=${query}&include_adult=false&language=en-US&page=1`
9 | );
10 |
11 | if (response.results.length === 0) {
12 | return res.status(404).send(null);
13 | }
14 |
15 | await User.findByIdAndUpdate(req.user._id, {
16 | $push: {
17 | searchHistory: {
18 | id: response.results[0].id,
19 | image: response.results[0].profile_path,
20 | title: response.results[0].name,
21 | searchType: "person",
22 | createdAt: new Date(),
23 | },
24 | },
25 | });
26 |
27 | res.status(200).json({ success: true, content: response.results });
28 | } catch (error) {
29 | console.log("Error in searchPerson controller: ", error.message);
30 | res.status(500).json({ success: false, message: "Internal Server Error" });
31 | }
32 | }
33 |
34 | export async function searchMovie(req, res) {
35 | const { query } = req.params;
36 |
37 | try {
38 | const response = await fetchFromTMDB(
39 | `https://api.themoviedb.org/3/search/movie?query=${query}&include_adult=false&language=en-US&page=1`
40 | );
41 |
42 | if (response.results.length === 0) {
43 | return res.status(404).send(null);
44 | }
45 |
46 | await User.findByIdAndUpdate(req.user._id, {
47 | $push: {
48 | searchHistory: {
49 | id: response.results[0].id,
50 | image: response.results[0].poster_path,
51 | title: response.results[0].title,
52 | searchType: "movie",
53 | createdAt: new Date(),
54 | },
55 | },
56 | });
57 | res.status(200).json({ success: true, content: response.results });
58 | } catch (error) {
59 | console.log("Error in searchMovie controller: ", error.message);
60 | res.status(500).json({ success: false, message: "Internal Server Error" });
61 | }
62 | }
63 |
64 | export async function searchTv(req, res) {
65 | const { query } = req.params;
66 |
67 | try {
68 | const response = await fetchFromTMDB(
69 | `https://api.themoviedb.org/3/search/tv?query=${query}&include_adult=false&language=en-US&page=1`
70 | );
71 |
72 | if (response.results.length === 0) {
73 | return res.status(404).send(null);
74 | }
75 |
76 | await User.findByIdAndUpdate(req.user._id, {
77 | $push: {
78 | searchHistory: {
79 | id: response.results[0].id,
80 | image: response.results[0].poster_path,
81 | title: response.results[0].name,
82 | searchType: "tv",
83 | createdAt: new Date(),
84 | },
85 | },
86 | });
87 | res.json({ success: true, content: response.results });
88 | } catch (error) {
89 | console.log("Error in searchTv controller: ", error.message);
90 | res.status(500).json({ success: false, message: "Internal Server Error" });
91 | }
92 | }
93 |
94 | export async function getSearchHistory(req, res) {
95 | try {
96 | res.status(200).json({ success: true, content: req.user.searchHistory });
97 | } catch (error) {
98 | res.status(500).json({ success: false, message: "Internal Server Error" });
99 | }
100 | }
101 |
102 | export async function removeItemFromSearchHistory(req, res) {
103 | let { id } = req.params;
104 |
105 | id = parseInt(id);
106 |
107 | try {
108 | await User.findByIdAndUpdate(req.user._id, {
109 | $pull: {
110 | searchHistory: { id: id },
111 | },
112 | });
113 |
114 | res.status(200).json({ success: true, message: "Item removed from search history" });
115 | } catch (error) {
116 | console.log("Error in removeItemFromSearchHistory controller: ", error.message);
117 | res.status(500).json({ success: false, message: "Internal Server Error" });
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/backend/controllers/tv.controller.js:
--------------------------------------------------------------------------------
1 | import { fetchFromTMDB } from "../services/tmdb.service.js";
2 |
3 | export async function getTrendingTv(req, res) {
4 | try {
5 | const data = await fetchFromTMDB("https://api.themoviedb.org/3/trending/tv/day?language=en-US");
6 | const randomMovie = data.results[Math.floor(Math.random() * data.results?.length)];
7 |
8 | res.json({ success: true, content: randomMovie });
9 | } catch (error) {
10 | res.status(500).json({ success: false, message: "Internal Server Error" });
11 | }
12 | }
13 |
14 | export async function getTvTrailers(req, res) {
15 | const { id } = req.params;
16 | try {
17 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${id}/videos?language=en-US`);
18 | res.json({ success: true, trailers: data.results });
19 | } catch (error) {
20 | if (error.message.includes("404")) {
21 | return res.status(404).send(null);
22 | }
23 |
24 | res.status(500).json({ success: false, message: "Internal Server Error" });
25 | }
26 | }
27 |
28 | export async function getTvDetails(req, res) {
29 | const { id } = req.params;
30 | try {
31 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${id}?language=en-US`);
32 | res.status(200).json({ success: true, content: data });
33 | } catch (error) {
34 | if (error.message.includes("404")) {
35 | return res.status(404).send(null);
36 | }
37 |
38 | res.status(500).json({ success: false, message: "Internal Server Error" });
39 | }
40 | }
41 |
42 | export async function getSimilarTvs(req, res) {
43 | const { id } = req.params;
44 | try {
45 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${id}/similar?language=en-US&page=1`);
46 | res.status(200).json({ success: true, similar: data.results });
47 | } catch (error) {
48 | res.status(500).json({ success: false, message: "Internal Server Error" });
49 | }
50 | }
51 |
52 | export async function getTvsByCategory(req, res) {
53 | const { category } = req.params;
54 | try {
55 | const data = await fetchFromTMDB(`https://api.themoviedb.org/3/tv/${category}?language=en-US&page=1`);
56 | res.status(200).json({ success: true, content: data.results });
57 | } catch (error) {
58 | res.status(500).json({ success: false, message: "Internal Server Error" });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/backend/middleware/protectRoute.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import { User } from "../models/user.model.js";
3 | import { ENV_VARS } from "../config/envVars.js";
4 |
5 | export const protectRoute = async (req, res, next) => {
6 | try {
7 | const token = req.cookies["jwt-netflix"];
8 |
9 | if (!token) {
10 | return res.status(401).json({ success: false, message: "Unauthorized - No Token Provided" });
11 | }
12 |
13 | const decoded = jwt.verify(token, ENV_VARS.JWT_SECRET);
14 |
15 | if (!decoded) {
16 | return res.status(401).json({ success: false, message: "Unauthorized - Invalid Token" });
17 | }
18 |
19 | const user = await User.findById(decoded.userId).select("-password");
20 |
21 | if (!user) {
22 | return res.status(404).json({ success: false, message: "User not found" });
23 | }
24 |
25 | req.user = user;
26 |
27 | next();
28 | } catch (error) {
29 | console.log("Error in protectRoute middleware: ", error.message);
30 | res.status(500).json({ success: false, message: "Internal Server Error" });
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/backend/models/user.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = mongoose.Schema({
4 | username: {
5 | type: String,
6 | required: true,
7 | unique: true,
8 | },
9 | email: {
10 | type: String,
11 | required: true,
12 | unique: true,
13 | },
14 | password: {
15 | type: String,
16 | required: true,
17 | },
18 | image: {
19 | type: String,
20 | default: "",
21 | },
22 | searchHistory: {
23 | type: Array,
24 | default: [],
25 | },
26 | });
27 |
28 | export const User = mongoose.model("User", userSchema);
29 |
--------------------------------------------------------------------------------
/backend/routes/auth.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { authCheck, login, logout, signup } from "../controllers/auth.controller.js";
3 | import { protectRoute } from "../middleware/protectRoute.js";
4 |
5 | const router = express.Router();
6 |
7 | router.post("/signup", signup);
8 | router.post("/login", login);
9 | router.post("/logout", logout);
10 |
11 | router.get("/authCheck", protectRoute, authCheck);
12 |
13 | export default router;
14 |
--------------------------------------------------------------------------------
/backend/routes/movie.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getMovieDetails,
4 | getMoviesByCategory,
5 | getMovieTrailers,
6 | getSimilarMovies,
7 | getTrendingMovie,
8 | } from "../controllers/movie.controller.js";
9 |
10 | const router = express.Router();
11 |
12 | router.get("/trending", getTrendingMovie);
13 | router.get("/:id/trailers", getMovieTrailers);
14 | router.get("/:id/details", getMovieDetails);
15 | router.get("/:id/similar", getSimilarMovies);
16 | router.get("/:category", getMoviesByCategory);
17 |
18 | export default router;
19 |
--------------------------------------------------------------------------------
/backend/routes/search.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getSearchHistory,
4 | removeItemFromSearchHistory,
5 | searchMovie,
6 | searchPerson,
7 | searchTv,
8 | } from "../controllers/search.controller.js";
9 |
10 | const router = express.Router();
11 |
12 | router.get("/person/:query", searchPerson);
13 | router.get("/movie/:query", searchMovie);
14 | router.get("/tv/:query", searchTv);
15 |
16 | router.get("/history", getSearchHistory);
17 |
18 | router.delete("/history/:id", removeItemFromSearchHistory);
19 |
20 | export default router;
21 |
--------------------------------------------------------------------------------
/backend/routes/tv.route.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getSimilarTvs,
4 | getTrendingTv,
5 | getTvDetails,
6 | getTvsByCategory,
7 | getTvTrailers,
8 | } from "../controllers/tv.controller.js";
9 |
10 | const router = express.Router();
11 |
12 | router.get("/trending", getTrendingTv);
13 | router.get("/:id/trailers", getTvTrailers);
14 | router.get("/:id/details", getTvDetails);
15 | router.get("/:id/similar", getSimilarTvs);
16 | router.get("/:category", getTvsByCategory);
17 |
18 | export default router;
19 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import cookieParser from "cookie-parser";
3 | import path from "path";
4 |
5 | import authRoutes from "./routes/auth.route.js";
6 | import movieRoutes from "./routes/movie.route.js";
7 | import tvRoutes from "./routes/tv.route.js";
8 | import searchRoutes from "./routes/search.route.js";
9 |
10 | import { ENV_VARS } from "./config/envVars.js";
11 | import { connectDB } from "./config/db.js";
12 | import { protectRoute } from "./middleware/protectRoute.js";
13 |
14 | const app = express();
15 |
16 | const PORT = ENV_VARS.PORT;
17 | const __dirname = path.resolve();
18 |
19 | app.use(express.json()); // will allow us to parse req.body
20 | app.use(cookieParser());
21 |
22 | app.use("/api/v1/auth", authRoutes);
23 | app.use("/api/v1/movie", protectRoute, movieRoutes);
24 | app.use("/api/v1/tv", protectRoute, tvRoutes);
25 | app.use("/api/v1/search", protectRoute, searchRoutes);
26 |
27 | if (ENV_VARS.NODE_ENV === "production") {
28 | app.use(express.static(path.join(__dirname, "/frontend/dist")));
29 |
30 | app.get("*", (req, res) => {
31 | res.sendFile(path.resolve(__dirname, "frontend", "dist", "index.html"));
32 | });
33 | }
34 |
35 | app.listen(PORT, () => {
36 | console.log("Server started at http://localhost:" + PORT);
37 | connectDB();
38 | });
39 |
--------------------------------------------------------------------------------
/backend/services/tmdb.service.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { ENV_VARS } from "../config/envVars.js";
3 |
4 | export const fetchFromTMDB = async (url) => {
5 | const options = {
6 | headers: {
7 | accept: "application/json",
8 | Authorization: "Bearer " + ENV_VARS.TMDB_API_KEY,
9 | },
10 | };
11 |
12 | const response = await axios.get(url, options);
13 |
14 | if (response.status !== 200) {
15 | throw new Error("Failed to fetch data from TMDB" + response.statusText);
16 | }
17 |
18 | return response.data;
19 | };
20 |
--------------------------------------------------------------------------------
/backend/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import { ENV_VARS } from "../config/envVars.js";
3 |
4 | export const generateTokenAndSetCookie = (userId, res) => {
5 | const token = jwt.sign({ userId }, ENV_VARS.JWT_SECRET, { expiresIn: "15d" });
6 |
7 | res.cookie("jwt-netflix", token, {
8 | maxAge: 15 * 24 * 60 * 60 * 1000, // 15 days in MS
9 | httpOnly: true, // prevent XSS attacks cross-site scripting attacks, make it not be accessed by JS
10 | sameSite: "strict", // CSRF attacks cross-site request forgery attacks
11 | secure: ENV_VARS.NODE_ENV !== "development",
12 | });
13 |
14 | return token;
15 | };
16 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:react/recommended",
7 | "plugin:react/jsx-runtime",
8 | "plugin:react-hooks/recommended",
9 | ],
10 | ignorePatterns: ["dist", ".eslintrc.cjs"],
11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
12 | settings: { react: { version: "18.2" } },
13 | plugins: ["react-refresh"],
14 | rules: {
15 | "react/jsx-no-target-blank": "off",
16 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
17 | "react/no-unescaped-entities": "off",
18 | "react/prop-types": "off",
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "axios": "^1.7.2",
14 | "lucide-react": "^0.408.0",
15 | "netflix-clone": "file:..",
16 | "react": "^18.3.1",
17 | "react-dom": "^18.3.1",
18 | "react-hot-toast": "^2.4.1",
19 | "react-player": "^2.16.0",
20 | "react-router-dom": "^6.25.0",
21 | "tailwind-scrollbar-hide": "^1.1.7",
22 | "zustand": "^4.5.4"
23 | },
24 | "devDependencies": {
25 | "@types/react": "^18.3.3",
26 | "@types/react-dom": "^18.3.0",
27 | "@vitejs/plugin-react": "^4.3.1",
28 | "autoprefixer": "^10.4.19",
29 | "eslint": "^8.57.0",
30 | "eslint-plugin-react": "^7.34.3",
31 | "eslint-plugin-react-hooks": "^4.6.2",
32 | "eslint-plugin-react-refresh": "^0.4.7",
33 | "postcss": "^8.4.39",
34 | "tailwindcss": "^3.4.6",
35 | "vite": "^5.3.4"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/public/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/404.png
--------------------------------------------------------------------------------
/frontend/public/avatar1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/avatar1.png
--------------------------------------------------------------------------------
/frontend/public/avatar2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/avatar2.png
--------------------------------------------------------------------------------
/frontend/public/avatar3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/avatar3.png
--------------------------------------------------------------------------------
/frontend/public/device-pile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/device-pile.png
--------------------------------------------------------------------------------
/frontend/public/download-icon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/download-icon.gif
--------------------------------------------------------------------------------
/frontend/public/extraction.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/extraction.jpg
--------------------------------------------------------------------------------
/frontend/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/favicon.png
--------------------------------------------------------------------------------
/frontend/public/hero-vid.m4v:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/hero-vid.m4v
--------------------------------------------------------------------------------
/frontend/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/hero.png
--------------------------------------------------------------------------------
/frontend/public/kids.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/kids.png
--------------------------------------------------------------------------------
/frontend/public/netflix-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/netflix-logo.png
--------------------------------------------------------------------------------
/frontend/public/screenshot-for-readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/screenshot-for-readme.png
--------------------------------------------------------------------------------
/frontend/public/stranger-things-lg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/stranger-things-lg.png
--------------------------------------------------------------------------------
/frontend/public/stranger-things-sm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/stranger-things-sm.png
--------------------------------------------------------------------------------
/frontend/public/tv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/tv.png
--------------------------------------------------------------------------------
/frontend/public/video-devices.m4v:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RKNITH/NETFLIX-CLONE/e3fb1cf24d9c72580e484f5bb6cfad73d0ea6c8a/frontend/public/video-devices.m4v
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Route, Routes } from "react-router-dom";
2 | import HomePage from "./pages/home/HomePage";
3 | import LoginPage from "./pages/LoginPage";
4 | import SignUpPage from "./pages/SignUpPage";
5 | import WatchPage from "./pages/WatchPage";
6 | import Footer from "./components/Footer";
7 | import { Toaster } from "react-hot-toast";
8 | import { useAuthStore } from "./store/authUser";
9 | import { useEffect } from "react";
10 | import { Loader } from "lucide-react";
11 | import SearchPage from "./pages/SearchPage";
12 | import SearchHistoryPage from "./pages/SearchHistoryPage";
13 | import NotFoundPage from "./pages/404";
14 |
15 | function App() {
16 | const { user, isCheckingAuth, authCheck } = useAuthStore();
17 |
18 | useEffect(() => {
19 | authCheck();
20 | }, [authCheck]);
21 |
22 | if (isCheckingAuth) {
23 | return (
24 |
29 | );
30 | }
31 |
32 | return (
33 | <>
34 |
35 | } />
36 | : } />
37 | : } />
38 | : } />
39 | : } />
40 | : } />
41 | } />
42 |
43 |
44 |
45 |
46 | >
47 | );
48 | }
49 |
50 | export default App;
51 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | const Footer = () => {
2 | return (
3 |
27 | );
28 | };
29 | export default Footer;
30 |
--------------------------------------------------------------------------------
/frontend/src/components/MovieSlider.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { useContentStore } from "../store/content";
3 | import axios from "axios";
4 | import { Link } from "react-router-dom";
5 | import { SMALL_IMG_BASE_URL } from "../utils/constants";
6 | import { ChevronLeft, ChevronRight } from "lucide-react";
7 |
8 | const MovieSlider = ({ category }) => {
9 | const { contentType } = useContentStore();
10 | const [content, setContent] = useState([]);
11 | const [showArrows, setShowArrows] = useState(false);
12 |
13 | const sliderRef = useRef(null);
14 |
15 | const formattedCategoryName =
16 | category.replaceAll("_", " ")[0].toUpperCase() + category.replaceAll("_", " ").slice(1);
17 | const formattedContentType = contentType === "movie" ? "Movies" : "TV Shows";
18 |
19 | useEffect(() => {
20 | const getContent = async () => {
21 | const res = await axios.get(`/api/v1/${contentType}/${category}`);
22 | setContent(res.data.content);
23 | };
24 |
25 | getContent();
26 | }, [contentType, category]);
27 |
28 | const scrollLeft = () => {
29 | if (sliderRef.current) {
30 | sliderRef.current.scrollBy({ left: -sliderRef.current.offsetWidth, behavior: "smooth" });
31 | }
32 | };
33 | const scrollRight = () => {
34 | sliderRef.current.scrollBy({ left: sliderRef.current.offsetWidth, behavior: "smooth" });
35 | };
36 |
37 | return (
38 | setShowArrows(true)}
41 | onMouseLeave={() => setShowArrows(false)}
42 | >
43 |
44 | {formattedCategoryName} {formattedContentType}
45 |
46 |
47 |
48 | {content.map((item) => (
49 |
50 |
51 |

56 |
57 |
{item.title || item.name}
58 |
59 | ))}
60 |
61 |
62 | {showArrows && (
63 | <>
64 |
72 |
73 |
81 | >
82 | )}
83 |
84 | );
85 | };
86 | export default MovieSlider;
87 |
--------------------------------------------------------------------------------
/frontend/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { LogOut, Menu, Search } from "lucide-react";
4 | import { useAuthStore } from "../store/authUser";
5 | import { useContentStore } from "../store/content";
6 |
7 | const Navbar = () => {
8 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
9 | const { user, logout } = useAuthStore();
10 |
11 | const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
12 |
13 | const { setContentType } = useContentStore();
14 |
15 | return (
16 |
17 |
18 |
19 |

20 |
21 |
22 | {/* desktop navbar items */}
23 |
24 | setContentType("movie")}>
25 | Movies
26 |
27 | setContentType("tv")}>
28 | Tv Shows
29 |
30 |
31 | Search History
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |

41 |
42 |
43 |
44 |
45 |
46 |
47 | {/* mobile navbar items */}
48 | {isMobileMenuOpen && (
49 |
50 |
51 | Movies
52 |
53 |
54 | Tv Shows
55 |
56 |
57 | Search History
58 |
59 |
60 | )}
61 |
62 | );
63 | };
64 | export default Navbar;
65 |
--------------------------------------------------------------------------------
/frontend/src/components/skeletons/WatchPageSkeleton.jsx:
--------------------------------------------------------------------------------
1 | const WatchPageSkeleton = () => {
2 | return (
3 |
10 | );
11 | };
12 | export default WatchPageSkeleton;
13 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useGetTrendingContent.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useContentStore } from "../store/content";
3 | import axios from "axios";
4 |
5 | const useGetTrendingContent = () => {
6 | const [trendingContent, setTrendingContent] = useState(null);
7 | const { contentType } = useContentStore();
8 |
9 | useEffect(() => {
10 | const getTrendingContent = async () => {
11 | const res = await axios.get(`/api/v1/${contentType}/trending`);
12 | setTrendingContent(res.data.content);
13 | };
14 |
15 | getTrendingContent();
16 | }, [contentType]);
17 |
18 | return { trendingContent };
19 | };
20 | export default useGetTrendingContent;
21 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .hero-bg {
6 | background-image: linear-gradient(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.1)), url("/hero.png");
7 | }
8 |
9 | .shimmer {
10 | animation: shimmer 2s infinite linear;
11 | background: linear-gradient(to right, #2c2c2c 4%, #333 25%, #2c2c2c 36%);
12 | background-size: 1000px 100%;
13 | }
14 |
15 | @keyframes shimmer {
16 | 0% {
17 | background-position: -1000px 0;
18 | }
19 | 100% {
20 | background-position: 1000px 0;
21 | }
22 | }
23 |
24 | .error-page--content::before {
25 | background: radial-gradient(
26 | ellipse at center,
27 | rgba(0, 0, 0, 0.5) 0,
28 | rgba(0, 0, 0, 0.2) 45%,
29 | rgba(0, 0, 0, 0.1) 55%,
30 | transparent 70%
31 | );
32 | bottom: -10vw;
33 | content: "";
34 | left: 10vw;
35 | position: absolute;
36 | right: 10vw;
37 | top: -10vw;
38 | z-index: -1;
39 | }
40 |
41 | ::-webkit-scrollbar {
42 | width: 8px;
43 | }
44 |
45 | ::-webkit-scrollbar-thumb {
46 | background-color: #4b5563;
47 | border-radius: 6px;
48 | }
49 |
50 | ::-webkit-scrollbar-track {
51 | background-color: #1a202c;
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 | import { BrowserRouter } from "react-router-dom";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")).render(
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/frontend/src/pages/404.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | const NotFoundPage = () => {
4 | return (
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Lost your way?
16 |
17 | Sorry, we can't find that page. You'll find lots to explore on the home page.
18 |
19 |
20 | Netflix Home
21 |
22 |
23 |
24 | );
25 | };
26 | export default NotFoundPage;
27 |
--------------------------------------------------------------------------------
/frontend/src/pages/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useAuthStore } from "../store/authUser";
4 |
5 | const LoginPage = () => {
6 | const [email, setEmail] = useState("");
7 | const [password, setPassword] = useState("");
8 |
9 | const { login, isLoggingIn } = useAuthStore();
10 |
11 | const handleLogin = (e) => {
12 | e.preventDefault();
13 | login({ email, password });
14 | };
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Login
27 |
28 |
66 |
67 | Don't have an account?{" "}
68 |
69 | Sign Up
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 | export default LoginPage;
78 |
--------------------------------------------------------------------------------
/frontend/src/pages/SearchHistoryPage.jsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { useEffect, useState } from "react";
3 | import Navbar from "../components/Navbar";
4 | import { SMALL_IMG_BASE_URL } from "../utils/constants";
5 | import { Trash } from "lucide-react";
6 | import toast from "react-hot-toast";
7 |
8 | function formatDate(dateString) {
9 | // Create a Date object from the input date string
10 | const date = new Date(dateString);
11 |
12 | const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
13 |
14 | // Extract the month, day, and year from the Date object
15 | const month = monthNames[date.getUTCMonth()];
16 | const day = date.getUTCDate();
17 | const year = date.getUTCFullYear();
18 |
19 | // Return the formatted date string
20 | return `${month} ${day}, ${year}`;
21 | }
22 |
23 | const SearchHistoryPage = () => {
24 | const [searchHistory, setSearchHistory] = useState([]);
25 |
26 | useEffect(() => {
27 | const getSearchHistory = async () => {
28 | try {
29 | const res = await axios.get(`/api/v1/search/history`);
30 | setSearchHistory(res.data.content);
31 | } catch (error) {
32 | setSearchHistory([]);
33 | }
34 | };
35 | getSearchHistory();
36 | }, []);
37 |
38 | const handleDelete = async (entry) => {
39 | try {
40 | await axios.delete(`/api/v1/search/history/${entry.id}`);
41 | setSearchHistory(searchHistory.filter((item) => item.id !== entry.id));
42 | } catch (error) {
43 | toast.error("Failed to delete search item");
44 | }
45 | };
46 |
47 | if (searchHistory?.length === 0) {
48 | return (
49 |
50 |
51 |
52 |
Search History
53 |
54 |
No search history found
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | return (
62 |
63 |
64 |
65 |
66 |
Search History
67 |
68 | {searchHistory?.map((entry) => (
69 |
70 |

75 |
76 | {entry.title}
77 | {formatDate(entry.createdAt)}
78 |
79 |
80 |
89 | {entry.searchType[0].toUpperCase() + entry.searchType.slice(1)}
90 |
91 |
handleDelete(entry)}
94 | />
95 |
96 | ))}
97 |
98 |
99 |
100 | );
101 | };
102 | export default SearchHistoryPage;
103 |
--------------------------------------------------------------------------------
/frontend/src/pages/SearchPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useContentStore } from "../store/content";
3 | import Navbar from "../components/Navbar";
4 | import { Search } from "lucide-react";
5 | import toast from "react-hot-toast";
6 | import axios from "axios";
7 | import { ORIGINAL_IMG_BASE_URL } from "../utils/constants";
8 | import { Link } from "react-router-dom";
9 |
10 | const SearchPage = () => {
11 | const [activeTab, setActiveTab] = useState("movie");
12 | const [searchTerm, setSearchTerm] = useState("");
13 |
14 | const [results, setResults] = useState([]);
15 | const { setContentType } = useContentStore();
16 |
17 | const handleTabClick = (tab) => {
18 | setActiveTab(tab);
19 | tab === "movie" ? setContentType("movie") : setContentType("tv");
20 | setResults([]);
21 | };
22 |
23 | const handleSearch = async (e) => {
24 | e.preventDefault();
25 | try {
26 | const res = await axios.get(`/api/v1/search/${activeTab}/${searchTerm}`);
27 | setResults(res.data.content);
28 | } catch (error) {
29 | if (error.response.status === 404) {
30 | toast.error("Nothing found, make sure you are searching under the right category");
31 | } else {
32 | toast.error("An error occurred, please try again later");
33 | }
34 | }
35 | };
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
50 |
58 |
66 |
67 |
68 |
80 |
81 |
82 | {results.map((result) => {
83 | if (!result.poster_path && !result.profile_path) return null;
84 |
85 | return (
86 |
87 | {activeTab === "person" ? (
88 |
89 |

94 |
{result.name}
95 |
96 | ) : (
97 |
{
100 | setContentType(activeTab);
101 | }}
102 | >
103 |

108 |
{result.title || result.name}
109 |
110 | )}
111 |
112 | );
113 | })}
114 |
115 |
116 |
117 | );
118 | };
119 | export default SearchPage;
120 |
--------------------------------------------------------------------------------
/frontend/src/pages/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import { useAuthStore } from "../store/authUser";
4 |
5 | const SignUpPage = () => {
6 | const { searchParams } = new URL(document.location);
7 | const emailValue = searchParams.get("email");
8 |
9 | const [email, setEmail] = useState(emailValue || "");
10 | const [username, setUsername] = useState("");
11 | const [password, setPassword] = useState("");
12 |
13 | const { signup, isSigningUp } = useAuthStore();
14 |
15 | const handleSignUp = (e) => {
16 | e.preventDefault();
17 | signup({ email, username, password });
18 | };
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Sign Up
31 |
32 |
84 |
85 | Already a member?{" "}
86 |
87 | Sign in
88 |
89 |
90 |
91 |
92 |
93 | );
94 | };
95 | export default SignUpPage;
96 |
--------------------------------------------------------------------------------
/frontend/src/pages/WatchPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { Link, useParams } from "react-router-dom";
3 | import { useContentStore } from "../store/content";
4 | import axios from "axios";
5 | import Navbar from "../components/Navbar";
6 | import { ChevronLeft, ChevronRight } from "lucide-react";
7 | import ReactPlayer from "react-player";
8 | import { ORIGINAL_IMG_BASE_URL, SMALL_IMG_BASE_URL } from "../utils/constants";
9 | import { formatReleaseDate } from "../utils/dateFunction";
10 | import WatchPageSkeleton from "../components/skeletons/WatchPageSkeleton";
11 |
12 | const WatchPage = () => {
13 | const { id } = useParams();
14 | const [trailers, setTrailers] = useState([]);
15 | const [currentTrailerIdx, setCurrentTrailerIdx] = useState(0);
16 | const [loading, setLoading] = useState(true);
17 | const [content, setContent] = useState({});
18 | const [similarContent, setSimilarContent] = useState([]);
19 | const { contentType } = useContentStore();
20 |
21 | const sliderRef = useRef(null);
22 |
23 | useEffect(() => {
24 | const getTrailers = async () => {
25 | try {
26 | const res = await axios.get(`/api/v1/${contentType}/${id}/trailers`);
27 | setTrailers(res.data.trailers);
28 | } catch (error) {
29 | if (error.message.includes("404")) {
30 | setTrailers([]);
31 | }
32 | }
33 | };
34 |
35 | getTrailers();
36 | }, [contentType, id]);
37 |
38 | useEffect(() => {
39 | const getSimilarContent = async () => {
40 | try {
41 | const res = await axios.get(`/api/v1/${contentType}/${id}/similar`);
42 | setSimilarContent(res.data.similar);
43 | } catch (error) {
44 | if (error.message.includes("404")) {
45 | setSimilarContent([]);
46 | }
47 | }
48 | };
49 |
50 | getSimilarContent();
51 | }, [contentType, id]);
52 |
53 | useEffect(() => {
54 | const getContentDetails = async () => {
55 | try {
56 | const res = await axios.get(`/api/v1/${contentType}/${id}/details`);
57 | setContent(res.data.content);
58 | } catch (error) {
59 | if (error.message.includes("404")) {
60 | setContent(null);
61 | }
62 | } finally {
63 | setLoading(false);
64 | }
65 | };
66 |
67 | getContentDetails();
68 | }, [contentType, id]);
69 |
70 | const handleNext = () => {
71 | if (currentTrailerIdx < trailers.length - 1) setCurrentTrailerIdx(currentTrailerIdx + 1);
72 | };
73 | const handlePrev = () => {
74 | if (currentTrailerIdx > 0) setCurrentTrailerIdx(currentTrailerIdx - 1);
75 | };
76 |
77 | const scrollLeft = () => {
78 | if (sliderRef.current) sliderRef.current.scrollBy({ left: -sliderRef.current.offsetWidth, behavior: "smooth" });
79 | };
80 | const scrollRight = () => {
81 | if (sliderRef.current) sliderRef.current.scrollBy({ left: sliderRef.current.offsetWidth, behavior: "smooth" });
82 | };
83 |
84 | if (loading)
85 | return (
86 |
87 |
88 |
89 | );
90 |
91 | if (!content) {
92 | return (
93 |
94 |
95 |
96 |
97 |
Content not found 😥
98 |
99 |
100 |
101 | );
102 | }
103 |
104 | return (
105 |
106 |
107 |
108 |
109 | {trailers.length > 0 && (
110 |
111 |
122 |
123 |
134 |
135 | )}
136 |
137 |
138 | {trailers.length > 0 && (
139 |
146 | )}
147 |
148 | {trailers?.length === 0 && (
149 |
150 | No trailers available for{" "}
151 | {content?.title || content?.name} 😥
152 |
153 | )}
154 |
155 |
156 | {/* movie details */}
157 |
161 |
162 |
{content?.title || content?.name}
163 |
164 |
165 | {formatReleaseDate(content?.release_date || content?.first_air_date)} |{" "}
166 | {content?.adult ? (
167 | 18+
168 | ) : (
169 | PG-13
170 | )}{" "}
171 |
172 |
{content?.overview}
173 |
174 |

179 |
180 |
181 | {similarContent.length > 0 && (
182 |
183 |
Similar Movies/Tv Show
184 |
185 |
186 | {similarContent.map((content) => {
187 | if (content.poster_path === null) return null;
188 | return (
189 |
190 |

195 |
{content.title || content.name}
196 |
197 | );
198 | })}
199 |
200 |
206 |
212 |
213 |
214 | )}
215 |
216 |
217 | );
218 | };
219 | export default WatchPage;
220 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/AuthScreen.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link, useNavigate } from "react-router-dom";
3 | import { ChevronRight } from "lucide-react";
4 |
5 | const AuthScreen = () => {
6 | const [email, setEmail] = useState("");
7 | const navigate = useNavigate();
8 |
9 | const handleFormSubmit = (e) => {
10 | e.preventDefault();
11 | navigate("/signup?email=" + email);
12 | };
13 |
14 | return (
15 |
16 | {/* Navbar */}
17 |
18 |
19 |
20 | Sign In
21 |
22 |
23 |
24 | {/* hero section */}
25 |
26 |
Unlimited movies, TV shows, and more
27 |
Watch anywhere. Cancel anytime.
28 |
Ready to watch? Enter your email to create or restart your membership.
29 |
30 |
43 |
44 |
45 | {/* separator */}
46 |
47 |
48 | {/* 1st section */}
49 |
50 |
51 | {/* left side */}
52 |
53 |
Enjoy on your TV
54 |
55 | Watch on Smart TVs, PlayStation, Xbox, Chromecast, Apple TV, Blu-ray players, and more.
56 |
57 |
58 | {/* right side */}
59 |
60 |

61 |
70 |
71 |
72 |
73 |
74 | {/* separator */}
75 |
76 |
77 | {/* 2nd section */}
78 |
79 |
80 | {/* left side */}
81 |
82 |
83 |

84 |
85 |
90 |

91 |
92 |
93 | Stranger Things
94 | Downloading...
95 |
96 |
97 |

98 |
99 |
100 |
101 |
102 | {/* right side */}
103 |
104 |
105 |
106 | Download your shows to watch offline
107 |
108 |
109 | Save your favorites easily and always have something to watch.
110 |
111 |
112 |
113 |
114 |
115 | {/* separator */}
116 |
117 |
118 |
119 | {/* 3rd section */}
120 |
121 |
122 | {/* left side */}
123 |
124 |
Watch everywhere
125 |
126 | Stream unlimited movies and TV shows on your phone, tablet, laptop, and TV.
127 |
128 |
129 |
130 | {/* right side */}
131 |
132 |

133 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | {/* 4th section*/}
151 |
152 |
157 | {/* left */}
158 |
159 |

160 |
161 | {/* right */}
162 |
163 |
Create profiles for kids
164 |
165 | Send kids on adventures with their favorite characters in a space made just for them—free
166 | with your membership.
167 |
168 |
169 |
170 |
171 |
172 | );
173 | };
174 | export default AuthScreen;
175 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import { useAuthStore } from "../../store/authUser";
2 | import AuthScreen from "./AuthScreen";
3 | import HomeScreen from "./HomeScreen";
4 |
5 | const HomePage = () => {
6 | const { user } = useAuthStore();
7 |
8 | return <>{user ? : }>;
9 | };
10 | export default HomePage;
11 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/HomeScreen.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import Navbar from "../../components/Navbar";
3 | import { Info, Play } from "lucide-react";
4 | import useGetTrendingContent from "../../hooks/useGetTrendingContent";
5 | import { MOVIE_CATEGORIES, ORIGINAL_IMG_BASE_URL, TV_CATEGORIES } from "../../utils/constants";
6 | import { useContentStore } from "../../store/content";
7 | import MovieSlider from "../../components/MovieSlider";
8 | import { useState } from "react";
9 |
10 | const HomeScreen = () => {
11 | const { trendingContent } = useGetTrendingContent();
12 | const { contentType } = useContentStore();
13 | const [imgLoading, setImgLoading] = useState(true);
14 |
15 | if (!trendingContent)
16 | return (
17 |
21 | );
22 |
23 | return (
24 | <>
25 |
26 |
27 |
28 | {/* COOL OPTIMIZATION HACK FOR IMAGES */}
29 | {imgLoading && (
30 |
31 | )}
32 |
33 |

{
38 | setImgLoading(false);
39 | }}
40 | />
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 | {trendingContent?.title || trendingContent?.name}
53 |
54 |
55 | {trendingContent?.release_date?.split("-")[0] ||
56 | trendingContent?.first_air_date.split("-")[0]}{" "}
57 | | {trendingContent?.adult ? "18+" : "PG-13"}
58 |
59 |
60 |
61 | {trendingContent?.overview.length > 200
62 | ? trendingContent?.overview.slice(0, 200) + "..."
63 | : trendingContent?.overview}
64 |
65 |
66 |
67 |
68 |
73 |
74 | Play
75 |
76 |
77 |
81 |
82 | More Info
83 |
84 |
85 |
86 |
87 |
88 |
89 | {contentType === "movie"
90 | ? MOVIE_CATEGORIES.map((category) => )
91 | : TV_CATEGORIES.map((category) => )}
92 |
93 | >
94 | );
95 | };
96 | export default HomeScreen;
97 |
--------------------------------------------------------------------------------
/frontend/src/store/authUser.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import toast from "react-hot-toast";
3 | import { create } from "zustand";
4 |
5 | export const useAuthStore = create((set) => ({
6 | user: null,
7 | isSigningUp: false,
8 | isCheckingAuth: true,
9 | isLoggingOut: false,
10 | isLoggingIn: false,
11 | signup: async (credentials) => {
12 | set({ isSigningUp: true });
13 | try {
14 | const response = await axios.post("/api/v1/auth/signup", credentials);
15 | set({ user: response.data.user, isSigningUp: false });
16 | toast.success("Account created successfully");
17 | } catch (error) {
18 | toast.error(error.response.data.message || "Signup failed");
19 | set({ isSigningUp: false, user: null });
20 | }
21 | },
22 | login: async (credentials) => {
23 | set({ isLoggingIn: true });
24 | try {
25 | const response = await axios.post("/api/v1/auth/login", credentials);
26 | set({ user: response.data.user, isLoggingIn: false });
27 | } catch (error) {
28 | set({ isLoggingIn: false, user: null });
29 | toast.error(error.response.data.message || "Login failed");
30 | }
31 | },
32 | logout: async () => {
33 | set({ isLoggingOut: true });
34 | try {
35 | await axios.post("/api/v1/auth/logout");
36 | set({ user: null, isLoggingOut: false });
37 | toast.success("Logged out successfully");
38 | } catch (error) {
39 | set({ isLoggingOut: false });
40 | toast.error(error.response.data.message || "Logout failed");
41 | }
42 | },
43 | authCheck: async () => {
44 | set({ isCheckingAuth: true });
45 | try {
46 | const response = await axios.get("/api/v1/auth/authCheck");
47 |
48 | set({ user: response.data.user, isCheckingAuth: false });
49 | } catch (error) {
50 | set({ isCheckingAuth: false, user: null });
51 | // toast.error(error.response.data.message || "An error occurred");
52 | }
53 | },
54 | }));
55 |
--------------------------------------------------------------------------------
/frontend/src/store/content.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | export const useContentStore = create((set) => ({
4 | contentType: "movie",
5 | setContentType: (type) => set({ contentType: type }),
6 | }));
7 |
--------------------------------------------------------------------------------
/frontend/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const SMALL_IMG_BASE_URL = "https://image.tmdb.org/t/p/w500";
2 | export const ORIGINAL_IMG_BASE_URL = "https://image.tmdb.org/t/p/original";
3 |
4 | export const MOVIE_CATEGORIES = ["now_playing", "top_rated", "popular", "upcoming"];
5 | export const TV_CATEGORIES = ["airing_today", "on_the_air", "popular", "top_rated"];
6 |
--------------------------------------------------------------------------------
/frontend/src/utils/dateFunction.js:
--------------------------------------------------------------------------------
1 | export function formatReleaseDate(date) {
2 | return new Date(date).toLocaleDateString("en-US", {
3 | year: "numeric",
4 | month: "long",
5 | day: "numeric",
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import tailwindScrollbarHide from "tailwind-scrollbar-hide";
2 | /** @type {import('tailwindcss').Config} */
3 | export default {
4 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [tailwindScrollbarHide],
9 | };
10 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | proxy: {
9 | "/api": {
10 | target: "http://localhost:5000",
11 | },
12 | },
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netflix-clone",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "netflix-clone",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "axios": "^1.7.2",
13 | "bcryptjs": "^2.4.3",
14 | "cookie-parser": "^1.4.6",
15 | "dotenv": "^16.4.5",
16 | "express": "^4.19.2",
17 | "jsonwebtoken": "^9.0.2",
18 | "mongoose": "^8.5.1"
19 | },
20 | "devDependencies": {
21 | "nodemon": "^3.1.4"
22 | }
23 | },
24 | "node_modules/@mongodb-js/saslprep": {
25 | "version": "1.1.7",
26 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.7.tgz",
27 | "integrity": "sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==",
28 | "dependencies": {
29 | "sparse-bitfield": "^3.0.3"
30 | }
31 | },
32 | "node_modules/@types/webidl-conversions": {
33 | "version": "7.0.3",
34 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
35 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
36 | },
37 | "node_modules/@types/whatwg-url": {
38 | "version": "11.0.5",
39 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
40 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
41 | "dependencies": {
42 | "@types/webidl-conversions": "*"
43 | }
44 | },
45 | "node_modules/accepts": {
46 | "version": "1.3.8",
47 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
48 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
49 | "dependencies": {
50 | "mime-types": "~2.1.34",
51 | "negotiator": "0.6.3"
52 | },
53 | "engines": {
54 | "node": ">= 0.6"
55 | }
56 | },
57 | "node_modules/anymatch": {
58 | "version": "3.1.3",
59 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
60 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
61 | "dev": true,
62 | "dependencies": {
63 | "normalize-path": "^3.0.0",
64 | "picomatch": "^2.0.4"
65 | },
66 | "engines": {
67 | "node": ">= 8"
68 | }
69 | },
70 | "node_modules/array-flatten": {
71 | "version": "1.1.1",
72 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
73 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
74 | },
75 | "node_modules/asynckit": {
76 | "version": "0.4.0",
77 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
78 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
79 | },
80 | "node_modules/axios": {
81 | "version": "1.7.2",
82 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
83 | "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
84 | "dependencies": {
85 | "follow-redirects": "^1.15.6",
86 | "form-data": "^4.0.0",
87 | "proxy-from-env": "^1.1.0"
88 | }
89 | },
90 | "node_modules/balanced-match": {
91 | "version": "1.0.2",
92 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
93 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
94 | "dev": true
95 | },
96 | "node_modules/bcryptjs": {
97 | "version": "2.4.3",
98 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
99 | "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
100 | },
101 | "node_modules/binary-extensions": {
102 | "version": "2.3.0",
103 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
104 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
105 | "dev": true,
106 | "engines": {
107 | "node": ">=8"
108 | },
109 | "funding": {
110 | "url": "https://github.com/sponsors/sindresorhus"
111 | }
112 | },
113 | "node_modules/body-parser": {
114 | "version": "1.20.2",
115 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
116 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
117 | "dependencies": {
118 | "bytes": "3.1.2",
119 | "content-type": "~1.0.5",
120 | "debug": "2.6.9",
121 | "depd": "2.0.0",
122 | "destroy": "1.2.0",
123 | "http-errors": "2.0.0",
124 | "iconv-lite": "0.4.24",
125 | "on-finished": "2.4.1",
126 | "qs": "6.11.0",
127 | "raw-body": "2.5.2",
128 | "type-is": "~1.6.18",
129 | "unpipe": "1.0.0"
130 | },
131 | "engines": {
132 | "node": ">= 0.8",
133 | "npm": "1.2.8000 || >= 1.4.16"
134 | }
135 | },
136 | "node_modules/brace-expansion": {
137 | "version": "1.1.11",
138 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
139 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
140 | "dev": true,
141 | "dependencies": {
142 | "balanced-match": "^1.0.0",
143 | "concat-map": "0.0.1"
144 | }
145 | },
146 | "node_modules/braces": {
147 | "version": "3.0.3",
148 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
149 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
150 | "dev": true,
151 | "dependencies": {
152 | "fill-range": "^7.1.1"
153 | },
154 | "engines": {
155 | "node": ">=8"
156 | }
157 | },
158 | "node_modules/bson": {
159 | "version": "6.8.0",
160 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz",
161 | "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==",
162 | "engines": {
163 | "node": ">=16.20.1"
164 | }
165 | },
166 | "node_modules/buffer-equal-constant-time": {
167 | "version": "1.0.1",
168 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
169 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
170 | },
171 | "node_modules/bytes": {
172 | "version": "3.1.2",
173 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
174 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
175 | "engines": {
176 | "node": ">= 0.8"
177 | }
178 | },
179 | "node_modules/call-bind": {
180 | "version": "1.0.7",
181 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
182 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
183 | "dependencies": {
184 | "es-define-property": "^1.0.0",
185 | "es-errors": "^1.3.0",
186 | "function-bind": "^1.1.2",
187 | "get-intrinsic": "^1.2.4",
188 | "set-function-length": "^1.2.1"
189 | },
190 | "engines": {
191 | "node": ">= 0.4"
192 | },
193 | "funding": {
194 | "url": "https://github.com/sponsors/ljharb"
195 | }
196 | },
197 | "node_modules/chokidar": {
198 | "version": "3.6.0",
199 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
200 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
201 | "dev": true,
202 | "dependencies": {
203 | "anymatch": "~3.1.2",
204 | "braces": "~3.0.2",
205 | "glob-parent": "~5.1.2",
206 | "is-binary-path": "~2.1.0",
207 | "is-glob": "~4.0.1",
208 | "normalize-path": "~3.0.0",
209 | "readdirp": "~3.6.0"
210 | },
211 | "engines": {
212 | "node": ">= 8.10.0"
213 | },
214 | "funding": {
215 | "url": "https://paulmillr.com/funding/"
216 | },
217 | "optionalDependencies": {
218 | "fsevents": "~2.3.2"
219 | }
220 | },
221 | "node_modules/combined-stream": {
222 | "version": "1.0.8",
223 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
224 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
225 | "dependencies": {
226 | "delayed-stream": "~1.0.0"
227 | },
228 | "engines": {
229 | "node": ">= 0.8"
230 | }
231 | },
232 | "node_modules/concat-map": {
233 | "version": "0.0.1",
234 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
235 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
236 | "dev": true
237 | },
238 | "node_modules/content-disposition": {
239 | "version": "0.5.4",
240 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
241 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
242 | "dependencies": {
243 | "safe-buffer": "5.2.1"
244 | },
245 | "engines": {
246 | "node": ">= 0.6"
247 | }
248 | },
249 | "node_modules/content-type": {
250 | "version": "1.0.5",
251 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
252 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
253 | "engines": {
254 | "node": ">= 0.6"
255 | }
256 | },
257 | "node_modules/cookie": {
258 | "version": "0.4.1",
259 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
260 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
261 | "engines": {
262 | "node": ">= 0.6"
263 | }
264 | },
265 | "node_modules/cookie-parser": {
266 | "version": "1.4.6",
267 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
268 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
269 | "dependencies": {
270 | "cookie": "0.4.1",
271 | "cookie-signature": "1.0.6"
272 | },
273 | "engines": {
274 | "node": ">= 0.8.0"
275 | }
276 | },
277 | "node_modules/cookie-signature": {
278 | "version": "1.0.6",
279 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
280 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
281 | },
282 | "node_modules/debug": {
283 | "version": "2.6.9",
284 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
285 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
286 | "dependencies": {
287 | "ms": "2.0.0"
288 | }
289 | },
290 | "node_modules/define-data-property": {
291 | "version": "1.1.4",
292 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
293 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
294 | "dependencies": {
295 | "es-define-property": "^1.0.0",
296 | "es-errors": "^1.3.0",
297 | "gopd": "^1.0.1"
298 | },
299 | "engines": {
300 | "node": ">= 0.4"
301 | },
302 | "funding": {
303 | "url": "https://github.com/sponsors/ljharb"
304 | }
305 | },
306 | "node_modules/delayed-stream": {
307 | "version": "1.0.0",
308 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
309 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
310 | "engines": {
311 | "node": ">=0.4.0"
312 | }
313 | },
314 | "node_modules/depd": {
315 | "version": "2.0.0",
316 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
317 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
318 | "engines": {
319 | "node": ">= 0.8"
320 | }
321 | },
322 | "node_modules/destroy": {
323 | "version": "1.2.0",
324 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
325 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
326 | "engines": {
327 | "node": ">= 0.8",
328 | "npm": "1.2.8000 || >= 1.4.16"
329 | }
330 | },
331 | "node_modules/dotenv": {
332 | "version": "16.4.5",
333 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
334 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
335 | "engines": {
336 | "node": ">=12"
337 | },
338 | "funding": {
339 | "url": "https://dotenvx.com"
340 | }
341 | },
342 | "node_modules/ecdsa-sig-formatter": {
343 | "version": "1.0.11",
344 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
345 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
346 | "dependencies": {
347 | "safe-buffer": "^5.0.1"
348 | }
349 | },
350 | "node_modules/ee-first": {
351 | "version": "1.1.1",
352 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
353 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
354 | },
355 | "node_modules/encodeurl": {
356 | "version": "1.0.2",
357 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
358 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
359 | "engines": {
360 | "node": ">= 0.8"
361 | }
362 | },
363 | "node_modules/es-define-property": {
364 | "version": "1.0.0",
365 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
366 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
367 | "dependencies": {
368 | "get-intrinsic": "^1.2.4"
369 | },
370 | "engines": {
371 | "node": ">= 0.4"
372 | }
373 | },
374 | "node_modules/es-errors": {
375 | "version": "1.3.0",
376 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
377 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
378 | "engines": {
379 | "node": ">= 0.4"
380 | }
381 | },
382 | "node_modules/escape-html": {
383 | "version": "1.0.3",
384 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
385 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
386 | },
387 | "node_modules/etag": {
388 | "version": "1.8.1",
389 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
390 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
391 | "engines": {
392 | "node": ">= 0.6"
393 | }
394 | },
395 | "node_modules/express": {
396 | "version": "4.19.2",
397 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
398 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
399 | "dependencies": {
400 | "accepts": "~1.3.8",
401 | "array-flatten": "1.1.1",
402 | "body-parser": "1.20.2",
403 | "content-disposition": "0.5.4",
404 | "content-type": "~1.0.4",
405 | "cookie": "0.6.0",
406 | "cookie-signature": "1.0.6",
407 | "debug": "2.6.9",
408 | "depd": "2.0.0",
409 | "encodeurl": "~1.0.2",
410 | "escape-html": "~1.0.3",
411 | "etag": "~1.8.1",
412 | "finalhandler": "1.2.0",
413 | "fresh": "0.5.2",
414 | "http-errors": "2.0.0",
415 | "merge-descriptors": "1.0.1",
416 | "methods": "~1.1.2",
417 | "on-finished": "2.4.1",
418 | "parseurl": "~1.3.3",
419 | "path-to-regexp": "0.1.7",
420 | "proxy-addr": "~2.0.7",
421 | "qs": "6.11.0",
422 | "range-parser": "~1.2.1",
423 | "safe-buffer": "5.2.1",
424 | "send": "0.18.0",
425 | "serve-static": "1.15.0",
426 | "setprototypeof": "1.2.0",
427 | "statuses": "2.0.1",
428 | "type-is": "~1.6.18",
429 | "utils-merge": "1.0.1",
430 | "vary": "~1.1.2"
431 | },
432 | "engines": {
433 | "node": ">= 0.10.0"
434 | }
435 | },
436 | "node_modules/express/node_modules/cookie": {
437 | "version": "0.6.0",
438 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
439 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
440 | "engines": {
441 | "node": ">= 0.6"
442 | }
443 | },
444 | "node_modules/fill-range": {
445 | "version": "7.1.1",
446 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
447 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
448 | "dev": true,
449 | "dependencies": {
450 | "to-regex-range": "^5.0.1"
451 | },
452 | "engines": {
453 | "node": ">=8"
454 | }
455 | },
456 | "node_modules/finalhandler": {
457 | "version": "1.2.0",
458 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
459 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
460 | "dependencies": {
461 | "debug": "2.6.9",
462 | "encodeurl": "~1.0.2",
463 | "escape-html": "~1.0.3",
464 | "on-finished": "2.4.1",
465 | "parseurl": "~1.3.3",
466 | "statuses": "2.0.1",
467 | "unpipe": "~1.0.0"
468 | },
469 | "engines": {
470 | "node": ">= 0.8"
471 | }
472 | },
473 | "node_modules/follow-redirects": {
474 | "version": "1.15.6",
475 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
476 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
477 | "funding": [
478 | {
479 | "type": "individual",
480 | "url": "https://github.com/sponsors/RubenVerborgh"
481 | }
482 | ],
483 | "engines": {
484 | "node": ">=4.0"
485 | },
486 | "peerDependenciesMeta": {
487 | "debug": {
488 | "optional": true
489 | }
490 | }
491 | },
492 | "node_modules/form-data": {
493 | "version": "4.0.0",
494 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
495 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
496 | "dependencies": {
497 | "asynckit": "^0.4.0",
498 | "combined-stream": "^1.0.8",
499 | "mime-types": "^2.1.12"
500 | },
501 | "engines": {
502 | "node": ">= 6"
503 | }
504 | },
505 | "node_modules/forwarded": {
506 | "version": "0.2.0",
507 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
508 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
509 | "engines": {
510 | "node": ">= 0.6"
511 | }
512 | },
513 | "node_modules/fresh": {
514 | "version": "0.5.2",
515 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
516 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
517 | "engines": {
518 | "node": ">= 0.6"
519 | }
520 | },
521 | "node_modules/fsevents": {
522 | "version": "2.3.3",
523 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
524 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
525 | "dev": true,
526 | "hasInstallScript": true,
527 | "optional": true,
528 | "os": [
529 | "darwin"
530 | ],
531 | "engines": {
532 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
533 | }
534 | },
535 | "node_modules/function-bind": {
536 | "version": "1.1.2",
537 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
538 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
539 | "funding": {
540 | "url": "https://github.com/sponsors/ljharb"
541 | }
542 | },
543 | "node_modules/get-intrinsic": {
544 | "version": "1.2.4",
545 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
546 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
547 | "dependencies": {
548 | "es-errors": "^1.3.0",
549 | "function-bind": "^1.1.2",
550 | "has-proto": "^1.0.1",
551 | "has-symbols": "^1.0.3",
552 | "hasown": "^2.0.0"
553 | },
554 | "engines": {
555 | "node": ">= 0.4"
556 | },
557 | "funding": {
558 | "url": "https://github.com/sponsors/ljharb"
559 | }
560 | },
561 | "node_modules/glob-parent": {
562 | "version": "5.1.2",
563 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
564 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
565 | "dev": true,
566 | "dependencies": {
567 | "is-glob": "^4.0.1"
568 | },
569 | "engines": {
570 | "node": ">= 6"
571 | }
572 | },
573 | "node_modules/gopd": {
574 | "version": "1.0.1",
575 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
576 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
577 | "dependencies": {
578 | "get-intrinsic": "^1.1.3"
579 | },
580 | "funding": {
581 | "url": "https://github.com/sponsors/ljharb"
582 | }
583 | },
584 | "node_modules/has-flag": {
585 | "version": "3.0.0",
586 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
587 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
588 | "dev": true,
589 | "engines": {
590 | "node": ">=4"
591 | }
592 | },
593 | "node_modules/has-property-descriptors": {
594 | "version": "1.0.2",
595 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
596 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
597 | "dependencies": {
598 | "es-define-property": "^1.0.0"
599 | },
600 | "funding": {
601 | "url": "https://github.com/sponsors/ljharb"
602 | }
603 | },
604 | "node_modules/has-proto": {
605 | "version": "1.0.3",
606 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
607 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
608 | "engines": {
609 | "node": ">= 0.4"
610 | },
611 | "funding": {
612 | "url": "https://github.com/sponsors/ljharb"
613 | }
614 | },
615 | "node_modules/has-symbols": {
616 | "version": "1.0.3",
617 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
618 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
619 | "engines": {
620 | "node": ">= 0.4"
621 | },
622 | "funding": {
623 | "url": "https://github.com/sponsors/ljharb"
624 | }
625 | },
626 | "node_modules/hasown": {
627 | "version": "2.0.2",
628 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
629 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
630 | "dependencies": {
631 | "function-bind": "^1.1.2"
632 | },
633 | "engines": {
634 | "node": ">= 0.4"
635 | }
636 | },
637 | "node_modules/http-errors": {
638 | "version": "2.0.0",
639 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
640 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
641 | "dependencies": {
642 | "depd": "2.0.0",
643 | "inherits": "2.0.4",
644 | "setprototypeof": "1.2.0",
645 | "statuses": "2.0.1",
646 | "toidentifier": "1.0.1"
647 | },
648 | "engines": {
649 | "node": ">= 0.8"
650 | }
651 | },
652 | "node_modules/iconv-lite": {
653 | "version": "0.4.24",
654 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
655 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
656 | "dependencies": {
657 | "safer-buffer": ">= 2.1.2 < 3"
658 | },
659 | "engines": {
660 | "node": ">=0.10.0"
661 | }
662 | },
663 | "node_modules/ignore-by-default": {
664 | "version": "1.0.1",
665 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
666 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
667 | "dev": true
668 | },
669 | "node_modules/inherits": {
670 | "version": "2.0.4",
671 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
672 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
673 | },
674 | "node_modules/ipaddr.js": {
675 | "version": "1.9.1",
676 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
677 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
678 | "engines": {
679 | "node": ">= 0.10"
680 | }
681 | },
682 | "node_modules/is-binary-path": {
683 | "version": "2.1.0",
684 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
685 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
686 | "dev": true,
687 | "dependencies": {
688 | "binary-extensions": "^2.0.0"
689 | },
690 | "engines": {
691 | "node": ">=8"
692 | }
693 | },
694 | "node_modules/is-extglob": {
695 | "version": "2.1.1",
696 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
697 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
698 | "dev": true,
699 | "engines": {
700 | "node": ">=0.10.0"
701 | }
702 | },
703 | "node_modules/is-glob": {
704 | "version": "4.0.3",
705 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
706 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
707 | "dev": true,
708 | "dependencies": {
709 | "is-extglob": "^2.1.1"
710 | },
711 | "engines": {
712 | "node": ">=0.10.0"
713 | }
714 | },
715 | "node_modules/is-number": {
716 | "version": "7.0.0",
717 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
718 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
719 | "dev": true,
720 | "engines": {
721 | "node": ">=0.12.0"
722 | }
723 | },
724 | "node_modules/jsonwebtoken": {
725 | "version": "9.0.2",
726 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
727 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
728 | "dependencies": {
729 | "jws": "^3.2.2",
730 | "lodash.includes": "^4.3.0",
731 | "lodash.isboolean": "^3.0.3",
732 | "lodash.isinteger": "^4.0.4",
733 | "lodash.isnumber": "^3.0.3",
734 | "lodash.isplainobject": "^4.0.6",
735 | "lodash.isstring": "^4.0.1",
736 | "lodash.once": "^4.0.0",
737 | "ms": "^2.1.1",
738 | "semver": "^7.5.4"
739 | },
740 | "engines": {
741 | "node": ">=12",
742 | "npm": ">=6"
743 | }
744 | },
745 | "node_modules/jsonwebtoken/node_modules/ms": {
746 | "version": "2.1.3",
747 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
748 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
749 | },
750 | "node_modules/jwa": {
751 | "version": "1.4.1",
752 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
753 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
754 | "dependencies": {
755 | "buffer-equal-constant-time": "1.0.1",
756 | "ecdsa-sig-formatter": "1.0.11",
757 | "safe-buffer": "^5.0.1"
758 | }
759 | },
760 | "node_modules/jws": {
761 | "version": "3.2.2",
762 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
763 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
764 | "dependencies": {
765 | "jwa": "^1.4.1",
766 | "safe-buffer": "^5.0.1"
767 | }
768 | },
769 | "node_modules/kareem": {
770 | "version": "2.6.3",
771 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
772 | "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
773 | "engines": {
774 | "node": ">=12.0.0"
775 | }
776 | },
777 | "node_modules/lodash.includes": {
778 | "version": "4.3.0",
779 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
780 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
781 | },
782 | "node_modules/lodash.isboolean": {
783 | "version": "3.0.3",
784 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
785 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
786 | },
787 | "node_modules/lodash.isinteger": {
788 | "version": "4.0.4",
789 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
790 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
791 | },
792 | "node_modules/lodash.isnumber": {
793 | "version": "3.0.3",
794 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
795 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
796 | },
797 | "node_modules/lodash.isplainobject": {
798 | "version": "4.0.6",
799 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
800 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
801 | },
802 | "node_modules/lodash.isstring": {
803 | "version": "4.0.1",
804 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
805 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
806 | },
807 | "node_modules/lodash.once": {
808 | "version": "4.1.1",
809 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
810 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
811 | },
812 | "node_modules/media-typer": {
813 | "version": "0.3.0",
814 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
815 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
816 | "engines": {
817 | "node": ">= 0.6"
818 | }
819 | },
820 | "node_modules/memory-pager": {
821 | "version": "1.5.0",
822 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
823 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
824 | },
825 | "node_modules/merge-descriptors": {
826 | "version": "1.0.1",
827 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
828 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
829 | },
830 | "node_modules/methods": {
831 | "version": "1.1.2",
832 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
833 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
834 | "engines": {
835 | "node": ">= 0.6"
836 | }
837 | },
838 | "node_modules/mime": {
839 | "version": "1.6.0",
840 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
841 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
842 | "bin": {
843 | "mime": "cli.js"
844 | },
845 | "engines": {
846 | "node": ">=4"
847 | }
848 | },
849 | "node_modules/mime-db": {
850 | "version": "1.52.0",
851 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
852 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
853 | "engines": {
854 | "node": ">= 0.6"
855 | }
856 | },
857 | "node_modules/mime-types": {
858 | "version": "2.1.35",
859 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
860 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
861 | "dependencies": {
862 | "mime-db": "1.52.0"
863 | },
864 | "engines": {
865 | "node": ">= 0.6"
866 | }
867 | },
868 | "node_modules/minimatch": {
869 | "version": "3.1.2",
870 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
871 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
872 | "dev": true,
873 | "dependencies": {
874 | "brace-expansion": "^1.1.7"
875 | },
876 | "engines": {
877 | "node": "*"
878 | }
879 | },
880 | "node_modules/mongodb": {
881 | "version": "6.7.0",
882 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz",
883 | "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==",
884 | "dependencies": {
885 | "@mongodb-js/saslprep": "^1.1.5",
886 | "bson": "^6.7.0",
887 | "mongodb-connection-string-url": "^3.0.0"
888 | },
889 | "engines": {
890 | "node": ">=16.20.1"
891 | },
892 | "peerDependencies": {
893 | "@aws-sdk/credential-providers": "^3.188.0",
894 | "@mongodb-js/zstd": "^1.1.0",
895 | "gcp-metadata": "^5.2.0",
896 | "kerberos": "^2.0.1",
897 | "mongodb-client-encryption": ">=6.0.0 <7",
898 | "snappy": "^7.2.2",
899 | "socks": "^2.7.1"
900 | },
901 | "peerDependenciesMeta": {
902 | "@aws-sdk/credential-providers": {
903 | "optional": true
904 | },
905 | "@mongodb-js/zstd": {
906 | "optional": true
907 | },
908 | "gcp-metadata": {
909 | "optional": true
910 | },
911 | "kerberos": {
912 | "optional": true
913 | },
914 | "mongodb-client-encryption": {
915 | "optional": true
916 | },
917 | "snappy": {
918 | "optional": true
919 | },
920 | "socks": {
921 | "optional": true
922 | }
923 | }
924 | },
925 | "node_modules/mongodb-connection-string-url": {
926 | "version": "3.0.1",
927 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
928 | "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
929 | "dependencies": {
930 | "@types/whatwg-url": "^11.0.2",
931 | "whatwg-url": "^13.0.0"
932 | }
933 | },
934 | "node_modules/mongoose": {
935 | "version": "8.5.1",
936 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.5.1.tgz",
937 | "integrity": "sha512-OhVcwVl91A1G6+XpjDcpkGP7l7ikZkxa0DylX7NT/lcEqAjggzSdqDxb48A+xsDxqNAr0ntSJ1yiE3+KJTOd5Q==",
938 | "dependencies": {
939 | "bson": "^6.7.0",
940 | "kareem": "2.6.3",
941 | "mongodb": "6.7.0",
942 | "mpath": "0.9.0",
943 | "mquery": "5.0.0",
944 | "ms": "2.1.3",
945 | "sift": "17.1.3"
946 | },
947 | "engines": {
948 | "node": ">=16.20.1"
949 | },
950 | "funding": {
951 | "type": "opencollective",
952 | "url": "https://opencollective.com/mongoose"
953 | }
954 | },
955 | "node_modules/mongoose/node_modules/ms": {
956 | "version": "2.1.3",
957 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
958 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
959 | },
960 | "node_modules/mpath": {
961 | "version": "0.9.0",
962 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
963 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
964 | "engines": {
965 | "node": ">=4.0.0"
966 | }
967 | },
968 | "node_modules/mquery": {
969 | "version": "5.0.0",
970 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
971 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
972 | "dependencies": {
973 | "debug": "4.x"
974 | },
975 | "engines": {
976 | "node": ">=14.0.0"
977 | }
978 | },
979 | "node_modules/mquery/node_modules/debug": {
980 | "version": "4.3.5",
981 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
982 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
983 | "dependencies": {
984 | "ms": "2.1.2"
985 | },
986 | "engines": {
987 | "node": ">=6.0"
988 | },
989 | "peerDependenciesMeta": {
990 | "supports-color": {
991 | "optional": true
992 | }
993 | }
994 | },
995 | "node_modules/mquery/node_modules/ms": {
996 | "version": "2.1.2",
997 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
998 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
999 | },
1000 | "node_modules/ms": {
1001 | "version": "2.0.0",
1002 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1003 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1004 | },
1005 | "node_modules/negotiator": {
1006 | "version": "0.6.3",
1007 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1008 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1009 | "engines": {
1010 | "node": ">= 0.6"
1011 | }
1012 | },
1013 | "node_modules/nodemon": {
1014 | "version": "3.1.4",
1015 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz",
1016 | "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==",
1017 | "dev": true,
1018 | "dependencies": {
1019 | "chokidar": "^3.5.2",
1020 | "debug": "^4",
1021 | "ignore-by-default": "^1.0.1",
1022 | "minimatch": "^3.1.2",
1023 | "pstree.remy": "^1.1.8",
1024 | "semver": "^7.5.3",
1025 | "simple-update-notifier": "^2.0.0",
1026 | "supports-color": "^5.5.0",
1027 | "touch": "^3.1.0",
1028 | "undefsafe": "^2.0.5"
1029 | },
1030 | "bin": {
1031 | "nodemon": "bin/nodemon.js"
1032 | },
1033 | "engines": {
1034 | "node": ">=10"
1035 | },
1036 | "funding": {
1037 | "type": "opencollective",
1038 | "url": "https://opencollective.com/nodemon"
1039 | }
1040 | },
1041 | "node_modules/nodemon/node_modules/debug": {
1042 | "version": "4.3.5",
1043 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
1044 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
1045 | "dev": true,
1046 | "dependencies": {
1047 | "ms": "2.1.2"
1048 | },
1049 | "engines": {
1050 | "node": ">=6.0"
1051 | },
1052 | "peerDependenciesMeta": {
1053 | "supports-color": {
1054 | "optional": true
1055 | }
1056 | }
1057 | },
1058 | "node_modules/nodemon/node_modules/ms": {
1059 | "version": "2.1.2",
1060 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1061 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
1062 | "dev": true
1063 | },
1064 | "node_modules/normalize-path": {
1065 | "version": "3.0.0",
1066 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1067 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1068 | "dev": true,
1069 | "engines": {
1070 | "node": ">=0.10.0"
1071 | }
1072 | },
1073 | "node_modules/object-inspect": {
1074 | "version": "1.13.2",
1075 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
1076 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
1077 | "engines": {
1078 | "node": ">= 0.4"
1079 | },
1080 | "funding": {
1081 | "url": "https://github.com/sponsors/ljharb"
1082 | }
1083 | },
1084 | "node_modules/on-finished": {
1085 | "version": "2.4.1",
1086 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1087 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1088 | "dependencies": {
1089 | "ee-first": "1.1.1"
1090 | },
1091 | "engines": {
1092 | "node": ">= 0.8"
1093 | }
1094 | },
1095 | "node_modules/parseurl": {
1096 | "version": "1.3.3",
1097 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1098 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1099 | "engines": {
1100 | "node": ">= 0.8"
1101 | }
1102 | },
1103 | "node_modules/path-to-regexp": {
1104 | "version": "0.1.7",
1105 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
1106 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
1107 | },
1108 | "node_modules/picomatch": {
1109 | "version": "2.3.1",
1110 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1111 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1112 | "dev": true,
1113 | "engines": {
1114 | "node": ">=8.6"
1115 | },
1116 | "funding": {
1117 | "url": "https://github.com/sponsors/jonschlinkert"
1118 | }
1119 | },
1120 | "node_modules/proxy-addr": {
1121 | "version": "2.0.7",
1122 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1123 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1124 | "dependencies": {
1125 | "forwarded": "0.2.0",
1126 | "ipaddr.js": "1.9.1"
1127 | },
1128 | "engines": {
1129 | "node": ">= 0.10"
1130 | }
1131 | },
1132 | "node_modules/proxy-from-env": {
1133 | "version": "1.1.0",
1134 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1135 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
1136 | },
1137 | "node_modules/pstree.remy": {
1138 | "version": "1.1.8",
1139 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1140 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1141 | "dev": true
1142 | },
1143 | "node_modules/punycode": {
1144 | "version": "2.3.1",
1145 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1146 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1147 | "engines": {
1148 | "node": ">=6"
1149 | }
1150 | },
1151 | "node_modules/qs": {
1152 | "version": "6.11.0",
1153 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
1154 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
1155 | "dependencies": {
1156 | "side-channel": "^1.0.4"
1157 | },
1158 | "engines": {
1159 | "node": ">=0.6"
1160 | },
1161 | "funding": {
1162 | "url": "https://github.com/sponsors/ljharb"
1163 | }
1164 | },
1165 | "node_modules/range-parser": {
1166 | "version": "1.2.1",
1167 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1168 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1169 | "engines": {
1170 | "node": ">= 0.6"
1171 | }
1172 | },
1173 | "node_modules/raw-body": {
1174 | "version": "2.5.2",
1175 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1176 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1177 | "dependencies": {
1178 | "bytes": "3.1.2",
1179 | "http-errors": "2.0.0",
1180 | "iconv-lite": "0.4.24",
1181 | "unpipe": "1.0.0"
1182 | },
1183 | "engines": {
1184 | "node": ">= 0.8"
1185 | }
1186 | },
1187 | "node_modules/readdirp": {
1188 | "version": "3.6.0",
1189 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1190 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1191 | "dev": true,
1192 | "dependencies": {
1193 | "picomatch": "^2.2.1"
1194 | },
1195 | "engines": {
1196 | "node": ">=8.10.0"
1197 | }
1198 | },
1199 | "node_modules/safe-buffer": {
1200 | "version": "5.2.1",
1201 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1202 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1203 | "funding": [
1204 | {
1205 | "type": "github",
1206 | "url": "https://github.com/sponsors/feross"
1207 | },
1208 | {
1209 | "type": "patreon",
1210 | "url": "https://www.patreon.com/feross"
1211 | },
1212 | {
1213 | "type": "consulting",
1214 | "url": "https://feross.org/support"
1215 | }
1216 | ]
1217 | },
1218 | "node_modules/safer-buffer": {
1219 | "version": "2.1.2",
1220 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1221 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1222 | },
1223 | "node_modules/semver": {
1224 | "version": "7.6.2",
1225 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
1226 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
1227 | "bin": {
1228 | "semver": "bin/semver.js"
1229 | },
1230 | "engines": {
1231 | "node": ">=10"
1232 | }
1233 | },
1234 | "node_modules/send": {
1235 | "version": "0.18.0",
1236 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
1237 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
1238 | "dependencies": {
1239 | "debug": "2.6.9",
1240 | "depd": "2.0.0",
1241 | "destroy": "1.2.0",
1242 | "encodeurl": "~1.0.2",
1243 | "escape-html": "~1.0.3",
1244 | "etag": "~1.8.1",
1245 | "fresh": "0.5.2",
1246 | "http-errors": "2.0.0",
1247 | "mime": "1.6.0",
1248 | "ms": "2.1.3",
1249 | "on-finished": "2.4.1",
1250 | "range-parser": "~1.2.1",
1251 | "statuses": "2.0.1"
1252 | },
1253 | "engines": {
1254 | "node": ">= 0.8.0"
1255 | }
1256 | },
1257 | "node_modules/send/node_modules/ms": {
1258 | "version": "2.1.3",
1259 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1260 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1261 | },
1262 | "node_modules/serve-static": {
1263 | "version": "1.15.0",
1264 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1265 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1266 | "dependencies": {
1267 | "encodeurl": "~1.0.2",
1268 | "escape-html": "~1.0.3",
1269 | "parseurl": "~1.3.3",
1270 | "send": "0.18.0"
1271 | },
1272 | "engines": {
1273 | "node": ">= 0.8.0"
1274 | }
1275 | },
1276 | "node_modules/set-function-length": {
1277 | "version": "1.2.2",
1278 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
1279 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
1280 | "dependencies": {
1281 | "define-data-property": "^1.1.4",
1282 | "es-errors": "^1.3.0",
1283 | "function-bind": "^1.1.2",
1284 | "get-intrinsic": "^1.2.4",
1285 | "gopd": "^1.0.1",
1286 | "has-property-descriptors": "^1.0.2"
1287 | },
1288 | "engines": {
1289 | "node": ">= 0.4"
1290 | }
1291 | },
1292 | "node_modules/setprototypeof": {
1293 | "version": "1.2.0",
1294 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1295 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1296 | },
1297 | "node_modules/side-channel": {
1298 | "version": "1.0.6",
1299 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
1300 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
1301 | "dependencies": {
1302 | "call-bind": "^1.0.7",
1303 | "es-errors": "^1.3.0",
1304 | "get-intrinsic": "^1.2.4",
1305 | "object-inspect": "^1.13.1"
1306 | },
1307 | "engines": {
1308 | "node": ">= 0.4"
1309 | },
1310 | "funding": {
1311 | "url": "https://github.com/sponsors/ljharb"
1312 | }
1313 | },
1314 | "node_modules/sift": {
1315 | "version": "17.1.3",
1316 | "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
1317 | "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="
1318 | },
1319 | "node_modules/simple-update-notifier": {
1320 | "version": "2.0.0",
1321 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1322 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1323 | "dev": true,
1324 | "dependencies": {
1325 | "semver": "^7.5.3"
1326 | },
1327 | "engines": {
1328 | "node": ">=10"
1329 | }
1330 | },
1331 | "node_modules/sparse-bitfield": {
1332 | "version": "3.0.3",
1333 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
1334 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
1335 | "dependencies": {
1336 | "memory-pager": "^1.0.2"
1337 | }
1338 | },
1339 | "node_modules/statuses": {
1340 | "version": "2.0.1",
1341 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1342 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1343 | "engines": {
1344 | "node": ">= 0.8"
1345 | }
1346 | },
1347 | "node_modules/supports-color": {
1348 | "version": "5.5.0",
1349 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1350 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1351 | "dev": true,
1352 | "dependencies": {
1353 | "has-flag": "^3.0.0"
1354 | },
1355 | "engines": {
1356 | "node": ">=4"
1357 | }
1358 | },
1359 | "node_modules/to-regex-range": {
1360 | "version": "5.0.1",
1361 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1362 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1363 | "dev": true,
1364 | "dependencies": {
1365 | "is-number": "^7.0.0"
1366 | },
1367 | "engines": {
1368 | "node": ">=8.0"
1369 | }
1370 | },
1371 | "node_modules/toidentifier": {
1372 | "version": "1.0.1",
1373 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1374 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1375 | "engines": {
1376 | "node": ">=0.6"
1377 | }
1378 | },
1379 | "node_modules/touch": {
1380 | "version": "3.1.1",
1381 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1382 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1383 | "dev": true,
1384 | "bin": {
1385 | "nodetouch": "bin/nodetouch.js"
1386 | }
1387 | },
1388 | "node_modules/tr46": {
1389 | "version": "4.1.1",
1390 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
1391 | "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
1392 | "dependencies": {
1393 | "punycode": "^2.3.0"
1394 | },
1395 | "engines": {
1396 | "node": ">=14"
1397 | }
1398 | },
1399 | "node_modules/type-is": {
1400 | "version": "1.6.18",
1401 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1402 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1403 | "dependencies": {
1404 | "media-typer": "0.3.0",
1405 | "mime-types": "~2.1.24"
1406 | },
1407 | "engines": {
1408 | "node": ">= 0.6"
1409 | }
1410 | },
1411 | "node_modules/undefsafe": {
1412 | "version": "2.0.5",
1413 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1414 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1415 | "dev": true
1416 | },
1417 | "node_modules/unpipe": {
1418 | "version": "1.0.0",
1419 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1420 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1421 | "engines": {
1422 | "node": ">= 0.8"
1423 | }
1424 | },
1425 | "node_modules/utils-merge": {
1426 | "version": "1.0.1",
1427 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1428 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1429 | "engines": {
1430 | "node": ">= 0.4.0"
1431 | }
1432 | },
1433 | "node_modules/vary": {
1434 | "version": "1.1.2",
1435 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1436 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1437 | "engines": {
1438 | "node": ">= 0.8"
1439 | }
1440 | },
1441 | "node_modules/webidl-conversions": {
1442 | "version": "7.0.0",
1443 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
1444 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
1445 | "engines": {
1446 | "node": ">=12"
1447 | }
1448 | },
1449 | "node_modules/whatwg-url": {
1450 | "version": "13.0.0",
1451 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
1452 | "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
1453 | "dependencies": {
1454 | "tr46": "^4.1.1",
1455 | "webidl-conversions": "^7.0.0"
1456 | },
1457 | "engines": {
1458 | "node": ">=16"
1459 | }
1460 | }
1461 | }
1462 | }
1463 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netflix-clone",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "backend/server.js",
6 | "scripts": {
7 | "dev": "NODE_ENV=development nodemon backend/server.js",
8 | "start": "NODE_ENV=production node backend/server.js",
9 | "build": "npm install && npm install --prefix frontend && npm run build --prefix frontend"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "type": "module",
14 | "license": "ISC",
15 | "dependencies": {
16 | "axios": "^1.7.2",
17 | "bcryptjs": "^2.4.3",
18 | "cookie-parser": "^1.4.6",
19 | "dotenv": "^16.4.5",
20 | "express": "^4.19.2",
21 | "jsonwebtoken": "^9.0.2",
22 | "mongoose": "^8.5.1"
23 | },
24 | "devDependencies": {
25 | "nodemon": "^3.1.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------