├── .babelrc ├── .codeclimate.yml ├── .coveralls.yml ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── Procfile ├── README.md ├── Task Force application challenges.pdf ├── jest.config.js ├── jest.setup.js ├── package-lock.json ├── package.json └── src ├── app.js ├── config └── database.js ├── controllers ├── employees.js └── managers.js ├── middlewares ├── async.js ├── auth.js ├── errorHandler.js └── index.js ├── models ├── employees.js └── managers.js ├── routes ├── employees.js ├── index.js └── managers.js ├── tests ├── employee.test.js ├── errorResponse.test.js ├── excel.test.js ├── mail.test.js └── password.test.js └── utils ├── errorResponse.js ├── excel.js ├── index.js ├── logger.js ├── mail.js ├── password.js └── token.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "debug": false, 7 | "targets": { 8 | "node": "current" 9 | } 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | exclude_patterns: 3 | - 'node_modules' 4 | - 'src/tests' 5 | checks: 6 | argument-count: 7 | enabled: false 8 | complex-logic: 9 | enabled: false 10 | file-lines: 11 | enabled: false 12 | method-complexity: 13 | enabled: false 14 | method-count: 15 | enabled: false 16 | method-lines: 17 | enabled: false 18 | nested-control-flow: 19 | enabled: false 20 | return-statements: 21 | enabled: false 22 | similar-code: 23 | enabled: false 24 | identical-code: 25 | enabled: false 26 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: 6xIP8094OSFYPJT8DDLDyZ3Un6cuYFhCG 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=4000 2 | NODE_ENV=development 3 | POSTGRES_URI=postgres://:@localhost:5432/ 4 | MAIL=@gmail.com 5 | MAIL_PASS= 6 | JWT_CONFIRMATION_SECRET= 7 | JWT_LOGIN_SECRET= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/tests -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2020: true, 4 | node: true, 5 | }, 6 | extends: ['airbnb-base', 'prettier'], 7 | plugins: ['prettier'], 8 | parserOptions: { 9 | ecmaVersion: 11, 10 | sourceType: 'module', 11 | }, 12 | globals: { 13 | AnalysisView: true, 14 | PollingView: true, 15 | Prism: true, 16 | Spinner: true, 17 | Timer: true, 18 | moment: true, 19 | }, 20 | rules: { 21 | 'no-console': 'off', 22 | 'import/prefer-default-export': 'off', 23 | 'brace-style': [2, '1tbs', { allowSingleLine: true }], 24 | 'comma-style': [ 25 | 2, 26 | 'first', 27 | { exceptions: { ArrayExpression: true, ObjectExpression: true } }, 28 | ], 29 | complexity: [2, 6], 30 | curly: 2, 31 | eqeqeq: [2, 'allow-null'], 32 | 'max-statements': [2, 30], 33 | 'no-shadow-restricted-names': 2, 34 | 'no-undef': 2, 35 | 'no-use-before-define': 2, 36 | radix: 2, 37 | semi: 2, 38 | 'space-infix-ops': 2, 39 | strict: 0, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What does this PR do? 2 | #### Description of Task to be completed? 3 | #### How should this be manually tested? 4 | #### Any background context you want to provide? 5 | #### What are the relevant pivotal tracker stories? 6 | #### What are the relevant Github Issues? 7 | #### Screenshots (if appropriate) 8 | #### Questions: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "endOfLine": "lf", 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 'stable' 7 | 8 | services: 9 | - postgresql 10 | 11 | before_script: 12 | - psql -c 'CREATE DATABASE igihe2;' -U postgres 13 | - psql -c 'CREATE TABLE employees (uuid uuid primary key not null, name varchar(255) not null,email varchar(100) not null unique,status varchar(24) not null,nid varchar(255) not null unique,phone varchar(100) not null unique,position varchar(200) not null,birthday date not null,"createdAt" timestamp not null,"updatedAt" timestamp not null);' -U postgres 14 | - psql -c "CREATE TABLE managers (uuid uuid primary key not null, name varchar(255) not null,email varchar(100) not null unique,password varchar(255) not null,confirmed bool default 'f',status varchar(24) not null,nid varchar(255) not null unique,phone varchar(100) not null unique,position varchar(200) not null,birthday date not null,"createdAt" timestamp not null,"updatedAt" timestamp not null);" -U postgres 15 | 16 | env: 17 | - DATABASE_URL: postgres://localhost:5432/igihe2 18 | 19 | cache: 20 | npm: false 21 | 22 | notifications: 23 | email: false 24 | 25 | script: 26 | - npm ci 27 | - npm run coverage 28 | 29 | after_success: 30 | - npm run coveralls 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RedJanvier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Employee-management-API 2 | 3 | [![Build Status](https://travis-ci.org/RedJanvier/Employee-management-API.svg?branch=develop)](https://travis-ci.org/RedJanvier/Employee-management-API) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/19cd4a6e5a087888aa96/maintainability)](https://codeclimate.com/github/RedJanvier/Employee-management-API/maintainability) 5 | [![Coverage Status](https://coveralls.io/repos/github/RedJanvier/Employee-management-API/badge.svg?branch=develop)](https://coveralls.io/github/RedJanvier/Employee-management-API?branch=develop) 6 | 7 | A REST API to manage your employees easily and with bulk add employees and specific employee tracking. 8 | 9 | ## Getting Started 10 | 11 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on your own live system. 12 | 13 | ### Prerequisites 14 | 15 | You need to have: 16 | 17 | - NodeJs Runtime if not [download it here](https://nodejs.org/en/) 18 | - PostgreSQL Database if not [download it here](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads) 19 | - Git Version Control if not [download it here](https://git-scm.com/downloads) 20 | 21 | To check The Prerequisites are installed you can use these terminal commands: 22 | 23 | For NodeJs: `node --version` 24 | 25 | For Postgres: `psql --version` 26 | 27 | For Git: `git --version` 28 | 29 | ### Installing 30 | 31 | A step by step series of examples that tell you how to get a development env running 32 | 33 | Clone the Repo with the terminal command: 34 | 35 | ```bash 36 | git clone https://github.com/RedJanvier/Employee-management-API.git 37 | ``` 38 | 39 | then make a file called .env using sample.env by replacing with your own data. 40 | 41 | Run the command inside the cloned directory: 42 | 43 | ```bash 44 | npm install 45 | ``` 46 | 47 | To start the app in development run the command: 48 | 49 | ```bash 50 | npm run dev 51 | ``` 52 | 53 | ## Built With 54 | 55 | - [NodeJS](https://nodejs.org/en/) - The javascript runtime used 56 | - [ExpressJS](http://expressjs.com//) - The web framework used 57 | - [NPM](http://npmjs.com/) - Dependency Management 58 | - [PostgreSQL](https://www.postgres.org/) - Database system used 59 | - [Sequelize](http://sequelize.org/) - Database management system (DBMS) used 60 | - [NodeMailer](https://nodemailer.com/about/) - Email client system used 61 | 62 | ## Author 63 | 64 | - **RedJanvier** - _uzakuraHub_ - [RedJanvier](https://redjanvier.uzakurahub.xyzz) 65 | 66 | See also the list of [contributors](https://github.com/RedJanvier/Employee-management-API.git/contributors) who participated in this project. 67 | 68 | ## License 69 | 70 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 71 | 72 | ### Documentation 73 | 74 | The documentation of full endpoints and all the requirements can be found at the root endpoint of the API or [here](https://documenter.getpostman.com/view/8357211/SzYW2euW?version=latest) 75 | -------------------------------------------------------------------------------- /Task Force application challenges.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedJanvier/Employee-management-API/3bd3b442ba0145f8b7102becd09f5dabcc828f9b/Task Force application challenges.pdf -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // setupTestFrameworkScriptFile has been deprecated in 3 | // favor of setupFilesAfterEnv in jest 24 4 | setupFilesAfterEnv: ['./jest.setup.js'], 5 | }; 6 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(5000); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "employee-management-rest-api", 3 | "version": "1.0.0", 4 | "description": "Task Force challenge number 3", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "mocha ./src/tests/**.test.js --require @babel/register --exit", 8 | "test:jest": "jest --runInBand --detectOpenHandles --forceExit", 9 | "coverage": "nyc --reporter=text --reporter=html npm run test", 10 | "coverage:jest": "npm run test -- --coverage", 11 | "dev": "cross-env NODE_ENV=development nodemon --exec babel-node src/app", 12 | "start": "babel-node src/app", 13 | "coveralls:jest": "jest --coverage && coveralls < coverage/lcov.info", 14 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/RedJanvier/Task-Force---Employee-management-REST-API-Back-End-3.git" 19 | }, 20 | "keywords": [ 21 | "employee" 22 | ], 23 | "author": "RedJanvier", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/RedJanvier/Task-Force---Employee-management-REST-API-Back-End-3/issues" 27 | }, 28 | "homepage": "https://github.com/RedJanvier/Task-Force---Employee-management-REST-API-Back-End-3#readme", 29 | "dependencies": { 30 | "@babel/register": "^7.11.5", 31 | "bcrypt": "^5.0.0", 32 | "colors": "^1.4.0", 33 | "cross-env": "^7.0.2", 34 | "dotenv": "^8.2.0", 35 | "express": "^4.17.1", 36 | "express-fileupload": "^1.1.9", 37 | "helmet": "^3.23.3", 38 | "jest": "^26.1.0", 39 | "jsonwebtoken": "^8.5.1", 40 | "morgan": "^1.10.0", 41 | "nodemailer": "^6.4.2", 42 | "pg": "^7.17.1", 43 | "pg-hstore": "^2.3.3", 44 | "sequelize": "^5.21.3", 45 | "uuid": "^8.2.0", 46 | "xlsx": "^0.15.6" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.10.2", 50 | "@babel/node": "^7.10.1", 51 | "@babel/preset-env": "^7.10.2", 52 | "@types/uuid": "^8.0.0", 53 | "chai": "^4.2.0", 54 | "coveralls": "^3.1.0", 55 | "eslint": "^7.4.0", 56 | "eslint-config-airbnb-base": "^14.2.0", 57 | "eslint-config-prettier": "^6.11.0", 58 | "eslint-plugin-import": "^2.22.0", 59 | "eslint-plugin-prettier": "^3.1.4", 60 | "mocha": "^8.1.3", 61 | "nodemon": "^2.0.2", 62 | "nyc": "^15.1.0", 63 | "prettier": "^2.0.5", 64 | "supertest": "^4.0.2" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import 'colors'; 2 | import helmet from 'helmet'; 3 | import morgan from 'morgan'; 4 | import { config } from 'dotenv'; 5 | import express, { json } from 'express'; 6 | import fileUpload from 'express-fileupload'; 7 | 8 | import routes from './routes'; 9 | import { errorHandler } from './middlewares'; 10 | 11 | config(); 12 | const app = express(); 13 | const { PORT } = process.env; 14 | 15 | app.use(json()); 16 | app.use(helmet()); 17 | app.use(fileUpload()); 18 | app.use(morgan('dev')); 19 | 20 | app.use('/api/v1', routes); 21 | app.use(errorHandler); 22 | 23 | const server = app.listen( 24 | PORT, 25 | console.log(`Server started at http://localhost:${PORT}/api/v1/`) 26 | ); 27 | 28 | export { app, server }; 29 | -------------------------------------------------------------------------------- /src/config/database.js: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import Sequelize from 'sequelize'; 3 | 4 | config(); 5 | const { DATABASE_URL } = process.env; 6 | export const conn = new Sequelize(DATABASE_URL, { 7 | dialect: 'postgres', 8 | protocol: 'postgres', 9 | dialectOptions: { 10 | ssl: false, 11 | }, 12 | }); 13 | 14 | export const testConnection = () => { 15 | Sequelize.authenticate() 16 | .then(console.log('Connection to the database was successful.')) 17 | .catch((err) => { 18 | console.error('Unable to connect to the database:', err); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/controllers/employees.js: -------------------------------------------------------------------------------- 1 | import { QueryTypes, Op } from 'sequelize'; 2 | 3 | import { 4 | sendEmail, 5 | managerLog, 6 | uploadXL, 7 | readXL, 8 | ErrorResponse, 9 | } from '../utils'; 10 | import { conn as db } from '../config/database'; 11 | import { asyncHandler } from '../middlewares'; 12 | import Employee from '../models/employees'; 13 | 14 | // @desc Create an employee 15 | // Route POST /api/v1/employees 16 | // Access Private 17 | export const create = asyncHandler(async (req, res) => { 18 | const employee = await Employee.create(req.body); 19 | await sendEmail('communication', employee.email); 20 | managerLog('create', { 21 | manager: req.decoded.name, 22 | employee: employee.name, 23 | }); 24 | res.status(201).json({ 25 | success: true, 26 | message: `Employee ${employee.name} successfully created`, 27 | data: employee, 28 | }); 29 | }); 30 | 31 | // @desc Create many employees from excelsheet 32 | // Route POST /api/v1/employees/many 33 | // Access Private 34 | export const createMany = asyncHandler(async (req, res) => { 35 | await uploadXL(req); 36 | setTimeout(() => { 37 | const employeesList = readXL(); 38 | employeesList.map( 39 | asyncHandler(async (data) => { 40 | const employee = await Employee.create(data); 41 | await sendEmail('communication', employee.email); 42 | managerLog('create', { 43 | manager: req.decoded.name, 44 | employee: employee.name, 45 | }); 46 | return employee; 47 | }) 48 | ); 49 | res.status(200).json('Successfully stored employees list'); 50 | }, 100); 51 | }); 52 | 53 | // @desc Delete an employee 54 | // Route DELETE /api/v1/employees/:uuid 55 | // Access Private 56 | export const deleteEmployee = asyncHandler(async (req, res) => { 57 | const { uuid } = req.params; 58 | const employee = await Employee.destroy({ where: { uuid } }); 59 | if (!employee) throw new ErrorResponse('Employee not found', 400); 60 | 61 | managerLog('delete', { 62 | manager: req.decoded.name, 63 | employee: uuid, 64 | }); 65 | 66 | res.status(200).json({ 67 | success: true, 68 | message: `${employee} Employees successfully deleted`, 69 | }); 70 | }); 71 | 72 | // @desc Edit an employee 73 | // Route PUT /api/v1/employees/:id 74 | // Access Private 75 | export const edit = asyncHandler(async (req, res) => { 76 | const { uuid } = req.params; 77 | 78 | const [employee] = await Employee.update( 79 | { ...req.body, updatedAt: new Date() }, 80 | { where: { uuid } } 81 | ); 82 | 83 | if (!employee) throw new ErrorResponse('Employee not modified', 400); 84 | managerLog('edit', { 85 | manager: req.decoded.name, 86 | employee: uuid, 87 | }); 88 | res.status(200).json({ 89 | success: true, 90 | message: `Employee successfully modified`, 91 | }); 92 | }); 93 | 94 | // @desc Suspend/Activate an employee 95 | // Route PUT /api/v1/employees/:id/:status 96 | // Access Private 97 | export const changeStatus = asyncHandler(async (req, res) => { 98 | const { uuid } = req.params; 99 | let { status } = req.params; 100 | if (status === 'activate' || status === 'suspend') { 101 | await db.query( 102 | `UPDATE employees SET status = :status, "updatedAt" = :updatedAt WHERE employees.uuid = :uuid`, 103 | { 104 | replacements: { 105 | status: status === 'activate' ? 'active' : 'inactive', 106 | uuid, 107 | updatedAt: new Date(), 108 | }, 109 | type: QueryTypes.UPDATE, 110 | } 111 | ); 112 | status = status === 'suspend' ? 'suspende' : status; 113 | managerLog('status', { 114 | manager: req.decoded.name, 115 | status, 116 | employee: uuid, 117 | }); 118 | res.status(201).json({ 119 | success: true, 120 | message: `Employee was ${status}d successfully`, 121 | }); 122 | } else { 123 | throw new ErrorResponse('Route does not exist', 404); 124 | } 125 | }); 126 | 127 | // @desc Search for employees 128 | // Route PUT /api/v1/employees/search 129 | // Access Private 130 | export const search = asyncHandler(async (req, res) => { 131 | const { page, pageSize } = req.query; 132 | const offset = (page - 1) * pageSize; 133 | const limit = pageSize; 134 | 135 | const where = {}; 136 | const { query } = req; 137 | delete query.page; 138 | delete query.pageSize; 139 | 140 | Object.keys(query).map((q) => { 141 | where[q] = { 142 | [Op.iLike]: `%${req.query[q]}%`, 143 | }; 144 | return true; 145 | }); 146 | 147 | const employees = await Employee.findAll({ offset, limit, where }); 148 | const all = await Employee.findAll({ where }); 149 | managerLog('search', { 150 | manager: req.decoded.name, 151 | }); 152 | res.status(200).json({ 153 | success: true, 154 | found: all.length, 155 | data: employees.map(({ dataValues }) => ({ 156 | ...dataValues, 157 | password: null, 158 | })), 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /src/controllers/managers.js: -------------------------------------------------------------------------------- 1 | import { asyncHandler } from '../middlewares'; 2 | import Manager from '../models/managers'; 3 | import { 4 | encryptPassword, 5 | sendEmail, 6 | managerLog, 7 | verifyToken, 8 | decryptPassword, 9 | signToken, 10 | ErrorResponse 11 | } from '../utils'; 12 | 13 | // @desc Create a manager 14 | // Route POST /api/v1/managers/signup 15 | // Access Public 16 | export const create = asyncHandler(async (req, res) => { 17 | const manager = await Manager.create({ 18 | ...req.body, 19 | password: await encryptPassword(req.body.password) 20 | }); 21 | await sendEmail('confirmation', manager.dataValues.email); 22 | managerLog('create', { 23 | manager: req.decoded.name, 24 | employee: manager.dataValues.name 25 | }); 26 | res.status(201).json({ 27 | success: true, 28 | message: `Please check your inbox to confirm your email!`, 29 | data: manager 30 | }); 31 | }); 32 | 33 | // @desc Confirm a manager from email 34 | // Route GET /api/v1/managers/confirm/:confirmationToken 35 | // Access Private 36 | export const confirm = asyncHandler(async (req, res) => { 37 | const { confirmationToken } = req.params; 38 | const email = verifyToken( 39 | confirmationToken, 40 | process.env.JWT_CONFIRMATION_SECRET 41 | ); 42 | 43 | const [manager] = await Manager.update( 44 | { confirmed: true }, 45 | { where: { email } } 46 | ); 47 | if (!manager) throw new ErrorResponse(`Manager's email not confirmed`, 400); 48 | await sendEmail('communication', email); 49 | 50 | res.status(200).json({ 51 | success: true, 52 | message: `Thank you for confirming your email!` 53 | }); 54 | }); 55 | 56 | // @desc Login a manager 57 | // Route POST /api/v1/managers/login 58 | // Access Public 59 | export const login = asyncHandler(async (req, res) => { 60 | const { email, password } = req.body; 61 | 62 | const { dataValues } = await Manager.findOne({ where: { email } }); 63 | if (!dataValues) throw new ErrorResponse(`Manager doesn't exist!`, 404); 64 | const { confirmed, name, uuid, status } = dataValues; 65 | if (!confirmed) { 66 | throw new ErrorResponse(`Please verify your email first`, 400); 67 | } 68 | 69 | await decryptPassword(password, dataValues.password); 70 | const token = signToken( 71 | { name, uuid, status, email: dataValues.email }, 72 | process.env.JWT_LOGIN_SECRET, 73 | '1h' 74 | ); 75 | managerLog('login', { manager: name }); 76 | 77 | res.status(200).json({ 78 | success: true, 79 | data: token 80 | }); 81 | }); 82 | 83 | // @desc Request to reset a manager's Password 84 | // Route POST /api/v1/managers/reset 85 | // Access Public 86 | export const requestReset = asyncHandler(async (req, res) => { 87 | const { 88 | dataValues: { email } 89 | } = await Manager.findOne({ where: { email: req.body.email } }); 90 | if (!email) throw new ErrorResponse(`Manager does not exist!`, 404); 91 | await sendEmail('password reset', email); 92 | 93 | res.status(201).json({ 94 | success: true, 95 | message: `Please check your inbox to reset your password!` 96 | }); 97 | }); 98 | 99 | // @desc Confirm a manager's password reset 100 | // Route POST /api/v1/managers/reset/:token 101 | // Access Private 102 | export const confirmReset = asyncHandler(async (req, res) => { 103 | const password = await encryptPassword(req.body.password); 104 | const email = verifyToken( 105 | req.params.token, 106 | process.env.JWT_CONFIRMATION_SECRET 107 | ); 108 | 109 | const [manager] = await Manager.update({ password }, { where: { email } }); 110 | if (!manager) throw new ErrorResponse(`Manager's password not reset`, 404); 111 | managerLog('reset', { 112 | manager: req.decoded.name 113 | }); 114 | 115 | res.status(200).json({ 116 | success: true, 117 | message: `Thank you! You can now use your new password to login!` 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/middlewares/async.js: -------------------------------------------------------------------------------- 1 | export default (func) => (req, res, next) => 2 | Promise.resolve(func(req, res, next)).catch(next); 3 | -------------------------------------------------------------------------------- /src/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | import { verifyToken, ErrorResponse } from '../utils'; 2 | import asyncHandler from './async'; 3 | 4 | // eslint-disable-next-line 5 | export const checkAuth = asyncHandler(async (req, res, next) => { 6 | const token = req.headers.authorization.startsWith('Bearer') 7 | ? req.headers.authorization.split(' ')[1] 8 | : req.headers.authorization; 9 | 10 | switch (true) { 11 | case token === undefined: 12 | throw new ErrorResponse( 13 | 'unauthorised to use this resource, please signup/login', 14 | 401 15 | ); 16 | 17 | case token !== null: 18 | req.decoded = verifyToken(token, process.env.JWT_LOGIN_SECRET); 19 | return next(); 20 | 21 | default: 22 | break; 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/middlewares/errorHandler.js: -------------------------------------------------------------------------------- 1 | const errorHandler = (err, req, res, next) => { 2 | const error = { ...err, message: err.message }; 3 | 4 | console.log('Error:'.red.bold, error); 5 | 6 | if (error.message === 'jwt expired') { 7 | error.statusCode = 401; 8 | error.message = 'please signup/login first!'; 9 | } 10 | if (error.message === `Cannot read property 'startsWith' of undefined`) { 11 | error.statusCode = 401; 12 | error.message = 'make sure you provide a login token'; 13 | } 14 | if (error.message === `jwt malformed`) { 15 | error.statusCode = 401; 16 | error.message = 'make sure you provide a valid login token'; 17 | } 18 | res 19 | .status(error.statusCode || 500) 20 | .json({ success: false, error: error.message || 'Server Error' }); 21 | next(); 22 | }; 23 | 24 | export default errorHandler; 25 | -------------------------------------------------------------------------------- /src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | import { checkAuth as auth } from './auth'; 2 | import errorHandler from './errorHandler'; 3 | import asyncHandler from './async'; 4 | 5 | export { auth, errorHandler, asyncHandler }; 6 | -------------------------------------------------------------------------------- /src/models/employees.js: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes } from 'sequelize'; 2 | import { conn as db } from '../config/database'; 3 | import { checkAge } from '../utils'; 4 | 5 | const Employee = db.define('employee', { 6 | uuid: { 7 | type: DataTypes.UUID, 8 | defaultValue: Sequelize.UUIDV4, 9 | primaryKey: true 10 | }, 11 | name: { 12 | type: DataTypes.STRING, 13 | allowNull: false, 14 | validate: { 15 | notNull: { 16 | args: true, 17 | msg: 'Name must be provided' 18 | } 19 | } 20 | }, 21 | email: { 22 | type: DataTypes.STRING, 23 | unique: { 24 | args: true, 25 | msg: 'Email already exists' 26 | }, 27 | allowNull: false, 28 | validate: { 29 | isEmail: { 30 | args: true, 31 | msg: 'Email is not a valid email' 32 | } 33 | } 34 | }, 35 | status: { 36 | type: DataTypes.STRING, 37 | allowNull: false, 38 | validate: { 39 | isIn: { 40 | args: [['active', 'inactive']], 41 | msg: 'Must be Active or Inactive' 42 | } 43 | } 44 | }, 45 | nid: { 46 | type: DataTypes.BIGINT, 47 | allowNull: false, 48 | unique: true, 49 | validate: { 50 | len: { 51 | args: [16, 16], 52 | msg: 'Nid must be a valid Rwandan National ID' 53 | } 54 | } 55 | }, 56 | phone: { 57 | type: DataTypes.STRING, 58 | allowNull: false, 59 | unique: true, 60 | validate: { 61 | is: { 62 | args: /^(25)?0?7[3 2 8]{1}[0-9]{7}$/, 63 | msg: 'Phone number must be a valid Rwandan Phone' 64 | } 65 | } 66 | }, 67 | position: { 68 | type: DataTypes.STRING, 69 | allowNull: false, 70 | validate: { 71 | isIn: { 72 | args: [['developer', 'designer']], 73 | msg: 'Position must be a developer or designer' 74 | } 75 | } 76 | }, 77 | birthday: { 78 | type: DataTypes.DATE, 79 | allowNull: false, 80 | validate: { 81 | isDate: { 82 | args: true, 83 | msg: 'Birthday must be a valid date (timestamp)' 84 | }, 85 | ageRestriction() { 86 | if (this.birthday) { 87 | if (checkAge(this.birthday) < 18) { 88 | throw new Error('Employee must be atleast 18 yrs old.'); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | }); 95 | 96 | export default Employee; 97 | -------------------------------------------------------------------------------- /src/models/managers.js: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes } from 'sequelize'; 2 | 3 | import { conn as db } from '../config/database'; 4 | import { checkAge } from '../utils'; 5 | 6 | const Manager = db.define('manager', { 7 | uuid: { 8 | type: DataTypes.UUID, 9 | defaultValue: Sequelize.UUIDV4, 10 | primaryKey: true 11 | }, 12 | name: { 13 | type: DataTypes.STRING, 14 | allowNull: false, 15 | validate: { 16 | notNull: { 17 | args: true, 18 | msg: 'Name must be provided' 19 | } 20 | } 21 | }, 22 | email: { 23 | type: DataTypes.STRING, 24 | unique: { 25 | args: true, 26 | msg: 'Email already exists' 27 | }, 28 | allowNull: false, 29 | validate: { 30 | isEmail: { 31 | args: true, 32 | msg: 'Email is not a valid email' 33 | } 34 | } 35 | }, 36 | password: { 37 | type: DataTypes.STRING, 38 | allowNull: false, 39 | validate: { 40 | notNull: { 41 | args: true, 42 | msg: 'Password must be provided' 43 | } 44 | } 45 | }, 46 | status: { 47 | type: DataTypes.STRING, 48 | allowNull: false, 49 | validate: { 50 | isIn: { 51 | args: [['active', 'inactive']], 52 | msg: 'Must be Active or Inactive' 53 | } 54 | } 55 | }, 56 | nid: { 57 | type: DataTypes.BIGINT, 58 | allowNull: false, 59 | unique: true, 60 | validate: { 61 | len: { 62 | args: [16, 16], 63 | msg: 'Nid must be a valid Rwandan National ID' 64 | } 65 | } 66 | }, 67 | phone: { 68 | type: DataTypes.STRING, 69 | allowNull: false, 70 | unique: true, 71 | validate: { 72 | is: { 73 | args: /^(25)?0?7[3 2 8]{1}[0-9]{7}$/, 74 | msg: 'Phone number must be a valid Rwandan Phone' 75 | } 76 | } 77 | }, 78 | position: { 79 | type: DataTypes.STRING, 80 | defaultValue: 'manager' 81 | }, 82 | birthday: { 83 | type: DataTypes.DATE, 84 | allowNull: false, 85 | validate: { 86 | isDate: { 87 | args: true, 88 | msg: 'Birthday must be a valid date (timestamp)' 89 | }, 90 | ageRestriction() { 91 | if (this.birthday) { 92 | if (checkAge(this.birthday) < 18) { 93 | throw new Error('Employee must be atleast 18 yrs old.'); 94 | } 95 | } 96 | } 97 | } 98 | }, 99 | confirmed: { 100 | type: DataTypes.BOOLEAN, 101 | defaultValue: false 102 | } 103 | }); 104 | 105 | export default Manager; 106 | -------------------------------------------------------------------------------- /src/routes/employees.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { 4 | create, 5 | createMany, 6 | search, 7 | edit, 8 | deleteEmployee, 9 | changeStatus 10 | } from '../controllers/employees'; 11 | 12 | const router = Router(); 13 | 14 | router.route('/').post(create); 15 | router.route('/search').put(search); 16 | router.route('/many').post(createMany); 17 | router.route('/:uuid/:status').put(changeStatus); 18 | router.route('/:uuid').put(edit).delete(deleteEmployee); 19 | 20 | export default router; 21 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import employeeRoutes from './employees'; 4 | import managerRoutes from './managers'; 5 | import { auth } from '../middlewares'; 6 | 7 | const router = Router(); 8 | 9 | router.use('/managers', managerRoutes); 10 | router.use('/employees', auth, employeeRoutes); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /src/routes/managers.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { auth } from '../middlewares'; 3 | import { 4 | login, 5 | create, 6 | requestReset, 7 | confirm, 8 | confirmReset 9 | } from '../controllers/managers'; 10 | 11 | const router = Router(); 12 | 13 | router.route('/login').post(login); 14 | router.route('/signup').post(auth, create); 15 | router.route('/reset').post(requestReset); 16 | 17 | router.route('/confirm/:confirmationToken').get(confirm); 18 | router 19 | .route('/reset/:token') 20 | .get((_, res) => res.sendStatus(400)) 21 | .post(auth, confirmReset); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /src/tests/employee.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import request from 'supertest'; 3 | import { describe, it, beforeEach, afterEach, after } from 'mocha'; 4 | import Employee from '../models/employees'; 5 | import { app, server } from '../app'; 6 | import { signToken } from '../utils'; 7 | 8 | const { JWT_LOGIN_SECRET } = process.env; 9 | describe.skip('employee tests: ', () => { 10 | after(() => { 11 | server.close(); 12 | }); 13 | beforeEach(async () => { 14 | await Employee.destroy({ truncate: true }); 15 | }); 16 | afterEach(async () => { 17 | await Employee.destroy({ truncate: true }); 18 | }); 19 | const mockToken = `Bearer ${signToken({}, JWT_LOGIN_SECRET)}`; 20 | const mockEmployee = { 21 | name: 'test name', 22 | email: 'test@email.com', 23 | phone: 250788477237, 24 | position: 'developer', 25 | nid: 1199880037471326, 26 | status: 'active', 27 | birthday: '1996-01-02', 28 | password: 'Jannyda1', 29 | }; 30 | it('should search for employees', async () => { 31 | await Employee.bulkCreate([ 32 | mockEmployee, 33 | { 34 | ...mockEmployee, 35 | nid: 1199880037471322, 36 | phone: 250788477232, 37 | email: 'test2@email.com', 38 | }, 39 | { 40 | ...mockEmployee, 41 | nid: 1199880037471323, 42 | phone: 250788477233, 43 | email: 'test3@email.com', 44 | }, 45 | { 46 | ...mockEmployee, 47 | nid: 1199880037471324, 48 | phone: 250788477234, 49 | email: 'test1@email.com', 50 | }, 51 | ]); 52 | const res = await request(app) 53 | .put(`/api/v1/employees/search?email=test&page=1&pageSize=10`) 54 | .set('authorization', mockToken); 55 | 56 | expect(res).to.have.property('status', 200); 57 | expect(res.body).to.have.property('found', 4); 58 | expect(res.body).to.have.property('success', true); 59 | }); 60 | it('should delete an employee', async () => { 61 | const employee = await Employee.create(mockEmployee); 62 | const res = await request(app) 63 | .delete(`/api/v1/employees/${employee.uuid}`) 64 | .set('authorization', mockToken); 65 | 66 | expect(res).to.have.property('status', 200); 67 | expect(res.body).to.have.property('success', true); 68 | }); 69 | it('should create an employee', async () => { 70 | const res = await request(app) 71 | .post(`/api/v1/employees`) 72 | .set('authorization', mockToken) 73 | .send(mockEmployee); 74 | 75 | expect(res).to.have.property('status', 201); 76 | expect(res.body).to.have.property('data'); 77 | expect(res.body).to.have.property('success', true); 78 | expect(res.body).to.have.property( 79 | 'message', 80 | `Employee ${mockEmployee.name} successfully created` 81 | ); 82 | }); 83 | it('should update an employee', async () => { 84 | const employee = await Employee.create(mockEmployee); 85 | const res = await request(app) 86 | .put(`/api/v1/employees/${employee.uuid}`) 87 | .set('authorization', mockToken) 88 | .send({ name: 'random' }); 89 | 90 | expect(res).to.have.property('status', 200); 91 | expect(res.body).to.have.property('success', true); 92 | expect(res.body).to.have.property( 93 | 'message', 94 | `Employee successfully modified` 95 | ); 96 | }); 97 | it('should suspend and activate an employee', async () => { 98 | const employee = await Employee.create(mockEmployee); 99 | let status = 'suspend'; 100 | let res = await request(app) 101 | .put(`/api/v1/employees/${employee.uuid}/${status}`) 102 | .set('authorization', mockToken); 103 | 104 | expect(res).to.have.property('status', 201); 105 | expect(res.body).to.have.property('success', true); 106 | expect(res.body).to.have.property( 107 | 'message', 108 | `Employee was ${status}ed successfully` 109 | ); 110 | status = 'activate'; 111 | res = await request(app) 112 | .put(`/api/v1/employees/${employee.uuid}/${status}`) 113 | .set('authorization', mockToken); 114 | 115 | expect(res).to.have.property('status', 201); 116 | expect(res.body).to.have.property('success', true); 117 | expect(res.body).to.have.property( 118 | 'message', 119 | `Employee was ${status}d successfully` 120 | ); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/tests/errorResponse.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import { ErrorResponse } from '../utils'; 4 | 5 | describe('Error Response Tests: ', () => { 6 | it('It should add a message and a statusCode to the Error object: ', async () => { 7 | const fakeErrorCode = 400; 8 | const fakeErrorMsg = 'fake message'; 9 | const fakeError = new ErrorResponse(fakeErrorMsg, fakeErrorCode); 10 | expect(fakeError).to.have.property('message'); 11 | expect(fakeError).to.have.property('statusCode'); 12 | expect(fakeError.message).to.be.equal(fakeErrorMsg); 13 | expect(fakeError.statusCode).to.be.equal(fakeErrorCode); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/tests/excel.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import { uploadXL } from '../utils'; 4 | 5 | const fakeReq = { 6 | files: { 7 | employees: { 8 | mv: (path, cb) => { 9 | return cb(null); 10 | }, 11 | }, 12 | }, 13 | }; 14 | describe('Excel Tests: ', () => { 15 | it('It should upload an excel sheet: ', async () => { 16 | const fakeErrorRes = uploadXL(fakeReq); 17 | expect(fakeErrorRes).to.be.equal(true); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/tests/mail.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import { sendEmail } from '../utils'; 4 | import { 5 | sendCommunication, 6 | sendConfirmation, 7 | sendMail, 8 | sendReset, 9 | } from '../utils/mail'; 10 | 11 | describe('Email Tests: ', () => { 12 | const fakeTo = 'jdoe@fake.mail'; 13 | it('It should send email with type and to: ', async () => { 14 | const fakeType = 'fakeType'; 15 | const send = await sendEmail(fakeType, fakeTo); 16 | expect(send).to.be.equal(undefined); 17 | }); 18 | it('It should send email for communication: ', () => { 19 | const fakeType = 'communication'; 20 | const send = sendCommunication(fakeType, fakeTo); 21 | expect(send).to.be.equal(undefined); 22 | }); 23 | it('It should send email for confirmation: ', () => { 24 | const fakeType = 'confirmation'; 25 | const send = sendConfirmation(fakeType, fakeTo); 26 | expect(send).to.be.equal(undefined); 27 | }); 28 | it('It should send email for password reset: ', () => { 29 | const fakeType = 'password reset'; 30 | const send = sendReset(fakeType, fakeTo); 31 | expect(send).to.be.equal(undefined); 32 | }); 33 | it('It should send email and return a preview: ', async () => { 34 | const fakeMail = { 35 | from: 'fakeFrom@mail.fake', 36 | to: fakeTo, 37 | subject: 'fake subject', 38 | html: `

