Leave Management system Node, MongoDB, Express
Tags: node, express, mongodb, leavemanagement
Published: 2020-11-21 21:00:24

Node.js is eating the world. Many of the largest companies are building more and more of their backend using Node.js, **NODEJS IS GROWING WE ARE ALSO GROWING**.


This tutorial will take you step-by-step through building a fully functional  Leave Management system. Along the way you’ll learn about Express.js, the most popular web framework, user authentication, locking down routes to enforce login restrictions, and performing CRUD operations with a database (creating, reading, updating, and deleting data). This tutorial uses the following technologies but doesn’t require any prior experience:

**What we want to do.**
We want to build a leave management system where a registered staff of an organization can request leave and the admin(manager or HR) will approve the leave request and an email notification will be sent to the staff. 
**This article will be broken into Modules.**
1:Overview of dependencies, project setup, and server setups.
2:Authentication
3:Authorization
4:Leave request and approval 

**Main Dependencies**
1:Nodejs must be installed
2:MongoDB database
3:Babel
4:sendGrid 

**Overview of dependencies, project setup, and server setups.**
The src folder contains the source file of the project. the purpose of each of the subfolders will be explained subsequently.

Let's get to it.
Create a server.js file in the root folder and copy this into the empty file.

```js
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
const vm = require("v-response");
const morgan = require('morgan');
const mongoose = require("mongoose")
app.use(express.json());
app.use(morgan('dev'));
const database = 'mongodb://localhost:27017/levemanagementdb';

mongoose.connect((database), {
    useUnifiedTopology: true,
    useFindAndModify: false,
    useNewUrlParser: true,
    useCreateIndex: true,
})
    .then(async () => {
        vm.log("connected to database", database);
    })
    .catch(err => vm.log("error mongodb", err));


app.listen(port, () => vm.log("server running on port:", port));

```
The server.js file will start up our Nodejs server to run this you can type this to your terminal 

```node server.js```
If all goes well you should get this in your terminal 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/63bycuw4gzzggcg00h53.png)

We need to create an endpoint whereby users can register to achieve that we need to create an authentication folder in our project and create four files inside the folder (auth.model.js,auth.route.js,auth.controller.js, auth.validation.js) in the auth. model file 

```js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;


const userSchema = new Schema({
    fullname:{
        type: String

    },
    manager: {
        type: Schema.Types.ObjectId,
        ref: "user"
    },
    email: {
        type: String
    },
    password: {
        type: String
    },
    role: {
        type: String,
        enum: ['manager', 'staff'],
        default: 'staff'
    },
    deleted: {
        type: Boolean,
        default: false
    }
}, {timestamps: true});

const Usermodel = mongoose.model('user', userSchema, 'Users');
module.exports = Usermodel

```
auth.validation.js:
```js
'use strict';
const Validator = require("validatorjs");

export const UserValidator = {
    /**
     * @param {Object} obj The validation object
     * @return {Object}
     * */
    validateAccount(obj) {
        const rules = {
            email: 'required|string',
            password: 'required|string',
            fullname: 'required|string',
            manager: 'required|string',

        };
        const validator = new Validator(obj, rules);
        return {
            errors: validator.errors.all(),
            passed: validator.passes(),
        }
    },
};
```
auth.controller.js

