diff --git a/Utils/errorhander.js b/Utils/errorhander.js new file mode 100644 index 0000000..60df087 --- /dev/null +++ b/Utils/errorhander.js @@ -0,0 +1,25 @@ + +class ErrorHander extends Error { + constructor(message, status) { + + // Calling parent constructor of base Error class. + super(message); + + // Saving class name in the property of our custom error as a shortcut. + this.name = this.constructor.name; + + // Capturing stack trace, excluding constructor call from it + // (this is probably no longer required in node >=8, see the comments) + Error.captureStackTrace(this, this.constructor); + + // You can use any additional properties you want. + // I'm going to use preferred HTTP status for this error types. + // `500` is the default value if not specified. + this.status = status || 500; + + } +}; + + +//module.exports = ErrorHander +export default ErrorHander; \ No newline at end of file diff --git a/Utils/jwtToken.js b/Utils/jwtToken.js index a6f85c3..50317a4 100644 --- a/Utils/jwtToken.js +++ b/Utils/jwtToken.js @@ -1,4 +1,4 @@ -// Create Token and saving in cookie +//Create Token and saving in cookie const sendToken = (user, statusCode, res) => { const token = user.getJWTToken(); diff --git a/Utils/sendEmail.js b/Utils/sendEmail.js new file mode 100644 index 0000000..86dc0cd --- /dev/null +++ b/Utils/sendEmail.js @@ -0,0 +1,27 @@ +import nodeMailer from "nodemailer" + +const sendEmail = async (options) => { + const transporter = nodeMailer.createTransport({ + host: process.env.SMPT_HOST, + port: process.env.SMPT_PORT, + service: process.env.SMPT_SERVICE, + auth: { + user: process.env.SMPT_MAIL, + pass: process.env.SMPT_PASSWORD, + }, + }); + // console.log(process.env.SMPT_PORT) + // console.log(process.env.SMPT_MAIL) + // console.log(process.env.SMPT_PASSWORD) + //console.log(transporter) + + const mailOptions = { + from: process.env.SMPT_MAIL, + to: options.email, + subject: options.subject, + text: options.message, + }; + + await transporter.sendMail(mailOptions); +}; +export default sendEmail; diff --git a/app.js b/app.js index 2ad18e7..95712c9 100644 --- a/app.js +++ b/app.js @@ -24,7 +24,7 @@ app.use(fileUpload({ //auth import user from "./routes/userRoute.js" -app.use("/api", user); +app.use("/api/v1/", user); //category import category from "./routes/categoryRoute.js" app.use("/api", category); diff --git a/controllers/userController.js b/controllers/userController.js index 5a50c55..42d9b61 100644 --- a/controllers/userController.js +++ b/controllers/userController.js @@ -1,129 +1,227 @@ - +//require("dotenv").config({ path: "backend/config/config.env" }); +import ErrorHander from "../utils/errorhander.js" +import catchAsyncErrors from "../middlewares/catchAsyncErrors.js" import User from "../models/userModel.js" -import sendToken from "../Utils/jwtToken.js" -// 1.Register a User -export const registerUser = async (req, res) => { - try { - const { name, email, password, confirmPassword } = req.body; - // console.log(name) - if (password !== confirmPassword) { - res.status(401).json({ msg: "Password not Match!!" }) - } - const user = await User.create({ - name, - email, - password - }); - // const token = user.getJWTToken(); - // // console.log(token) - // res.status(201).json({ - // success: true, - // token, - // }) - sendToken(user, 201, res); +import sendToken from "../utils/jwtToken.js" +import sendEmail from "../utils/sendEmail.js" +import crypto from "crypto" +import cloudinary from "cloudinary" - } catch (error) { - console.log(error) - res.status(500).json({ - success: false, - msg: "Failled to register !!" - }); - } -}; +// 1.Register a User +export const registerUser = catchAsyncErrors(async (req, res, next) => { + const files = req.files.avatar; + const myCloud = await cloudinary.uploader.upload(files.tempFilePath, { + folder: "cmp-user/image", + }, + function (error, result) { (result, error) }); + + const { name, email, password, phone } = req.body; + + const user = await User.create({ + name, + email, + password, + phone, + avatar: { + public_id: myCloud.public_id, + url: myCloud.secure_url, + }, + }); + sendToken(user, 201, res); +}); // 2.Login User -export const loginUser = async (req, res) => { - try { - const { email, password } = req.body; +export const loginUser = catchAsyncErrors(async (req, res, next) => { + const { email, password } = req.body; - // checking if user has given password and email both + // checking if user has given password and email both - if (!email || !password) { - res.status(400).json({ msg: "Please Enter Email & Password" }) - } - - const user = await User.findOne({ email }).select("+password"); - - if (!user) { - res.status(401).json({ msg: "Invalid email or password" }) - } - - const isPasswordMatched = await user.comparePassword(password); - - if (!isPasswordMatched) { - res.status(401).json({ msg: "Invalid email or password" }) - } - // const token = user.getJWTToken(); - // res.status(201).json({ - // success: true, - // token, - // }) - sendToken(user, 200, res); - } catch (error) { - res.status(500).json({ - success: false, - msg: "Failled to Login !!" - }); + if (!email || !password) { + return next(new ErrorHander("Please Enter Email & Password", 400)); } -}; + const user = await User.findOne({ email }).select("+password"); + + if (!user) { + return next(new ErrorHander("Invalid email or password", 401)); + } + + const isPasswordMatched = await user.comparePassword(password); + + if (!isPasswordMatched) { + return next(new ErrorHander("Invalid email or password", 401)); + } + // const token = user.getJWTToken(); + // res.status(201).json({ + // success: true, + // token, + // }) + sendToken(user, 200, res); +}); // 3.Logout User -export const logout = async (req, res) => { +export const logout = catchAsyncErrors(async (req, res, next) => { + res.cookie("token", null, { + expires: new Date(Date.now()), + httpOnly: true, + }); + + res.status(200).json({ + success: true, + message: "Logged Out", + }); +}); + + +// 4.Forgot Password + +export const forgotPassword = catchAsyncErrors(async (req, res, next) => { + const user = await User.findOne({ email: req.body.email }); + + if (!user) { + return next(new ErrorHander("User not found", 404)); + } + // Get ResetPassword Token + const resetToken = user.getResetPasswordToken();//call function + + //save database reset token + await user.save({ validateBeforeSave: false }); + //create link for send mail + // const resetPasswordUrl = `http://localhost:5000/api/v1/user/password/reset/${resetToken}` //send from localhost + //send from anyhost + const resetPasswordUrl = `${req.protocol}://${req.get( + "host" + )}/api/v1/user/password/reset/${resetToken}`; + //const resetPasswordUrl = `${process.env.FRONTEND_URL}:/api/user/password/reset/${resetToken}`; + //const resetPasswordUrl = `${process.env.FRONTEND_URL}/password/reset/${resetToken}`; + + + const message = `Your password reset token are :- \n\n ${resetPasswordUrl} \n\nIf you have not requested this email then, please ignore it.`; + try { - res.cookie("token", null, { - expires: new Date(Date.now()), - httpOnly: true, + await sendEmail({ + email: user.email, + subject: `CMP Password Recovery`, + message, }); res.status(200).json({ success: true, - message: "Logged Out", + message: `Email sent to ${user.email} successfully`, }); } catch (error) { - res.status(500).json({ - success: false, - msg: "Failled to logOut !!" - }); + user.resetPasswordToken = undefined; + user.resetPasswordExpire = undefined; + + await user.save({ validateBeforeSave: false }); + + return next(new ErrorHander(error.message, 500)); + } +}); + + +// 5.Reset Password +export const resetPassword = catchAsyncErrors(async (req, res, next) => { + // creating token hash + const resetPasswordToken = crypto + .createHash("sha256") + .update(req.params.token) + .digest("hex"); + + const user = await User.findOne({ + resetPasswordToken, + resetPasswordExpire: { $gt: Date.now() }, + }); + + if (!user) { + return next( + new ErrorHander( + "Reset Password Token is invalid or has been expired", + 400 + ) + ); + } + //replace previous password + if (req.body.password !== req.body.confirmPassword) { + return next(new ErrorHander("Password does not password", 400)); } + user.password = req.body.password; + user.resetPasswordToken = undefined; + user.resetPasswordExpire = undefined; -}; + await user.save(); + sendToken(user, 200, res); +}); -// 4.update User password -export const updatePassword = async (req, res) => { - try { - // console.log("fhrbhebhgbfr") - // console.log(req.user._id) - if (!req.user) { - return res.status(400).json({ message: 'User Not Found' }); - } - const user = await User.findById(req.user._id).select("+password"); +//6.Get User Detail +export const getUserDetails = catchAsyncErrors(async (req, res, next) => { + const user = await User.findById(req.user.id); - const isPasswordMatched = await user.comparePassword(req.body.oldPassword); + res.status(200).json({ + success: true, + user, + }); +}); - if (!isPasswordMatched) { - res.status(400).json({ msg: "Old password is incorrect" }) +// 7.update User password +export const updatePassword = catchAsyncErrors(async (req, res, next) => { + const user = await User.findById(req.user.id).select("+password"); - } + const isPasswordMatched = await user.comparePassword(req.body.oldPassword); - if (req.body.newPassword !== req.body.confirmPassword) { - res.status(400).json({ msg: "password does not match" }) - - } - - user.password = req.body.newPassword; - - await user.save(); - - sendToken(user, 200, res); - } catch (error) { - // console.log(error) - res.status(500).json({ - success: false, - msg: "Failled to Password Change !!" - }); + if (!isPasswordMatched) { + return next(new ErrorHander("Old password is incorrect", 400)); } -} + + if (req.body.newPassword !== req.body.confirmPassword) { + return next(new ErrorHander("password does not match", 400)); + } + + user.password = req.body.newPassword; + + await user.save(); + + sendToken(user, 200, res); +}); + +// 8.update User Profile +export const updateProfile = catchAsyncErrors(async (req, res, next) => { + const newUserData = { + name: req.body.name, + phone: req.body.phone, + email: req.body.email, + }; + + if (req.files) { + const files = req.files.avatar; + const user = await User.findById(req.user.id); + + const imageId = user.avatar.public_id; + + await cloudinary.uploader.destroy(imageId) + + const myCloud = await cloudinary.uploader.upload(files.tempFilePath, { + folder: "image", + }, + function (error, result) { (result, error) }); + + newUserData.avatar = { + public_id: myCloud.public_id, + url: myCloud.secure_url, + }; + } + const user = await User.findByIdAndUpdate(req.user.id, newUserData, { + new: true, + runValidators: true, + useFindAndModify: false, + }); + + res.status(200).json({ + success: true, + user + }); +}); + diff --git a/middlewares/auth.js b/middlewares/auth.js index b9cf9f8..80df338 100644 --- a/middlewares/auth.js +++ b/middlewares/auth.js @@ -1,14 +1,16 @@ import User from "../models/userModel.js"; import jwt from "jsonwebtoken"; +import ErrorHander from "../utils/errorhander.js" -export const isAuthenticated = async (req, res, next) => { +export const isAuthenticatedUser = async (req, res, next) => { try { - // const { token } = req.cookies; - const getToken = req.headers; + const { token } = req.cookies; + //const getToken = req.headers; // // console.log(getToken.authorization) + // console.log(token) // //remove Bearer from token - const token = getToken.authorization.slice(7); + // const token = getToken.authorization.slice(7); // // console.log(token) if (!token) { @@ -33,3 +35,17 @@ export const isAuthenticated = async (req, res, next) => { }); } }; +export const authorizeRoles = (...roles) => {//pass admin + return (req, res, next) => { + if (!roles.includes(req.user.role)) { + return next( + new ErrorHander( + `Role: ${req.user.role} is not allowed to access this resouce `, + 403 + ) + ); + } + + next(); + }; +}; \ No newline at end of file diff --git a/middlewares/catchAsyncErrors.js b/middlewares/catchAsyncErrors.js new file mode 100644 index 0000000..7595c68 --- /dev/null +++ b/middlewares/catchAsyncErrors.js @@ -0,0 +1,3 @@ +export default (theFunc) => (req, res, next) => { + Promise.resolve(theFunc(req, res, next)).catch(next);//prebuild javascript class// basicaly try catch +}; \ No newline at end of file diff --git a/middlewares/error.js b/middlewares/error.js new file mode 100644 index 0000000..c560aea --- /dev/null +++ b/middlewares/error.js @@ -0,0 +1,36 @@ +import ErrorHandler from "../utils/errorhander.js"; + + +module.exports = (err, req, res, next) => { + err.statusCode = err.statusCode || 500; + err.message = err.message || "Internal Server Error"; + + // Wrong Mongodb Id error(castError) + if (err.name === "CastError") { + const message = `Resource not found. Invalid: ${err.path}`; + err = new ErrorHandler(message, 400); + } + + // Mongoose duplicate key error + if (err.code === 11000) { + const message = `Duplicate ${Object.keys(err.keyValue)} Entered`; + err = new ErrorHandler(message, 400); + } + + // Wrong JWT error + if (err.name === "JsonWebTokenError") { + const message = `Json Web Token is invalid, Try again `; + err = new ErrorHandler(message, 400); + } + + // JWT EXPIRE error + if (err.name === "TokenExpiredError") { + const message = `Json Web Token is Expired, Try again `; + err = new ErrorHandler(message, 400); + } + res.status(err.statusCode).json({ + success: false, + //err: err.stack //full location of error + message: err.message, + }) +} diff --git a/models/userModel.js b/models/userModel.js index 85d44bd..7549c82 100644 --- a/models/userModel.js +++ b/models/userModel.js @@ -1,3 +1,5 @@ +import dotenv from 'dotenv' +dotenv.config() import mongoose from "mongoose" import validator from "validator" import bcrypt from "bcryptjs" @@ -9,7 +11,7 @@ const userSchema = new mongoose.Schema({ type: String, required: [true, "Please Enter Your Name"], maxLength: [30, "Name cannot exceed 30 characters"], - minLength: [3, "Name should have more than 4 characters"], + minLength: [4, "Name should have more than 4 characters"], }, email: { type: String, @@ -17,15 +19,40 @@ const userSchema = new mongoose.Schema({ unique: true, validate: [validator.isEmail, "Please Enter a valid Email"], }, + phone: { + type: Number, + required: [true, "Please Enter Your phone no."], + maxLength: [12, "phone cannot exceed 12 characters"], + minLength: [6, "phone should have more than 6 characters"], + }, password: { type: String, required: [true, "Please Enter Your Password"], - minLength: [4, "Password should be greater than 8 characters"], + minLength: [6, "Password should be greater than 6 characters"], select: false,//find not got passpord }, + avatar: { + public_id: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + }, + }, + role: { + type: String, + default: "user", + }, + // createdAt: { + // type: Date, + // default: Date.now, + // }, -}, { timestamps: true } -); + resetPasswordToken: String, + resetPasswordExpire: Date, +}, { timestamps: true }); userSchema.pre("save", async function (next) { if (!this.isModified("password")) { @@ -41,12 +68,30 @@ userSchema.methods.getJWTToken = function () { expiresIn: "1d", }); }; -//process.env.JWT_SECRET +// console.log(process.env.JWT_SECRET) // Compare Password + userSchema.methods.comparePassword = async function (password) { return await bcrypt.compare(password, this.password); }; +// Generating Reset Token +userSchema.methods.getResetPasswordToken = function () { + // Generating Token + const resetToken = crypto.randomBytes(20).toString("hex"); + + // Hashing and adding reset PasswordToken to userSchema + this.resetPasswordToken = crypto + .createHash("sha256") + .update(resetToken) + .digest("hex"); + //expire password time + // console.log(this.resetPasswordToken) + this.resetPasswordExpire = Date.now() + 15 * 60 * 1000;//15 minut + + return resetToken; +}; + const UserModel = mongoose.model("User", userSchema); export default UserModel; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b175dc9..1bdf485 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.5", "multer": "^1.4.5-lts.1", + "nodemailer": "^6.7.5", "validator": "^13.7.0" } }, @@ -1251,6 +1252,14 @@ "node": ">= 0.4.0" } }, + "node_modules/nodemailer": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.5.tgz", + "integrity": "sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2820,6 +2829,11 @@ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "optional": true }, + "nodemailer": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.5.tgz", + "integrity": "sha512-6VtMpwhsrixq1HDYSBBHvW0GwiWawE75dS3oal48VqRhUvKJNnKnJo2RI/bCVQubj1vgrgscMNW4DHaD6xtMCg==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/package.json b/package.json index 510b7a6..7a4fa46 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.5", "multer": "^1.4.5-lts.1", + "nodemailer": "^6.7.5", "validator": "^13.7.0" } } diff --git a/routes/directoryRoute.js b/routes/directoryRoute.js index de7ef49..ceed28d 100644 --- a/routes/directoryRoute.js +++ b/routes/directoryRoute.js @@ -7,7 +7,7 @@ import { getOneDirectory } from "../controllers/directoryController.js" const router = express.Router(); -import { isAuthenticated } from "../middlewares/auth.js" +import { isAuthenticatedUser } from "../middlewares/auth.js" router.route("/directory/create/").post(createDirectory) router.route("/directory/getAll/").get(getAllDirectory) router.route("/directory/getOne/:id").get(getOneDirectory) diff --git a/routes/userRoute.js b/routes/userRoute.js index 5cc5745..bc63b72 100644 --- a/routes/userRoute.js +++ b/routes/userRoute.js @@ -1,31 +1,33 @@ -import express from "express"; -// import isAuthenticated from "../Utils/aurhe"; +import express from "express" import { registerUser, loginUser, logout, - updatePassword + forgotPassword, + resetPassword, + getUserDetails, + updatePassword, + updateProfile, } from "../controllers/userController.js" -// import {isAuthenticatedUser} from "../Middleware/Auth.js"; -import { isAuthenticated } from "../middlewares/auth.js" -import multer from 'multer' +import { isAuthenticatedUser, authorizeRoles } from "../middlewares/auth.js" -const uploaderImage = multer({ - storage: multer.diskStorage({}), - fileFilter: (req, file, cb) => { - let ext = path.extname(file.originalname); - if (ext !== ".jpg" && ext !== ".jpeg" && ext !== ".png") { - cb(new Error("File type not supported!"), false) - return - } - cb(null, true); - } -}); const router = express.Router(); -router.route("/user/register/").post(uploaderImage.single("image"), registerUser) -router.route("/user/login/").post(loginUser) +router.route("/user/register").post(registerUser); + +router.route("/user/login").post(loginUser); + +router.route("/user/password/forgot").post(forgotPassword); + +router.route("/user/password/reset/:token").put(resetPassword); + router.route("/user/logout").get(logout); -router.route("/user/update/password").put(isAuthenticated, updatePassword); + +router.route("/user/details").get(isAuthenticatedUser, getUserDetails); + +router.route("/user/password/update").put(isAuthenticatedUser, updatePassword); + +router.route("/user/update/profile").put(isAuthenticatedUser, updateProfile); + export default router; \ No newline at end of file diff --git a/tmp/tmp-1-1655728922687 b/tmp/tmp-1-1655728922687 new file mode 100644 index 0000000..b9a863b Binary files /dev/null and b/tmp/tmp-1-1655728922687 differ diff --git a/tmp/tmp-1-1655730583107 b/tmp/tmp-1-1655730583107 new file mode 100644 index 0000000..b9a863b Binary files /dev/null and b/tmp/tmp-1-1655730583107 differ diff --git a/tmp/tmp-1-1655730644545 b/tmp/tmp-1-1655730644545 new file mode 100644 index 0000000..b9a863b Binary files /dev/null and b/tmp/tmp-1-1655730644545 differ diff --git a/tmp/tmp-1-1655730940920 b/tmp/tmp-1-1655730940920 new file mode 100644 index 0000000..6fb99c9 Binary files /dev/null and b/tmp/tmp-1-1655730940920 differ