fake body

`, 39 | }; 40 | const send = sendMail(fakeMail); 41 | console.log(send); 42 | expect(send).to.be.equal(undefined); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/tests/password.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import { encryptPassword, decryptPassword } from '../utils'; 4 | 5 | describe('Password Tests: ', () => { 6 | it('It should encrypt and decrypt password: ', async () => { 7 | const fakePass = 'fakePassword'; 8 | const fakePassEncrypted = await encryptPassword(fakePass); 9 | const isValid = await decryptPassword(fakePass, fakePassEncrypted); 10 | expect(isValid).to.be.equal(true); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/utils/errorResponse.js: -------------------------------------------------------------------------------- 1 | class ErrorResponse extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | } 6 | } 7 | 8 | export default ErrorResponse; 9 | -------------------------------------------------------------------------------- /src/utils/excel.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { readFile, utils } from 'xlsx'; 3 | 4 | export const uploadXL = (req) => { 5 | if (!req.files) { 6 | console.log('No files were uploaded.'); 7 | return false; 8 | } 9 | 10 | const sampleFile = req.files.employees; 11 | 12 | const uploadPath = resolve(__dirname, '../uploads/', 'Boo21.xlsx'); 13 | 14 | return sampleFile.mv(uploadPath, (err) => { 15 | if (err) { 16 | console.log(err); 17 | return false; 18 | } 19 | 20 | return true; 21 | }); 22 | }; 23 | 24 | export const readXL = () => { 25 | const wb = readFile(resolve(__dirname, '../uploads/', 'Boo21.xlsx'), { 26 | cellDates: true 27 | }); 28 | const ws = wb.Sheets.Sheet1; 29 | const employeesList = utils.sheet_to_json(ws).map((entry) => ({ 30 | name: entry.name, 31 | email: entry.email, 32 | phone: entry.phone, 33 | nid: entry.nid, 34 | position: entry.position, 35 | birthday: `${entry.birthday.split('/')[2]}-${ 36 | entry.birthday.split('/')[1] 37 | }-${entry.birthday.split('/')[0]}`, 38 | status: entry.status 39 | })); 40 | return employeesList; 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { encryptPassword, decryptPassword } from './password'; 2 | import { signToken, verifyToken } from './token'; 3 | import ErrorResponse from './errorResponse'; 4 | import { uploadXL, readXL } from './excel'; 5 | import { managerLog } from './logger'; 6 | import { sendEmail } from './mail'; 7 | 8 | const checkAge = (birthday) => { 9 | const ageDifMs = Date.now() - birthday.getTime(); 10 | const ageDate = new Date(ageDifMs); // miliseconds from epoch 11 | const age = Math.abs(ageDate.getUTCFullYear() - 1970); 12 | return age; 13 | }; 14 | 15 | export { 16 | checkAge, 17 | signToken, 18 | verifyToken, 19 | sendEmail, 20 | encryptPassword, 21 | decryptPassword, 22 | managerLog, 23 | readXL, 24 | uploadXL, 25 | ErrorResponse 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable complexity */ 2 | export const managerLog = (type, payload = null) => { 3 | const { manager, employee, status } = payload; 4 | let msg = '===== MANAGER LOG: '; 5 | const today = new Date(Date.now()); 6 | switch (type) { 7 | case 'reset': 8 | msg += `${manager} did reset his/her password at ${today} ======`; 9 | break; 10 | case 'login': 11 | msg += `${manager} logged into his account at ${today} ======`; 12 | break; 13 | case 'create': 14 | msg += `${manager} created a new employee ${employee} at ${today} ======`; 15 | break; 16 | case 'edit': 17 | msg += `${manager} ${type}d ${employee} ${today} ======`; 18 | break; 19 | case 'status': 20 | msg += `${manager} ${status}d ${employee} at ${today} ======`; 21 | break; 22 | case 'search': 23 | msg += `${manager} searched for employees at ${today} ======`; 24 | break; 25 | case 'delete': 26 | msg += `${manager} deleted an employee with ${employee} uuid at ${today} ======`; 27 | break; 28 | default: 29 | } 30 | console.log(`${msg}`.blue.underline); 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/mail.js: -------------------------------------------------------------------------------- 1 | import { config as dotenvConfig } from 'dotenv'; 2 | import { createTransport, getTestMessageUrl } from 'nodemailer'; 3 | import asyncHandler from '../middlewares/async'; 4 | import { signToken } from './token'; 5 | 6 | dotenvConfig(); 7 | const { JWT_CONFIRMATION_SECRET, MAIL, MAIL_PASS } = process.env; 8 | 9 | export const sendMail = (mail) => { 10 | const mailserver = { 11 | host: 'smtp.gmail.com', 12 | port: 587, 13 | secure: false, 14 | auth: { 15 | user: MAIL, 16 | pass: MAIL_PASS 17 | } 18 | }; 19 | const transporter = createTransport(mailserver); 20 | const info = transporter.sendMail(mail); 21 | return console.log(`Preview: ${getTestMessageUrl(info)}`); 22 | }; 23 | 24 | export const sendConfirmation = (type, to) => { 25 | const mail = { 26 | from: 'mamager@company.org', 27 | to, 28 | subject: `${type} - Verify your Email`, 29 | html: `

