Skip to content

Commit

Permalink
Merge a0d2dd7 into 2a3b93c
Browse files Browse the repository at this point in the history
  • Loading branch information
jesseinit committed Jan 15, 2019
2 parents 2a3b93c + a0d2dd7 commit 1fa114b
Show file tree
Hide file tree
Showing 22 changed files with 485 additions and 91 deletions.
5 changes: 3 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"presets": ["@babel/preset-env"]
}
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
DATABASE_URL_TEST = postgresql://neon:''@localhost:5432/haven_test
DATABASE_URL_DEV = postgresql://neon:''@localhost:5432/haven_dev
JWT_SECRETE = <...>
4 changes: 4 additions & 0 deletions .eslintIgnore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

build/
server/migrations/
server/test/
9 changes: 6 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
sudo: required
language: node_js
node_js:
- "stable"
- 'stable'
cache:
directories:
- node_modules
script:
- npm test
- npm test
after_success:
- npm run coverage
- npm run coverage
9 changes: 6 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
import routes from './server/routes';

const swaggerDocument = YAML.load('swagger.yaml');
const app = express();
app.use(express.json());

app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.get('/', (req, res) => {
res.status(200).json('Welcome to Todos');
});

app.use('/api/v1', routes);

const port = process.env.PORT || 3000;

app.listen(port, () => {
// eslint-disable-next-line
console.log(`Server started on ${port}`);
});

export default app;
50 changes: 38 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,30 @@
"version": "1.0.0",
"description": "A Social platform for the creative at heart",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/andela/neon-ah-backend.git"
},
"engines": {
"node": "10.14.1"
},
"scripts": {
"db:migrate": "node_modules/.bin/sequelize db:migrate:undo:all && node_modules/.bin/sequelize db:migrate",
"db:seed": "node_modules/.bin/sequelize db:seed:all",
"comp": "babel index.js -d build",
"clear": "rimraf build && babel index.js -d build",
"start": "npm run comp && node build/index.js",
"start:dev": "nodemon -w src --exec npm run start",
"test": "nyc --reporter=html --reporter=text --require @babel/register mocha server/test/**/*.js --exit --timeout=20000",
"build": "rimraf build/* && babel ./ -d build --ignore 'node_modules','coverage' ",
"start": "npm run build && node build/index.js",
"dev": "nodemon --exec babel-node ./index.js --env",
"db:migrate:test": "cross-env NODE_ENV=test node_modules/.bin/sequelize db:migrate:undo:all && cross-env NODE_ENV=test node_modules/.bin/sequelize db:migrate",
"test": "npm run db:migrate:test && cross-env NODE_ENV=test nyc mocha ./server/test --exit --timeout=20000 --recursive",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"author": "Andela Simulations Programme",
"license": "MIT",
"dependencies": {
"@babel/polyfill": "^7.2.5",
"@babel/runtime": "^7.2.0",
"@sendgrid/mail": "^6.3.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3",
"cors": "^2.8.4",
"coveralls": "^3.0.2",
Expand All @@ -25,38 +36,53 @@
"express": "^4.16.3",
"express-jwt": "^5.3.1",
"express-session": "^1.15.6",
"jsonwebtoken": "^8.3.0",
"jsonwebtoken": "^8.4.0",
"method-override": "^2.3.10",
"methods": "^1.1.2",
"mocha": "^5.2.0",
"mocha-lcov-reporter": "^1.3.0",
"morgan": "^1.9.1",
"nyc": "^13.1.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pg": "^7.7.1",
"request": "^2.87.0",
"sequelize": "^4.42.0",
"slug": "^0.9.3",
"swagger-ui-express": "^4.0.2",
"underscore": "^1.9.1"
"underscore": "^1.9.1",
"yamljs": "^0.3.0"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.2.2",
"@babel/node": "^7.2.2",
"@babel/plugin-transform-runtime": "^7.2.0",
"@babel/preset-env": "^7.2.3",
"@babel/register": "^7.0.0",
"chai": "^4.2.0",
"chai-http": "^4.2.1",
"cross-env": "^5.2.0",
"eslint": "^5.3.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.14.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",
"sequelize-cli": "^5.4.0",
"nodemon": "^1.18.9",
"nyc": "^13.1.0",
"rimraf": "^2.6.3",
"yamljs": "^0.3.0"
"sequelize-cli": "^5.4.0",
"sinon": "^7.2.2"
},
"nyc": {
"require": [
"@babel/register"
],
"reporter": [
"lcov",
"text",
"html"
],
"exclude": [
"**/*.spec.js",
"/server/test/*.js`"
]
}
}
Empty file removed server/controllers/.gitkeep
Empty file.
87 changes: 87 additions & 0 deletions server/controllers/UserController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import TokenManager from '../helpers/TokenManager';
import MailManager from '../helpers/MailManager';
import db from '../models';
import PasswordManager from '../helpers/PasswordManager';

const { Users } = db;

