From b94e883f8ff5e621e1c28566257f13f26b093894 Mon Sep 17 00:00:00 2001 From: Sijuade Ajagunna Date: Tue, 9 Jun 2020 12:08:22 +0100 Subject: [PATCH 1/3] add validation for request input (inc. email) and dockerized file --- Dockerfile | 7 +++ api/controller/mailingController.js | 76 ++++++++++++++++------------- api/helpers/errors.js | 15 ++++++ api/middleware/validator.js | 44 +++++++++++++++++ api/routes/mailingRoutes.js | 14 ++++-- api/validation/mail.validation.js | 42 ++++++++++++++++ docker-compose.yml | 8 +++ package-lock.json | 19 ++++++++ package.json | 4 +- 9 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 Dockerfile create mode 100644 api/helpers/errors.js create mode 100644 api/middleware/validator.js create mode 100644 api/validation/mail.validation.js create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..950e008 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node:latest +WORKDIR /app +COPY package.json /app +RUN npm install +COPY . /app +EXPOSE 4000 +CMD ["npm", "start"] \ No newline at end of file diff --git a/api/controller/mailingController.js b/api/controller/mailingController.js index 6f6211d..a2e5d68 100644 --- a/api/controller/mailingController.js +++ b/api/controller/mailingController.js @@ -5,14 +5,14 @@ function mailingController() { function sendMail(req, res) { (async function mail() { try { - let { recipients, subject, body, cc, bcc } = req.body - debug(recipients, subject, body) + let { recipients, subject, body, cc, bcc } = req.body; + debug(recipients, subject, body); if (!recipients || !subject || !body) { res.status(400).send({ status: false, - message: 'These fields are required' - }) - return + message: 'These fields are required', + }); + return; } let mailOptions = { @@ -28,34 +28,38 @@ function mailingController() { service: 'gmail', auth: { user: process.env.USER, - pass: process.env.PASSWORD - } + pass: process.env.PASSWORD, + }, }); - transporter.sendMail(mailOptions, function (err, info) { + transporter.sendMail(mailOptions, function(err, info) { if (err) debug(err); debug(`Email sent: ${info.response}`); - res.status(200).json({ status: 'success', data: {message: 'mail sent successfully'} }); - }) - + res + .status(200) + .json({ + status: 'success', + data: { message: 'mail sent successfully' }, + }); + }); } catch (err) { - debug(err.stack) + debug(err.stack); } - }()); + })(); } function sendMailWithTemplate(req, res) { (async function mail() { try { - let { recipients, subject, body, cc, bcc } = req.body - debug(recipients, subject, body) - if (!recipients || !subject || !body) { - res.status(400).send({ - status: false, - message: 'These fields are required' - }) - return - } + let { recipients, subject, body, cc, bcc } = req.body; + debug(recipients, subject, body); + // if (!recipients || !subject || !body) { + // res.status(400).send({ + // status: false, + // message: 'These fields are required' + // }) + // return + // } // if (recipients.match(mailFormat)) { // res.json({msg: true}) // res.status(400).send({ @@ -70,33 +74,37 @@ function mailingController() { to: recipients, bcc: [], subject: subject, - html: body - } + html: body, + }; let transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: process.env.USER, - pass: process.env.PASSWORD - } + pass: process.env.PASSWORD, + }, }); - transporter.sendMail(mailOptions, function (err, info) { + transporter.sendMail(mailOptions, function(err, info) { if (err) debug(err); debug(`Email sent: ${info.response}`); - res.status(200).json({ status: 'success', data: {message: 'mail sent successfully'} }); - }) - + res + .status(200) + .json({ + status: 'success', + data: { message: 'mail sent successfully' }, + }); + }); } catch (err) { - debug(err.stack) + debug(err.stack); } - }()); + })(); } return { sendMail, - sendMailWithTemplate + sendMailWithTemplate, }; } -module.exports = mailingController \ No newline at end of file +module.exports = mailingController; diff --git a/api/helpers/errors.js b/api/helpers/errors.js new file mode 100644 index 0000000..9b6c84d --- /dev/null +++ b/api/helpers/errors.js @@ -0,0 +1,15 @@ +module.exports = class ApplicationError extends Error { + /** + * @description initializes the error class + * + * @param {number} statusCode status code of the request + * @param {string} message error message + * @param {array} errors an array containing errors + */ + constructor(statusCode, message = 'an error occurred', errors) { + super(message); + this.statusCode = statusCode || 500; + this.message = message; + this.errors = errors; + } +}; diff --git a/api/middleware/validator.js b/api/middleware/validator.js new file mode 100644 index 0000000..995409a --- /dev/null +++ b/api/middleware/validator.js @@ -0,0 +1,44 @@ +const { matchedData, validationResult } = require('express-validator'); +const ApplicationError = require('../helpers/errors'); + +/** + * @description express-validator schema validator + * + * @param {Array} schema + * @param {Number} status - http statusCode + * + * @returns {Array} array of validation results and middleware + */ +module.exports = (schemas, status = 400) => { + const validationCheck = async (request, response, next) => { + const errors = validationResult(request); + request = { ...request, ...matchedData(request) }; + + if (!errors.isEmpty()) { + const mappedErrors = Object.entries(errors.mapped()).reduce( + (accumulator, [key, value]) => { + accumulator[key] = value.msg; + return accumulator; + }, + {} + ); + + const validationErrors = new ApplicationError( + status, + 'validation error', + mappedErrors + ); + + return response.status(400).json({ + status: 'error', + error: { + validationErrors, + }, + }); + } + + return next(); + }; + + return [...(schemas.length && [schemas]), validationCheck]; +}; diff --git a/api/routes/mailingRoutes.js b/api/routes/mailingRoutes.js index 5a518ef..8b10a5d 100644 --- a/api/routes/mailingRoutes.js +++ b/api/routes/mailingRoutes.js @@ -1,14 +1,18 @@ const express = require('express'); const mailingRouter = express.Router(); -const mailingController = require('../controller/mailingController') +const mailingController = require('../controller/mailingController'); +const { sendMailSchema } = require('../validation/mail.validation'); +const validator = require('../middleware/validator'); function router() { - const { sendMail, sendMailWithTemplate } = mailingController() + const { sendMail, sendMailWithTemplate } = mailingController(); - mailingRouter.route('/sendmail').post(sendMail) - mailingRouter.route('/sendmailwithtemplate').post(sendMailWithTemplate) + mailingRouter.route('/sendmail').post(validator(sendMailSchema), sendMail); + mailingRouter + .route('/sendmailwithtemplate') + .post(validator(sendMailSchema), sendMailWithTemplate); - return mailingRouter + return mailingRouter; } module.exports = router; diff --git a/api/validation/mail.validation.js b/api/validation/mail.validation.js new file mode 100644 index 0000000..0323a2e --- /dev/null +++ b/api/validation/mail.validation.js @@ -0,0 +1,42 @@ +const { check } = require('express-validator'); + +const requiredString = 'This field is required'; +const emailString = 'Value is not a valid email'; + +module.exports = { + sendMailSchema: [ + check('recipients') + .trim() + .not() + .isEmpty() + .withMessage(requiredString) + .isEmail() + .withMessage(emailString), + + check('subject') + .trim() + .not() + .isEmpty() + .withMessage(requiredString) + .isString(), + + check('body') + .trim() + .not() + .isEmpty() + .withMessage(requiredString) + .isString(), + + check('cc') + .optional() + .trim() + .isEmail() + .withMessage(emailString), + + check('bcc') + .optional() + .trim() + .isEmail() + .withMessage(emailString), + ], +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b6865c6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + app: + container_name: team-fierce + restart: always + build: . + ports: + - '4000:4000' diff --git a/package-lock.json b/package-lock.json index b8044b7..4fd32cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -462,6 +462,15 @@ } } }, + "express-validator": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.5.0.tgz", + "integrity": "sha512-kXi99TuVeLWkxO0RtDOSj56T7YR0H5KZZyhtzoPSZ5TffBvrJpZPSp/frYcT/zVoLhH8NXDk+T0LCSeI6TbOGA==", + "requires": { + "lodash": "^4.17.15", + "validator": "^13.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -716,6 +725,11 @@ "package-json": "^6.3.0" } }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -1309,6 +1323,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "validator": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.0.0.tgz", + "integrity": "sha512-anYx5fURbgF04lQV18nEQWZ/3wHGnxiKdG4aL8J+jEDsm98n/sU/bey+tYk6tnGJzm7ioh5FoqrAiQ6m03IgaA==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index c8d1f8f..57f43ad 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "app.js", "scripts": { "start": "set DEBUG=app,app:* & nodemon app.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "docker:build": "docker-compose up -d --build" }, "repository": { "type": "git", @@ -26,6 +27,7 @@ "debug": "^4.1.1", "dotenv": "^8.2.0", "express": "^4.17.1", + "express-validator": "^6.5.0", "morgan": "^1.10.0", "nodemailer": "^6.4.8", "nodemon": "^2.0.4" From 2416482c200070eedd9d3e0199d481a2e0806c9a Mon Sep 17 00:00:00 2001 From: Sijuade Ajagunna Date: Tue, 9 Jun 2020 15:18:25 +0100 Subject: [PATCH 2/3] set up swagger and documentation --- api/controller/mailingController.js | 14 +- app.js | 3 + docs/email-api.json | 195 ++++++++++++++++++++++++++++ package-lock.json | 13 ++ package.json | 3 +- 5 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 docs/email-api.json diff --git a/api/controller/mailingController.js b/api/controller/mailingController.js index a2e5d68..3467679 100644 --- a/api/controller/mailingController.js +++ b/api/controller/mailingController.js @@ -7,13 +7,13 @@ function mailingController() { try { let { recipients, subject, body, cc, bcc } = req.body; debug(recipients, subject, body); - if (!recipients || !subject || !body) { - res.status(400).send({ - status: false, - message: 'These fields are required', - }); - return; - } + // if (!recipients || !subject || !body) { + // res.status(400).send({ + // status: false, + // message: 'These fields are required', + // }); + // return; + // } let mailOptions = { from: 'Team Fierce Mailing API ', diff --git a/app.js b/app.js index aa355a8..8e9d66d 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,8 @@ const express = require('express') const morgan = require('morgan'); //logger const bodyParser = require('body-parser') +const swaggerUI = require('swagger-ui-express'); +const docs = require('./docs/email-api.json') require('dotenv').config() const app = express(); @@ -13,6 +15,7 @@ app.use(morgan('tiny')) const mailingRouter = require('./api/routes/mailingRoutes')() app.use('/api/v1', mailingRouter); +app.use('/docs', swaggerUI.serve, swaggerUI.setup(docs)); app.get('/', (req, res) => { res.send('home') diff --git a/docs/email-api.json b/docs/email-api.json new file mode 100644 index 0000000..595007e --- /dev/null +++ b/docs/email-api.json @@ -0,0 +1,195 @@ +{ + "openapi": "3.0.0", + "servers": [ + { + "description": "Development server", + "url": "http:/localhost:4000" + } + ], + "info": { + "description": "HNG i7 Team Fierce API", + "version": "1.0.0", + "title": "Team Fierce Mailing API", + "contact": { + "email": "" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "tags": [ + { + "name": "users", + "description": "Operations available to all users" + } + ], + "paths": { + "api/v1/sendmail": { + "post": { + "tags": ["users"], + "summary": "send mail", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMailRequestSchema" + } + } + } + }, + "responses": { + "200": { + "description": "signup successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMailResponseSchema" + } + } + } + }, + "400": { + "description": "validation errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMailErrorResponseSchema" + } + } + } + } + } + } + }, + "api/v1/sendmailwithtemplate": { + "post": { + "tags": ["users"], + "summary": "send mail", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMailRequestSchema" + } + } + } + }, + "responses": { + "200": { + "description": "signup successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMailResponseSchema" + } + } + } + }, + "400": { + "description": "validation errors", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMailErrorResponseSchema" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SendMailRequestSchema": { + "required": ["body", "recipients", "subject"], + "properties": { + "recipients": { + "type": "string", + "format": "email", + "example": "teamfierce@hngseven.com" + }, + "subject": { + "type": "string", + "example": "Generic Email Subject" + }, + "body": { + "type": "string", + "example": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras accumsan interdum consequat. Duis quis sem in lorem fringilla dapibus. Integer tincidunt ornare mauris, at efficitur odio finibus sit amet. Aliquam posuere magna eu mi fermentum, nec ultrices ex maximus. Etiam elementum turpis vel massa bibendum, vel maximus elit sodales. Fusce ligula lorem, placerat in augue pharetra, gravida lacinia quam. Donec eget nisi ac nisi tristique pellentesque." + }, + "cc": { + "type": "string", + "format": "email", + "example": "teamfierce@hngseven.com" + }, + "bcc": { + "type": "string", + "format": "email", + "example": "teamfierce@hngseven.com" + } + } + }, + "SendMailResponseSchema": { + "properties": { + "status": { + "type": "string", + "example": "success" + }, + "data": { + "properties": { + "message": { + "type": "string", + "example": "mail sent successfully" + } + } + } + } + }, + "SendMailErrorResponseSchema": { + "properties": { + "status": { + "type": "string", + "example": "error" + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "validation error" + }, + "errors": { + "type": "object", + "properties": { + "recipients": { + "type": "string", + "example": "This field is required" + }, + "bcc": { + "type": "string", + "example": "Value is not a valid email" + }, + "cc": { + "type": "string", + "example": "Value is not a valid email" + }, + "title": { + "type": "string", + "example": "This field is required" + }, + "body": { + "type": "string", + "example": "This field is required" + } + } + } + } + } + } + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 4fd32cb..009c10c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1206,6 +1206,19 @@ "has-flag": "^3.0.0" } }, + "swagger-ui-dist": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.26.0.tgz", + "integrity": "sha512-z58RlRUk//dTg6jwgFBVv0JNyfDpoRNUgEyoA9cRheNvUuklMTKMY3hgDfXSZpmnGgZEG8iA/SAZGE56hvRuug==" + }, + "swagger-ui-express": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", + "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, "term-size": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", diff --git a/package.json b/package.json index 57f43ad..fdb82c3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "express-validator": "^6.5.0", "morgan": "^1.10.0", "nodemailer": "^6.4.8", - "nodemon": "^2.0.4" + "nodemon": "^2.0.4", + "swagger-ui-express": "^4.1.4" } } From 0287f4f16dc889f7d6f1a7048a33585493d6ae5d Mon Sep 17 00:00:00 2001 From: Sijuade Ajagunna Date: Tue, 9 Jun 2020 20:47:05 +0100 Subject: [PATCH 3/3] refactor code --- Dockerfile | 2 +- api/controller/mailingController.js | 84 +++++++++++++++++++++++------ app.js | 2 +- docker-compose.yml | 2 +- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index 950e008..8e27a68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,5 +3,5 @@ WORKDIR /app COPY package.json /app RUN npm install COPY . /app -EXPOSE 4000 +EXPOSE 8099 CMD ["npm", "start"] \ No newline at end of file diff --git a/api/controller/mailingController.js b/api/controller/mailingController.js index 3467679..d7b5121 100644 --- a/api/controller/mailingController.js +++ b/api/controller/mailingController.js @@ -1,5 +1,8 @@ const debug = require('debug')('app:mailingController'); const nodemailer = require('nodemailer'); +const { config } = require('dotenv'); + +config(); function mailingController() { function sendMail(req, res) { @@ -7,6 +10,8 @@ function mailingController() { try { let { recipients, subject, body, cc, bcc } = req.body; debug(recipients, subject, body); + + // request body paarameters now being validated in validation middleware // if (!recipients || !subject || !body) { // res.status(400).send({ // status: false, @@ -15,17 +20,37 @@ function mailingController() { // return; // } + // let mailOptions = { + // from: 'Team Fierce Mailing API ', + // to: recipients, + // cc: [], + // bcc: [], + // subject: subject, + // text: body, + // }; + let mailOptions = { - from: 'Team Fierce Mailing API ', + from: `Team Fierce Mailing API ${process.env.USER}`, to: recipients, - cc: [], - bcc: [], - subject: subject, + cc, + bcc, + subject, text: body, }; + // let transporter = nodemailer.createTransport({ + // service: 'gmail', + // auth: { + // user: process.env.USER, + // pass: process.env.PASSWORD, + // }, + // }); + let transporter = nodemailer.createTransport({ - service: 'gmail', + host: 'smtp.gmail.com', + port: 587, + secure: false, + requireTLS: true, auth: { user: process.env.USER, pass: process.env.PASSWORD, @@ -33,14 +58,21 @@ function mailingController() { }); transporter.sendMail(mailOptions, function(err, info) { - if (err) debug(err); - debug(`Email sent: ${info.response}`); - res - .status(200) - .json({ + if (err) { + debug(err); + res.status(500).json({ + status: 'error', + error: { + message: err.message + } + }) + }else { + debug(`Email sent: ${info.response}`); + res.status(200).json({ status: 'success', data: { message: 'mail sent successfully' }, }); + } }); } catch (err) { debug(err.stack); @@ -53,6 +85,8 @@ function mailingController() { try { let { recipients, subject, body, cc, bcc } = req.body; debug(recipients, subject, body); + + // request body paarameters now being validated in validation middleware // if (!recipients || !subject || !body) { // res.status(400).send({ // status: false, @@ -69,16 +103,36 @@ function mailingController() { // return // } + // let mailOptions = { + // from: process.env.USER, + // to: recipients, + // bcc: [], + // subject: subject, + // html: body, + // }; + let mailOptions = { - from: 'Team Fierce Mailing API ', + from: `Team Fierce Mailing API ${process.env.USER}`, to: recipients, - bcc: [], - subject: subject, - html: body, + cc, + bcc, + subject, + text: body, }; + // let transporter = nodemailer.createTransport({ + // service: 'gmail', + // auth: { + // user: process.env.USER, + // pass: process.env.PASSWORD, + // }, + // }); + let transporter = nodemailer.createTransport({ - service: 'gmail', + host: 'smtp.gmail.com', + port: 587, + secure: false, + requireTLS: true, auth: { user: process.env.USER, pass: process.env.PASSWORD, diff --git a/app.js b/app.js index 8e9d66d..be99dda 100644 --- a/app.js +++ b/app.js @@ -21,7 +21,7 @@ app.get('/', (req, res) => { res.send('home') }); -port = 4000 +port = 8099; app.listen(port, function () { console.log(`Listening on port ${port}...`) }) diff --git a/docker-compose.yml b/docker-compose.yml index b6865c6..1fbfb49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,4 +5,4 @@ services: restart: always build: . ports: - - '4000:4000' + - '8099:8099'