├── .gitignore ├── .env ├── public ├── stylesheets │ └── style.css └── index.html ├── routes ├── index.js ├── auth.js ├── watchList.js └── movies.js ├── middlewares ├── admin.js └── auth.js ├── utils └── jwtHelpers.js ├── package.json ├── Models ├── user.js └── movie.js ├── app.js ├── controllers ├── watchListConroller.js ├── authController.js └── moviesConroller.js └── bin └── www /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=4000 2 | DB_URL=mongodb://localhost:27017/movies-api 3 | JWT_SECRET=super-secret-goes-here 4 | JWT_EXPIRES_IN=2h -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get("/", function (req, res, next) { 6 | res.render("index", {title: "Express"}); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |Welcome to Express by me
10 | 11 | 12 | -------------------------------------------------------------------------------- /middlewares/admin.js: -------------------------------------------------------------------------------- 1 | const User = require("../Models/user"); 2 | 3 | exports.check = async (req, res, next) => { 4 | const user = await User.findById(req.userId); 5 | user.isAdmin 6 | ? next() 7 | : res.status(403).json({ 8 | message: "Forbidden", 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const auth = require("../middlewares/auth"); 4 | const controller = require("../controllers/authController"); 5 | 6 | router.post("/login", controller.login); 7 | router.post("/register", controller.register); 8 | router.get("/me", auth.check, controller.me); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /utils/jwtHelpers.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const secret = process.env.JWT_SECRET; 3 | const expiresIn = process.env.JWT_EXPIRES_IN; 4 | 5 | exports.sign = (payload) => { 6 | return jwt.sign(payload, secret, {expiresIn}); 7 | }; 8 | 9 | exports.verify = (token) => { 10 | try { 11 | return jwt.verify(token, secret); 12 | } catch (e) { 13 | return false; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /routes/watchList.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const auth = require("../middlewares/auth"); 4 | const controller = require("../controllers/watchListConroller"); 5 | 6 | router.post("/", auth.check, controller.add); 7 | router.delete("/:movie", auth.check, controller.delete); 8 | router.get("/", auth.check, controller.list); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const jwtHelper = require("../utils/jwtHelpers"); 2 | 3 | exports.check = (req, res, next) => { 4 | let token = req.headers["authorization"]; 5 | token = token?.replace("Bearer", "")?.trim(); 6 | const payload = jwtHelper.verify(token); 7 | 8 | if (payload) { 9 | req.userId = payload.sub; 10 | return next(); 11 | } 12 | res.status(401).json({message: "unauthorized!"}); 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movies-api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "commonjs", 6 | "scripts": { 7 | "start": "node ./bin/www", 8 | "dev": "nodemon ./bin/www" 9 | }, 10 | "dependencies": { 11 | "bcrypt": "^5.1.0", 12 | "cookie-parser": "~1.4.4", 13 | "debug": "~2.6.9", 14 | "dotenv": "^16.3.1", 15 | "express": "~4.16.1", 16 | "jsonwebtoken": "^9.0.1", 17 | "mongoose": "^7.4.2", 18 | "morgan": "~1.9.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /routes/movies.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const auth = require("../middlewares/auth"); 4 | const controller = require("../controllers/moviesConroller"); 5 | const admin = require("../middlewares/admin"); 6 | 7 | router.post("/", [auth.check, admin.check], controller.create); 8 | router.put("/:id", [auth.check, admin.check], controller.update); 9 | router.delete("/:id", [auth.check, admin.check], controller.delete); 10 | router.get("/", auth.check, controller.list); 11 | router.get("/:id", auth.check, controller.find); 12 | router.post("/:id/reviews", auth.check, controller.addReview); 13 | router.get("/:id/reviews", controller.reviews); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /Models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const ModelSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true, 7 | maxlength: 50, 8 | }, 9 | email: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | minlength: 6, 18 | }, 19 | watchList: [ 20 | { 21 | movie: { 22 | type: mongoose.Schema.Types.ObjectId, 23 | ref: "Movie", 24 | }, 25 | watched: Boolean, 26 | }, 27 | ], 28 | isAdmin: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | }); 33 | 34 | const Model = mongoose.model("User", ModelSchema); 35 | 36 | module.exports = Model; 37 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const path = require("path"); 3 | const cookieParser = require("cookie-parser"); 4 | const logger = require("morgan"); 5 | const mongoose = require("mongoose"); 6 | 7 | const indexRouter = require("./routes/index"); 8 | const authRouter = require("./routes/auth"); 9 | const moviesRouter = require("./routes/movies"); 10 | const watchListRouter = require("./routes/watchList"); 11 | 12 | const app = express(); 13 | 14 | app.use(logger("dev")); 15 | app.use(express.json()); 16 | app.use(express.urlencoded({extended: false})); 17 | app.use(cookieParser()); 18 | app.use(express.static(path.join(__dirname, "public"))); 19 | 20 | app.use("/", indexRouter); 21 | app.use("/api/auth", authRouter); 22 | app.use("/api/movies", moviesRouter); 23 | app.use("/api/watchlist", watchListRouter); 24 | 25 | mongoose.connect(process.env.DB_URL); 26 | 27 | module.exports = app; 28 | -------------------------------------------------------------------------------- /Models/movie.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const ModelSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | category: { 10 | type: String, 11 | required: true, 12 | }, 13 | description: { 14 | type: String, 15 | required: true, 16 | }, 17 | rate: { 18 | type: Number, 19 | default: 0, 20 | }, 21 | reviews: { 22 | type: [ 23 | { 24 | user: { 25 | type: mongoose.Schema.Types.ObjectId, 26 | ref: "User", 27 | }, 28 | comment: String, 29 | rate: Number, 30 | }, 31 | ], 32 | default: [], 33 | }, 34 | }, 35 | { 36 | timestamps: true, 37 | } 38 | ); 39 | 40 | ModelSchema.set("toJSON", { 41 | virtuals: true, 42 | versionKey: false, 43 | transform: (doc, ret) => { 44 | delete ret._id; 45 | }, 46 | }); 47 | const Model = mongoose.model("Movie", ModelSchema); 48 | 49 | module.exports = Model; 50 | -------------------------------------------------------------------------------- /controllers/watchListConroller.js: -------------------------------------------------------------------------------- 1 | const User = require("../Models/user"); 2 | 3 | exports.add = async (req, res) => { 4 | const {movie, watched} = req.body; 5 | const user = await User.findById(req.userId); 6 | const index = user.watchList.findIndex((e) => e.movie === movie); 7 | 8 | if (index > -1) { 9 | user.watchList[index].watched = watch; 10 | } else { 11 | user.watchList.push({movie, watched}); 12 | } 13 | await user.save(); 14 | res.json({ 15 | success: true, 16 | }); 17 | }; 18 | exports.delete = async (req, res) => { 19 | const {movie} = req.params; 20 | const user = await User.findById(req.userId); 21 | user.watchList = user.watchList.filter((e) => e.movie != movie); 22 | await user.save(); 23 | res.json({ 24 | success: true, 25 | }); 26 | }; 27 | exports.list = async (req, res) => { 28 | const user = await User.findById(req.userId) 29 | .select("-watchList._id") 30 | .populate("watchList.movie", ["name", "category"]); 31 | res.json({ 32 | success: true, 33 | data: user.watchList, 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require("../Models/user"); 2 | const jwtHelpers = require("../utils/jwtHelpers"); 3 | const bcrypt = require("bcrypt"); 4 | 5 | exports.login = async (req, res) => { 6 | const {email, password} = req.body; 7 | const user = await User.findOne({email}); 8 | if (user && bcrypt.compareSync(password, user.password)) { 9 | res.json({ 10 | success: true, 11 | data: { 12 | id: user.id, 13 | name: user.name, 14 | accessToken: jwtHelpers.sign({sub: user.id}), 15 | }, 16 | }); 17 | } else { 18 | res.status(401).json({message: "Invalid Credentials"}); 19 | } 20 | }; 21 | 22 | exports.register = async (req, res) => { 23 | const {name, email, password} = req.body; 24 | 25 | const user = User({ 26 | name, 27 | email, 28 | password: bcrypt.hashSync(password, 8), 29 | }); 30 | 31 | try { 32 | await user.save(); 33 | res.json({ 34 | success: true, 35 | }); 36 | } catch (e) { 37 | res.status(500).json({ 38 | success: "somthing went wrong", 39 | }); 40 | } 41 | }; 42 | 43 | exports.me = async (req, res) => { 44 | const user = await User.findById(req.userId); 45 | res.json({ 46 | success: true, 47 | data: { 48 | id: user.id, 49 | name: user.name, 50 | email: user.email, 51 | }, 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | require("dotenv").config(); 7 | var app = require("../app"); 8 | var debug = require("debug")("movies-api:server"); 9 | var http = require("http"); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || "3000"); 16 | app.set("port", port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on("error", onError); 30 | server.on("listening", onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== "listen") { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === "string" ? "Pipe " + port : "Port " + port; 62 | 63 | // handle specific listen errors with friendly messages 64 | switch (error.code) { 65 | case "EACCES": 66 | console.error(bind + " requires elevated privileges"); 67 | process.exit(1); 68 | break; 69 | case "EADDRINUSE": 70 | console.error(bind + " is already in use"); 71 | process.exit(1); 72 | break; 73 | default: 74 | throw error; 75 | } 76 | } 77 | 78 | /** 79 | * Event listener for HTTP server "listening" event. 80 | */ 81 | 82 | function onListening() { 83 | var addr = server.address(); 84 | var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; 85 | debug("Listening on " + bind); 86 | } 87 | -------------------------------------------------------------------------------- /controllers/moviesConroller.js: -------------------------------------------------------------------------------- 1 | const Movie = require("../Models/movie"); 2 | 3 | exports.create = async (req, res) => { 4 | const {name, category, description} = req.body; 5 | const movie = Movie({ 6 | name, 7 | category, 8 | description, 9 | }); 10 | await movie.save(); 11 | 12 | res.json({ 13 | success: true, 14 | data: movie, 15 | }); 16 | }; 17 | 18 | exports.find = async (req, res) => { 19 | const {id} = req.params; 20 | const movie = await Movie.findById(id).select("-reviews"); 21 | if (!movie) { 22 | return res.status(404).send(); 23 | } 24 | res.json({success: true, data: movie}); 25 | }; 26 | 27 | exports.update = async (req, res) => { 28 | const {id} = req.params; 29 | const {name, category, description} = req.body; 30 | await Movie.updateOne( 31 | {_id: id}, 32 | { 33 | $set: { 34 | name, 35 | category, 36 | description, 37 | }, 38 | } 39 | ); 40 | 41 | res.json({success: true}); 42 | }; 43 | 44 | exports.delete = async (req, res) => { 45 | const {id} = req.params; 46 | await Movie.deleteOne({_id: id}); 47 | res.json({success: true}); 48 | }; 49 | 50 | exports.list = async (req, res) => { 51 | const page = req.query?.page || 1; 52 | const limit = 20; 53 | const skip = (page - 1) * limit; 54 | const movies = await Movie.find().select("-reviews").skip(skip).limit(limit); 55 | const total = await Movie.countDocuments(); 56 | const pages = Math.ceil(total / limit); 57 | res.json({ 58 | success: true, 59 | pages: pages, 60 | data: movies, 61 | }); 62 | }; 63 | 64 | exports.reviews = async (req, res) => { 65 | const {id} = req.params; 66 | const movie = await Movie.findById(id) 67 | .select("-reviews._id") 68 | .populate("reviews.user", "name"); 69 | if (!movie) res.status(404).send(); 70 | 71 | res.json({ 72 | success: true, 73 | data: movie.reviews, 74 | }); 75 | }; 76 | 77 | exports.addReview = async (req, res) => { 78 | const {id} = req.params; 79 | const {comment, rate} = req.body; 80 | 81 | const movie = await Movie.findById(id); 82 | if (!movie) { 83 | return res.status(404).send(); 84 | } 85 | 86 | const isRated = movie.reviews.findIndex((m) => m.user === req.userId); 87 | 88 | if (isRated > -1) { 89 | return res.status(403).send({message: "Review is already added"}); 90 | } 91 | 92 | // const totalRate = movie.reviews.reduce((sum, review) => sum + review, 0); 93 | // const finalRate = (totalRate + rate) / (movie.reviews.length + 1); 94 | const totalRate = movie.reviews.reduce((sum, review) => sum + review.rate, 0); 95 | const finalRate = (totalRate + rate) / (movie.reviews.length + 1); 96 | 97 | await Movie.updateOne( 98 | {_id: id}, 99 | { 100 | $push: { 101 | reviews: { 102 | user: req.userId, 103 | comment, 104 | rate, 105 | }, 106 | }, 107 | $set: {rate: finalRate}, 108 | } 109 | ); 110 | 111 | res.status(201).json({ 112 | success: true, 113 | }); 114 | }; 115 | --------------------------------------------------------------------------------