/**
* @class UserController
*/
class UserController {
/**
*
* @description Method to send password reset link.
* @static
* @param {*} req Express Request object
* @param {*} res Express Response object
* @returns {object} Json response
* @memberof UserController
*/
static async forgotPassword(req, res) {
try {
const { email } = req.body;
const user = await Users.findOne({ where: { email } });
if (!user) {
res.status(404).send({ status: 'failure', message: 'User not found' });
return;
}

const token = TokenManager.sign({ email }, '24h');
await MailManager.sendPasswordResetLink({ user, token });

res
.status(200)
.send({ status: 'success', message: 'Kindly check your mail to reset your password' });
} catch (error) {
res.status(500).send({ error });
}
}

/**
* @description Method to reset user's password.
* @static
* @param {*} req Express Request object
* @param {*} res Express Response object
* @returns {object} Json response
* @memberof UserController
*/
static async passwordReset(req, res) {
try {
const { token } = req.params;

const { email } = TokenManager.verify(token);

const { newPassword, confirmPassword } = req.body;
const isPasswordEqual = newPassword === confirmPassword;

if (!isPasswordEqual) {
return res.status(400).send({ status: 'failure', message: 'Password does not match' });
}

const user = await Users.findOne({ where: { email } });

if (!user) {
res.status(404).send({ status: 'failure', message: 'User not found' });
return;
}

await Users.update(
{ password: PasswordManager.hashPassword(newPassword) },
{ where: { email } }
);

res.status(200).send({
status: 'success',
message: 'Password has been successfully updated. Kindly login.'
});
} catch (error) {
if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') {
return res.status(401).send({ status: 'failure', message: error.name });
}
res.status(500).send({ error });
}
}
}

export default UserController;
Empty file removed server/helpers/.gitkeep
Empty file.
34 changes: 34 additions & 0 deletions server/helpers/MailManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import sgMail from '@sendgrid/mail';
import env from 'dotenv';
import passwordResetEmailTemplate from './emailTemplates/resetPasswordTemplate';

env.config();

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

/**
*
* @description This class manages all things mailing.
* @class MailManager
*/
class MailManager {
/**
* @description Utility funcion to send password reset link
* @returns {Promise} promise object that represents mail success
* @static
* @param {string} email Email address if the user
* @memberof MailManager
*/
static sendPasswordResetLink({ user, token }) {
const message = {
to: `${user.email}`,
from: 'notification@neon-ah.com',
subject: 'Password Reset Link',
html: passwordResetEmailTemplate(user, token)
};

return sgMail.send(message);
}
}

export default MailManager;
19 changes: 19 additions & 0 deletions server/helpers/PasswordManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import bcrypt from 'bcryptjs';

/**
* @class PasswordManager
*/
class PasswordManager {
/**
* @description Helper method to hash passwords
* @static
* @param {string} password
* @returns {string} A string representing the hashed password
* @memberof PasswordManager
*/
static hashPassword(password) {
return bcrypt.hashSync(password, 10);
}
}

export default PasswordManager;
40 changes: 40 additions & 0 deletions server/helpers/TokenManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import jwt from 'jsonwebtoken';
import env from 'dotenv';

env.config();

const secret = process.env.JWT_SECRET;

/**
*
*
* @class TokenManager
*/
class TokenManager {
/**
*
* @description Encodes a passed token and returns a jwt token
* @static
* @param {*} payload
* @param {string} [ttl='2h']
* @returns {string} Jwt token
* @memberof Tokenize
*/
static sign(payload, ttl = '2h') {
return jwt.sign(payload, secret, { expiresIn: ttl });
}

/**
*
* @description Verifies a passed token and returns a decoded payload
* @static
* @param {string} token
* @returns {object} Payload
* @memberof Tokenize
*/
static verify(token) {
return jwt.verify(token, secret);
}
}

export default TokenManager;
37 changes: 37 additions & 0 deletions server/helpers/emailTemplates/resetPasswordTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const passwordResetEmailTemplate = (user, token) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link href="https://fonts.googleapis.com/css?family=Hind:400,500" rel="stylesheet">
<style>
* {font-family: 'Hind';}
.container {width: 80%;margin: 0 auto;background: #fbfbfb;padding: 30px;}
.username {font-size: 1.2rem;}
.message{font-size: 1.2rem;}
.reset-btn {display: inline-block;background: #2fb5ee;padding: 10px; color: #fff !important;text-decoration: none;font-size: 1rem;}
.logo {width: 165px; padding-bottom: 20px; border-bottom: 1px solid #2fb5ee;}
</style>
</head>
<body>
<div class="container">
<img class="logo"
src="https://res.cloudinary.com/jesseinit/image/upload/v1547303525/Logo.png"
alt="logo"
/>
<h3 class="username"">Hi ${user.lastName},</h3>
<p class="message">
You recently requested to reset your password for your Authors Haven's account. Use the
button below to reset it. This password reset is only valid for the next 24 hours.
</p>
<a class="reset-btn" href="http://localhost:3000/reset/${token}">
Reset your password
</a>
</div>
</body>
</html>
`;

export default passwordResetEmailTemplate;
Loading

0 comments on commit 1fa114b

Please sign in to comment.