Dear ${to},

This is a confirmation email and to confirm; Please click the link below:

Confirmation Email Link

The confirmation link is valid for 15 minutes

` 33 | }; 34 | sendMail(mail); 35 | }; 36 | 37 | export const sendCommunication = (type, to) => { 38 | const mail = { 39 | from: 'mamager@company.org', 40 | to, 41 | subject: `${type} - Joined the Company`, 42 | html: `

Dear ${to},

It is with great pleasure, we inform you that you have successfully joined The Company today. We invite you to be part of one of our best team and to favorhardwork and to be at the office in Rwanda tomorrow morning at 7am.

Looking forward for your active participation.

Regards

Manager

` 43 | }; 44 | sendMail(mail); 45 | }; 46 | 47 | export const sendReset = (type, to) => { 48 | const mail = { 49 | from: 'mamager@company.org', 50 | to, 51 | subject: `${type}`, 52 | html: `

Dear ${to},

Please click the link below to reset your password

Password Reset Link

The password reset link is valid for 5 minutes

` 56 | }; 57 | sendMail(mail); 58 | }; 59 | 60 | export const sendEmail = asyncHandler(async (type, to) => { 61 | switch (type) { 62 | case 'confirmation': 63 | sendConfirmation(type, to); 64 | break; 65 | case 'communication': 66 | sendCommunication(type, to); 67 | break; 68 | case 'password reset': 69 | sendReset(type, to); 70 | break; 71 | default: 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/utils/password.js: -------------------------------------------------------------------------------- 1 | import { genSalt, hash as _hash, compare } from 'bcrypt'; 2 | import ErrorResponse from './errorResponse'; 3 | 4 | export const encryptPassword = async (password) => { 5 | const salt = await genSalt(12); 6 | const hash = await _hash(password, salt); 7 | return hash; 8 | }; 9 | 10 | export const decryptPassword = async (password, hash) => { 11 | const isValid = await compare(password, hash); 12 | if (!isValid) throw new ErrorResponse('Email/Password Incorrect!', 403); 13 | return isValid; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/token.js: -------------------------------------------------------------------------------- 1 | import { sign, verify } from 'jsonwebtoken'; 2 | 3 | export const signToken = (data, secret, duration = null) => { 4 | const tokenOptions = duration ? { expiresIn: duration } : undefined; 5 | const token = sign(data, secret, tokenOptions); 6 | return token; 7 | }; 8 | export const verifyToken = (token, secret) => { 9 | const data = verify(token, secret); 10 | return data; 11 | }; 12 | --------------------------------------------------------------------------------