DEV Community

ronyfr3
ronyfr3

Posted on

Express js authentication & authorization code

if you need full code follow my github repository

https://github.com/ronyfr3

server/index.js

//handling uncaught exceptions, if something is undefined/uncaught then this will handled
process.on("uncaughtException", (err) => {
  console.log(
    `server is shutting down due to uncaught exception: ${err.message} ${err.stack}`
  );
});

require("dotenv").config();
const express = require("express");
const cors = require("cors");
const cookieParser = require("cookie-parser");

//app initialization
const app = express();
//require db
const connect = require("./config/db");
connect();

//body-parser
app.use(express.json());
//cookieParser
app.use(cookieParser());

//cors
app.use(cors());

//destructure env object
let { SERVER_DEV_NAME } = process.env;

app.get("/", (req, res) => {
  res
    .status(200)
    .send(`${SERVER_DEV_NAME} is running the server at port: ${PORT}`);
});

// Routes
app.use("/api/user", require("./routes/User"));

let PORT = process.env.PORT || 8080;
const server = app.listen(PORT, () =>
  console.log(`server is running at port ${PORT}`)
);

//unhandled promise rejection handling
process.on("unhandledRejection", (err) => {
  console.log(
    "shutting down server due to unhandled promise rejection. Error: " +
      err.message
  );
  server.close(() => {
    process.exit(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

server/controllers/User.js

const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { google } = require("googleapis");
const fetch = require("node-fetch");

const { OAuth2 } = google.auth;
const sendMail = require("../utils/sendMail");
const User = require("../Model/User");

const client = new OAuth2(process.env.MAILING_SERVICE_CLIENT_ID);
const { CLIENT_URL } = process.env;

const ErrorHandler = require("../utils/errorHandler");
const AsyncErrorHandler = require("../Middleware/catchAsyncError");

const userCtrl = {
  //user/register
  register: AsyncErrorHandler(async (req, res, next) => {
    // console.log(req.body);
    const { name, email, password } = req.body;

    if (!name || !email || !password)
      return next(new ErrorHandler("Please fill in all fields.", 406));

    if (!validateEmail(email))
      return next(new ErrorHandler("Invalid emails.", 406));

    const user = await User.findOne({ email });
    if (user) return next(new ErrorHandler("Email already exists.", 409));

    if (password.length < 6)
      return next(
        new ErrorHandler("Password must be at least 6 characters.", 406)
      );

    //HASHED PASSWORD
    const passwordHash = await bcrypt.hash(password, 12);

    const newUser = await User.create({
      name,
      email,
      password: passwordHash,
    });

    // return new user
    res.status(201).json({
      message: "Registration Successfull",
      newUser,
    });
  }),
  //user/login
  login: AsyncErrorHandler(async (req, res, next) => {
    // console.log(req.body);
    const { email, password } = req.body;

    if (!email || !password)
      return next(new ErrorHandler("Please fill in all fields.", 406));

    const user = await User.findOne({ email });

    if (!user) return next(new ErrorHandler("Email does not exist.", 400));

    //PASSWORD MATCH CHECK
    const isMatch = await bcrypt.compare(password, user.password);

    if (!isMatch) return next(new ErrorHandler("Password is incorrect.", 400));

    const refresh_token = createRefreshToken({ id: user._id });

    //HANDLE REFRESH TOKEN USING COOKIES
    res.cookie("refreshtoken", refresh_token, {
      sameSite: "strict",
      httpOnly: true,
      // secure: true, //only work for production
      path: "/user/refresh_token",
      maxAge: 900000, //3min=180sec=180000 milliseconds
    });
    // Cookies that have not been signed
    // console.log("Cookies: ", req.cookies);

    // Cookies that have been signed
    // console.log("Signed Cookies: ", req.signedCookies);

    res.status(200).json({
      message: "Login Successfull",
      refreshToken: refresh_token,
      data: {
        name: user.name,
        email: user.email,
        isAdmin: user.isAdmin,
      },
    });
  }),
  //user/logout
  logout: AsyncErrorHandler(async (req, res) => {
    res.clearCookie("refreshtoken", { path: "/user/refresh_token" });
    return res.status(200).json({ message: "You are logged out!" });
  }),
  //user/refresh_token
  getAccessToken: AsyncErrorHandler(async (req, res, next) => {
    const rf_token = req.cookies.refreshtoken;

    if (!rf_token) return next(new ErrorHandler("Please login now!", 400));

    jwt.verify(rf_token, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
      if (err) return next(new ErrorHandler("Please login now!", 400));

      const access_token = createAccessToken({ id: user.id });
      res
        .status(200)
        .json({ message: "user is already logged in", access_token });
    });
  }),
  //user/forgot
  forgotPassword: AsyncErrorHandler(async (req, res, next) => {
    const { email } = req.body;
    const user = await User.findOne({ email });
    if (!user) return next(new ErrorHandler("Email does not exist.", 400));

    const access_token = createAccessToken({ id: user._id });
    const url = `${CLIENT_URL}/user/reset/${access_token}`;

    //NODEMAILER SENDING EMAIL
    sendMail(email, url, "Reset your password");

    res.json({
      message: `Re-send the password, please check your email ${access_token}`,
    });
  }),
  //user/reset
  resetPassword: AsyncErrorHandler(async (req, res, next) => {
    const { password } = req.body;
    // console.log(password);
    if (!password)
      return next(new ErrorHandler("please enter your password", 400));

    const passwordHash = await bcrypt.hash(password, 12);

    await User.findOneAndUpdate(
      { _id: req.user.id },
      {
        password: passwordHash,
      }
    );

    res.status(200).json({ msg: "Password successfully changed!" });
  }),
  //user/user_info/:id
  getUser: AsyncErrorHandler(async (req, res, next) => {
    if (!mongoose.Types.ObjectId.isValid(req.params.id)) {
      return next(
        new ErrorHandler(`No user found with id:${req.params.id}`, 404)
      );
    }
    const user = await User.findById(req.params.id).select("-password");
    res.status(200).json(user);
  }),
  //user/all_user get access_token from hitting user/refresh_token paste the token in headers Authorization
  getUsers: AsyncErrorHandler(async (req, res) => {
    const users = await User.find().select("-password");
    res.status(200).json(users);
  }),
  //user/update_user patch req
  updateUser: AsyncErrorHandler(async (req, res) => {
    const { name, avatar, email } = req.body;
    await User.findOneAndUpdate(
      { _id: req.user.id },
      //update can be a single item just put one of them in body and send req
      {
        email,
        name,
        avatar,
      }
    );
    res.status(200).json({ msg: "Update Success!" });
  }),
  //user//update_role/:id
  updateUsersRole: AsyncErrorHandler(async (req, res) => {
    const { isAdmin } = req.body;

    await User.findOneAndUpdate(
      { _id: req.params.id },
      {
        isAdmin,
      }
    );
    res.status(200).json({ msg: "Update Success!" });
  }),
  //user/delete/:id
  deleteUser: AsyncErrorHandler(async (req, res, next) => {
    if (!mongoose.Types.ObjectId.isValid(req.params.id)) {
      return next(
        new ErrorHandler(`No user found with id:${req.params.id}`, 404)
      );
    }
    await User.findByIdAndDelete(req.params.id);
    res.status(200).json({ msg: "Deleted Success!" });
  }),
  //user/google_login
  googleLogin: AsyncErrorHandler(async (req, res, next) => {
    const { tokenId } = req.body;
    // console.log(tokenId);
    if (!tokenId)
      return next(new ErrorHandler("you need to provide a token id", 400));

    const verify = await client.verifyIdToken({
      idToken: tokenId,
      audience:
        "779648521547-gjlsus2l9aud4kosqdtc5gu5icmumqlp.apps.googleusercontent.com", //google client_id
    });
    const { email_verified, email, name, picture } = verify.payload;
    console.log(email); //google pop up selected email
    console.log("email_verified", email_verified); //return true / false

    if (!email_verified)
      return res.status(400).json({ msg: "Email verification failed." });

    //hashed password
    const password = email + process.env.GOOGLE_SECRET;
    const passwordHash = await bcrypt.hash(password, 12);

    const user = await User.findOne({ email });

    if (user) {
      const isMatch = await bcrypt.compare(password, user.password);

      if (!isMatch)
        return res.status(400).json({ msg: "Password is incorrect." });

      const refresh_token = createRefreshToken({ id: user._id });

      res.cookie("refreshtoken", refresh_token, {
        httpOnly: true,
        path: "/user/refresh_token",
        secure: true,
        maxAge: 180000,
      });

      res.status(200).json({
        message: "Login Successfull",
        refreshToken: refresh_token,
        data: {
          name: user.name,
          email: user.email,
          avatar: user.avatar,
          isAdmin: user.isAdmin,
        },
      });
    } else {
      const newUser = new User({
        name,
        email,
        password: passwordHash,
        avatar: picture,
      });

      const userData = await newUser.save();

      const refresh_token = createRefreshToken({ id: newUser._id });

      res.cookie("refreshtoken", refresh_token, {
        httpOnly: true,
        path: "/user/refresh_token",
        secure: true,
        maxAge: 180000,
      });

      res.status(200).json({
        message: "Login Success",
        refreshToken: refresh_token,
        data: {
          name: userData.name,
          email: userData.email,
          avatar: userData.avatar,
          isAdmin: userData.isAdmin,
        },
      });
    }
  }),
  //user/facebook_login
  facebookLogin: AsyncErrorHandler(async (req, res, next) => {
    const { accessToken, userID } = req.body;
    //developers.facebook.com/docs/graph-api/overview/ -->versions curl -i X GET \
    const URL = `https://graph.facebook.com/v2.9/${userID}/?fields=id,name,email,picture&access_token=${accessToken}`;

    const data = await fetch(URL)
      .then((res) => res.json())
      .then((res) => {
        return res;
      });

    const { email, name, picture } = data;

    const password = email + process.env.FACEBOOK_SECRET;

    const passwordHash = await bcrypt.hash(password, 12);

    const user = await User.findOne({ email });

    if (user) {
      const isMatch = await bcrypt.compare(password, user.password);
      if (!isMatch)
        return next(new ErrorHandler("Password is incorrect.", 400));

      const refresh_token = createRefreshToken({ id: user._id });
      res.cookie("refreshtoken", refresh_token, {
        httpOnly: true,
        path: "/user/refresh_token",
        secure: true,
        maxAge: 180000,
      });

      res.status(200).json({
        message: "Login Successfull",
        refreshToken: refresh_token,
        data: {
          name: user.name,
          email: user.email,
          avatar: user.avatar,
          isAdmin: user.isAdmin,
        },
      });
    } else {
      const newUser = new Users({
        name,
        email,
        password: passwordHash,
        avatar: picture.data.url,
      });

      const fbUserData = await newUser.save();

      const refresh_token = createRefreshToken({ id: newUser._id });
      res.cookie("refreshtoken", refresh_token, {
        httpOnly: true,
        path: "/user/refresh_token",
        secure: true,
        maxAge: 180000,
      });

      res.status(200).json({
        message: "Login Success",
        refreshToken: refresh_token,
        data: {
          name: fbUserData.name,
          email: fbUserData.email,
          avatar: fbUserData.avatar,
          isAdmin: fbUserData.isAdmin,
        },
      });
    }
  }),
};

function validateEmail(email) {
  const re =
    /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return re.test(email);
}

const createActivationToken = (payload) => {
  return jwt.sign(payload, process.env.ACTIVATION_TOKEN_SECRET, {
    expiresIn: "3m",
  });
};

const createAccessToken = (payload) => {
  // return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
  return jwt.sign(payload, "123456", {
    expiresIn: "3m",
  });
};

const createRefreshToken = (payload) => {
  //payload = user._id
  // return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
  return jwt.sign(payload, "123456", {
    expiresIn: "3m",
  });
};

module.exports = userCtrl;

Enter fullscreen mode Exit fullscreen mode

server/Model/Users.js

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: [true, "Please enter your name!"],
      trim: true,
    },
    email: {
      type: String,
      required: [true, "Please enter your email!"],
      trim: true,
      unique: true,
    },
    password: {
      type: String,
      required: [true, "Please enter your password!"],
    },
    isAdmin: {
      type: Boolean,
      default: false,
    },
    avatar: {
      type: String,
      default:
        "https://cdn.pixabay.com/photo/2016/08/08/09/17/avatar-1577909_960_720.png",
    },
    totalBuyCost: {
      type: Number,
      default: 0,
    },
  },
  {
    timestamps: true,
  }
);

module.exports = mongoose.model("User", userSchema);

Enter fullscreen mode Exit fullscreen mode

server/routes/Users.js

const router = require('express').Router()
const userCtrl = require('../controllers/User')
const { protect, admin } = require("../middleware/auth");

//for protect routes you need token to attach in headers

router.post('/register', userCtrl.register)
router.post('/login', userCtrl.login)
router.post('/logout', userCtrl.logout)
router.post('/refresh_token', userCtrl.getAccessToken)
router.post('/forgot',protect, userCtrl.forgotPassword)
router.post('/reset', protect, userCtrl.resetPassword)
router.get('/user_info/:id',protect, userCtrl.getUser)
router.get('/all_user', protect, admin, userCtrl.getUsers)
router.patch('/update_user', protect, userCtrl.updateUser)
router.patch('/update_role/:id', protect, admin, userCtrl.updateUsersRole)
router.delete('/delete_user/:id', protect, admin, userCtrl.deleteUser)

// Social Login
router.post('/google_login', userCtrl.googleLogin)
router.post('/facebook_login', userCtrl.facebookLogin)

module.exports = router
Enter fullscreen mode Exit fullscreen mode

server/Middleware/auth

const jwt = require("jsonwebtoken");
const AsyncErrorHandler = require("../Middleware/catchAsyncError");
const User = require("../Model/User");

const protect = AsyncErrorHandler(async (req, res, next) => {
  try {
    const token = req.header("Authorization");
    if (!token) return res.status(401).json({ message: "you are not authorized" });

    const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
    // console.log(`decoded user`,decoded)
    req.user = await User.findById(decoded.id).select("-password");
    // console.log(req.user)
    next();
  } catch (error) {
    return res.status(401).json({message: "you are not authorized", error: error.message });
  }
});

const admin = (req, res, next) => {
  console.log(req.user);
  if (req.user && req.user.isAdmin) {
    next();
  } else {
    return res.status(401).json({message: "Not authorized as an admin"});
  }
};

module.exports = { protect, admin };

Enter fullscreen mode Exit fullscreen mode

server/middleware/catchAsyncError.js

module.exports = (acceptedFunction) => (req,res,next) => {    Promise.resolve(acceptedFunction(req,res,next)).catch(next)
}
Enter fullscreen mode Exit fullscreen mode

server/middleware/error.js

const ErrorHandler = require("../utils/errorHandler");

module.exports = (err, req, res, next) => {
  console.log(`err`, err.message);
  let error = { ...err };
  error.message = err.message;

  //mongodb bad objectId
  if (err.name === "CastError") {
    const message = "Resource not found";
    error = new ErrorHandler(message, 404);
  }
  //mongodb duplicate field error
  if (err.code === 11000) {
    const message = "duplicate field value entered";
    error = new ErrorHandler(message, 404);
  }
  //mongodb validation error
  if (err.name === "ValidationError") {
    const message = Object.values(err.errors).map((value) => value.message);
    error = new ErrorHandler(message, 404);
  }

  res.status(error.statusCode || 500).json({
    success: false,
    message: error.message || "server error",
  });
};

Enter fullscreen mode Exit fullscreen mode

server/config/db.js

const mongoose =require("mongoose");

const connectDB = async () => {
  // Connecting to the database
  const { MONGO_URI } = process.env;
  mongoose
    .connect(MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      // useFindAndModify: false,
    })
    .then((data) => {
      console.log(`mongodb connection established with server: ${data.connection.host}`);
    })
};

module.exports = connectDB;
Enter fullscreen mode Exit fullscreen mode

Discussion (0)