```js
const UserValidator = require("./auth.validator");
const _ = require("underscore");

const User = require("./auth.model");
const vm = require("v-response");

/**
 * @controller User authentication controller
 * */
/**
 * @param
 * */

//CREATE ACCOUNT
exports.CreateAccount = async (req, res, next) => {
    try {
        let obj = req.body;
        const validateUserInput = await UserValidator.validateAccount(obj);
        if (!validateUserInput.passed) {
            return res.status(400)
                .json({
                    status: false,
                    code: 400,
                    message: "There's error in your inputs",
                    errors: validateUserInput.errors,
                })
        }
        const checkUserEmail = await User.findOne({email: req.body.email})
        if (checkUserEmail) {
            return res.status(409)
                .json(vm.ApiResponse(false, 409, 'email already exist'))
        } else if (!checkUserEmail) {

            _.extend(obj, {
                password: await vm.hashedPassword(obj.password, 10),
            });
            const account_object = await new User(obj);
            const saveAccount = await account_object.save();
            if (!saveAccount) {
                return res.status(400)
                    .json(vm.ApiResponse(false, 400, "Oops! an error occurr"))
            } else {
                saveAccount.password = null;
                return res.status(201)
                    .json(vm.ApiResponse(true, 200, `account created`, account_object));
            }
        }
    } catch (e) {
        return next(e);
    }

}


//LOGIN
exports.login = async (req, res, next) => {
    try {
        const checkEmail = await User.findOne({email: req.body.email});
        if (!checkEmail) {
            return res.status(400)
                .json(vm.ApiResponse(false, 400, 'email not found'))
        } else {
            const compareEmail = vm.comparepassword(checkEmail.password, req.body.password);
            if (compareEmail) {
                const signtoken = vm.signToken(checkEmail._id, 'yourSecret');
                checkEmail.password = null;
                return res.status(200)
                    .json(vm.ApiResponse(true, 200, "login sucessfull", {user: checkEmail, token: signtoken}))
            }
        }
    } catch (e) {
        return next(e);
    }

};

//list manager so when staff needs to register they can select their managers 
exports.listManagers = async (req, res, next) => {
    try {

        const findMangers = await User.find({role: "manager"})
        if (findMangers) {
            return res.json(findMangers);
        } else if (findMangers.length === 0) {
            return res.json('0 managers')
        } else {
            return res.json("Oops! an error occurr")
        }
    } catch (e) {
        return next(e);
    }
}

```
auth.route.js

```js
'use strict';
const {Router} = require('express');
const UserController = require("./auth.controller");

const router = Router();
router.post("/create/account", UserController.CreateAccount);
router.post("/account/login", UserController.login);
router.get("/managers/list", UserController.listManagers);

module.exports = router;

```

So We are done with the first module which enables users to register and login into their account so in the next module we will be working on sending a leave request as a staff and notifying the manager of the leave request via email and also notify the staff when the manager approves or reject leave request for the email notification we will be using SendGrid you can create an account https://signup.sendgrid.com/ 

Create a Mailer.js file 
```js
'use strict';
const Helper = require("sendgrid").mail;
const sg = require('sendgrid')('yourkey');
const vm = require("v-response");
module.exports.sendMail = sendMail;
function sendMail(from, to, subject, content, template) {
    let fromEmail = new Helper.Email(from);
    let toEmail = new Helper.Email(to);
    let emailContent = new Helper.Content("text/html", content);
    let mail = new Helper.Mail(fromEmail, subject, toEmail, emailContent);

    let isEmailSent = false;
    let request = sg.emptyRequest({
        method: 'POST',
        path: '/v3/mail/send',
        body: mail.toJSON()
    });

    sg.API(request, function (err, response) {

        if (err) {
            vm.log("error");
            vm.log("err in sendgrid: ", err);
            isEmailSent = false;
        }
        vm.log("sendgrid body:", response.statusCode);
        isEmailSent = true;
    });
    return isEmailSent;

}

```

Create a leave folder and create three files(leave.model.js,leave.route.js,leave.controller.js)

Leave.model.js
```js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const leaveSchema = new Schema({
    reason: {
        type: String
    },
    staff: {
        type: Schema.Types.ObjectId,
        ref: "user"
    },
    leave_status: {
        type: String,
        enum: ['pending', 'approved', 'rejected'],
        default: "pending"
    }

}, {timestamps: true})


const leaveModel = mongoose.model("leave", leaveSchema, "Leave");
module.exports = leaveModel;

```

Leave.controller.js

```js
const {sendMail} = require("../util/mailer");

const LeaveModel = require("./leave.model");
const User = require("../authentication/auth.model");
const _ = require("underscore");
const vm = require("v-response");

//request  leave
exports.requestLeave = async (req, res, next) => {
    try {
        const find_user = await User.findById(req.body.staff);
        if (!find_user) {
            return res.status(400)
                .json(vm.ApiResponse(false, 400, 'Invalid user details or unverified account'))
        } else {
            const leaverequestBody = _.extend(req.body, {staff: find_user._id, reason: req.body.reason})
            const createLeaveRequest = await new LeaveModel(leaverequestBody);
            await createLeaveRequest.save();
            const find_manager = await User.findOne({_id: find_user.manager});
            //notify staff manager about leave request
            await sendMail('noreply@leavemanagement', find_manager.email, 'Leave Request', `${find_user.fullname} is requesting for leave`);
            return res.status(200)
                .json(vm.ApiResponse(true, 200, "leave request sent"))
        }
    } catch (e) {
        return next(e);
    }

};

exports.approveLeaveOrRejectLeave = async (req, res, next) => {
    try {
        const findLeave = await LeaveModel.findById(req.query.leave_id);
        const findstaff = await User.findById(findLeave.staff);
        if (!findLeave) {
            return res.status(404)
                .json(vm.ApiResponse(false, 400, 'Leave request not found'))
        } else if (findLeave) {
            if (req.body.approvalstatus === 'approved') {
                await sendMail('noreply@leavemanagement', findstaff.email, 'Leave Approval', `Hello ${findstaff.fullname},your leave request has been approved`);
            } else {
                await sendMail('noreply@leavemanagement', findstaff.email, 'Leave Approval', `Hello ${findstaff.fullname},your leave request has been rejected `);
            }
            findLeave.leave_status = req.body.approvalstatus;
            await findLeave.save();
            return res.status(200)
                .json(vm.ApiResponse(true, 200, "leave request status updated successfully"))
        }
    } catch (e) {
        return next(e);
    }

};
```
Leave.route.js

```js
'use strict';
const {ManagerChecker} = require("../util/RoleChecker");

const {Router} = require('express');
const LeaveController = require("./leave.controller");

const router = Router();
router.post("/request/leave", LeaveController.requestLeave);
//the ManagerChecker ensures that only a manager can approve or reject a leave request 
router.patch("/update/leave/status", ManagerChecker, LeaveController.approveLeaveOrRejectLeave);

module.exports = router;

```

Create a MangerCheck.js file 

```js
const User = require("../authentication/auth.model");
const jwt = require("jsonwebtoken")

exports.ManagerChecker = async (req, res, next) => {
    let token = req.headers.authorization;
    if (!token) {
        return res.status(400)
            .json("please login to continue")
    }
    if (token !== undefined) {
        let decodeToken = jwt.decode(token);
        let id = decodeToken.id;
        if (id) {
            let user = await User.findById(id);
            if (user && user.role !== 'manager') {
                return res.status(403).json({
                    status: false,
                    code: 403,
                    message: 'You are not authorized to do this action'
                })
            } else {
                return next();
            }
        }
    }
    return next();
}

```

And lastly, let's seed a manager account so we can test all our hard work if you've made it this far "Ooin you're doing well"

CREATE A seeder.js file 
```js
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
mongoose.Promise = require('bluebird');
const db = 'mongodb://localhost:27017/levemanagementdb';

mongoose.connect(db);
const User = require("../authentication/auth.model");

async function seedUser() {
    const hash = await bcrypt.hash('password123', 10);
    User.create({
        email: "youremail@gmail.com",
        password: hash,
        fullname: "staff manager",
        role: 'manager',
    }).then(user => {
        console.log(`${user} user created`);
    }).catch((err) => {
        console.log(err);
    }).finally(() => {
        mongoose.connection.close();

    })
}
seedUser();

```
To use the seeder file add this to the script in your package.json file 

```    
"seed": "node util/seeder.js"
```
then run 
```
npm run seed

```
if all goes well you should see this in your terminal

```
 user created
```

then start your Nodejs server 

See API Documentation:https://www.getpostman.com/collections/02507f8d63e1342d42f6

See Github Repository:
https://github.com/waletayo/LeaveManagement-System-Node---devto/tree/main



You can also add other features to this simple leave management Restapi or do let me know in the comment section what you look out for next 

Thank you Guys!!!