Skip to content

Commit

Permalink
166816205-feat(utils, mailer): set up password reset feature
Browse files Browse the repository at this point in the history
  - added a new migration field to add new column to the users table
  - implement random token string for password reset
  - Implement change password feature

[Delivers #166816205]
  • Loading branch information
kevoese committed Jul 3, 2019
1 parent f9ad1eb commit 7ccef36
Show file tree
Hide file tree
Showing 15 changed files with 452 additions and 145 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"one-var": 0,
"one-var-declaration-per-line": 0,
"new-cap": 0,
"consistent-return": 0,
"consistent-return": 0,
"no-console": "off",
"no-param-reassign": 0,
"comma-dangle": 0,
"curly": ["error", "multi-line"],
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,4 @@ node_modules
# Optional REPL history
.node_repl_history

# NYC output
.nyc_output
82 changes: 71 additions & 11 deletions controllers/users/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import Sequelize from 'sequelize';
import db from '../../db/models';
import { blackListThisToken, getToken } from '../../utils';
import {
blackListThisToken, randomString, hashPassword, getToken
} from '../../utils';
import { sendMail } from '../../utils/mailer';
import htmlMessage from '../../utils/mailer/mails';

export default {
signUp: async (req, res) => {
Expand All @@ -12,16 +17,16 @@ export default {
lastName,
password,
email,
username
username,
});
user.response.token = getToken(user.id, user.email);
return res.status(201).json({
message: 'User Registration successful',
user: user.response()
user: user.response(),
});
} catch (e) {
return res.status(500).json({
message: 'Something went wrong'
message: 'Something went wrong',
});
}
},
Expand All @@ -30,26 +35,26 @@ export default {
const { email, password } = req.body;
try {
const user = await db.User.findOne({
where: { email }
where: { email },
});
if (!user) {
return res.status(400).send({
error: 'Invalid email or password'
error: 'Invalid email or password',
});
}
const isPasswordValid = await user.passwordsMatch(password);
if (!isPasswordValid) {
return res.status(400).send({
error: 'Invalid email or password'
error: 'Invalid email or password',
});
}
return res.status(200).json({
message: 'User Login in successful',
user: user.response()
user: user.response(),
});
} catch (e) {
return res.status(500).json({
message: 'Something went wrong'
message: 'Something went wrong',
});
}
},
Expand All @@ -58,7 +63,62 @@ export default {
const token = req.headers['x-access-token'];
await blackListThisToken(token);
return res.status(200).send({
message: 'Thank you'
message: 'Thank you',
});
}
},
resetPassword: async (req, res) => {
const { email } = req.body;
try {
const user = await db.User.findOne({ where: { email } });
if (user) {
const passwordResetToken = randomString();
const date = new Date();
date.setHours(date.getHours() + 2);
user.update({ passwordResetToken, passwordResetExpire: date });
await sendMail({
email: user.email,
subject: 'Password Reset LInk',
content: htmlMessage(user.email, passwordResetToken),
});
return res.status(200).json({
message: 'Password reset successful. Check your email for password reset link!',
});
}
throw new Error('User not found');
} catch (err) {
return res.status(400).json({
message: 'User does not exist',
});
}
},

changePassword: async (req, res) => {
const { password } = req.body;
const { Op } = Sequelize;

const passwordHash = await hashPassword(password);

const { resetToken } = req.query;
try {
const user = await db.User.findOne({
where: {
passwordResetToken: resetToken,
passwordResetExpire: {
[Op.gt]: new Date(),
},
},
});
if (user) {
user.update({ password: passwordHash, resetToken: null, resetExpire: null });
return res.status(200).json({
message: 'Password has successfully been changed.',
});
}
throw new Error('Invalid operation');
} catch (err) {
return res.status(400).json({
message: 'Bad request',
});
}
},
};
8 changes: 4 additions & 4 deletions db/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ module.exports = {
development: {
use_env_variable: 'DATABASE_URL',
dialect: 'postgres',
logging: false
logging: false,
},
test: {
use_env_variable: 'TEST_DATABASE_URL',
dialect: 'postgres',
logging: false
logging: false,
},
production: {
use_env_variable: 'DATABASE_URL',
dialect: 'postgres',
logging: false
}
logging: false,
},
};
12 changes: 12 additions & 0 deletions db/migrations/alter_users_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface
.addColumn('Users', 'passwordResetToken', {
type: Sequelize.STRING,
allowNull: true,
})
.then(() => queryInterface.addColumn('Users', 'passwordResetExpire', {
type: Sequelize.DATE,
allowNull: true,
})),
down: queryInterface => queryInterface.removeColumn('Users', 'passwordResetToken').then(() => queryInterface.removeColumn('Users', 'passwordResetExpire')),
};
14 changes: 8 additions & 6 deletions db/models/User.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import bcrypt from 'bcryptjs';
import { getToken } from '../../utils';
import { getToken, hashPassword } from '../../utils';

module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
Expand All @@ -11,14 +11,16 @@ module.exports = (sequelize, DataTypes) => {
username: DataTypes.STRING,
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
password: DataTypes.STRING
password: DataTypes.STRING,
passwordResetToken: DataTypes.STRING,
passwordResetExpire: DataTypes.DATE,
},
{
hooks: {
beforeCreate: async (user) => {
user.password = await bcrypt.hash(user.password, 10);
}
}
user.password = await hashPassword(user.password);
},
},
}
);
User.associate = models => User.hasMany(models.Article, {
Expand All @@ -41,7 +43,7 @@ module.exports = (sequelize, DataTypes) => {
image: this.image,
firstName: this.firstName,
lastName: this.lastName,
id: this.id
id: this.id,
};
};
return User;
Expand Down
34 changes: 0 additions & 34 deletions index.js

This file was deleted.

129 changes: 65 additions & 64 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,65 +1,66 @@
{
"name": "express-authorshaven",
"version": "1.0.0",
"description": "A Social platform for the creative at heart",
"author": "Andela Simulations Programme",
"license": "MIT",
"entry": "server/index.js",
"scripts": {
"test": "NODE_ENV=test npm run migrate && nyc --reporter=html --reporter=text mocha \"tests/**/*.spec.js\" --require @babel/register --timeout 20000 --recursive --exit",
"prettier": "prettier **/**/*.{js,json} --write",
"dev": "NODE_ENV=development nodemon --exec babel-node server/index",
"migrate": "node_modules/.bin/sequelize db:migrate",
"seed": "node_modules/.bin/sequelize db:seed:all",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"eslint --ext .json --ext .js --fix",
"git add"
]
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/node": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"coveralls": "^3.0.4",
"eslint": "^5.3.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-import": "^2.18.0",
"eslint-plugin-prettier": "^3.1.0",
"husky": "^2.5.0",
"lint-staged": "^8.2.1",
"mocha": "^6.1.4",
"nodemon": "^1.19.1",
"nyc": "^14.1.1",
"prettier": "^1.18.2",
"sequelize-cli": "^5.5.0"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"consola": "^2.9.0",
"dotenv": "^8.0.0",
"express": "^4.17.1",
"indicative": "^5.0.8",
"jsonwebtoken": "^8.5.1",
"pg": "^7.11.0",
"pg-hstore": "^2.3.3",
"sequelize": "^5.8.12",
"swagger-ui-express": "^4.0.6",
"parse-database-url": "^0.3.0",
"sinon": "^7.3.2",
"sequelize-slugify": "^0.7.0",
"yamljs": "^0.3.0"
}
}
"name": "express-authorshaven",
"version": "1.0.0",
"description": "A Social platform for the creative at heart",
"author": "Andela Simulations Programme",
"license": "MIT",
"entry": "server/index.js",
"scripts": {
"test": "NODE_ENV=test npm run migrate && nyc --reporter=html --reporter=text mocha \"tests/**/*.spec.js\" --require @babel/register --timeout 20000 --recursive --exit",
"prettier": "prettier **/**/*.{js,json} --write",
"dev": "nodemon --exec babel-node server/index",
"migrate": "sequelize db:migrate",
"seed": "sequelize db:seed:all",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"eslint --ext .json --ext .js --fix",
"git add"
]
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.5",
"@babel/node": "^7.4.5",
"@babel/preset-env": "^7.4.5",
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"coveralls": "^3.0.4",
"eslint": "^5.3.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-import": "^2.18.0",
"eslint-plugin-prettier": "^3.1.0",
"husky": "^2.5.0",
"lint-staged": "^8.2.1",
"mocha": "^6.1.4",
"nodemon": "^1.19.1",
"nyc": "^14.1.1",
"prettier": "^1.18.2",
"sequelize-cli": "^5.5.0"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"consola": "^2.9.0",
"dotenv": "^8.0.0",
"express": "^4.17.1",
"indicative": "^5.0.8",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.2.1",
"pg": "^7.11.0",
"pg-hstore": "^2.3.3",
"sequelize": "^5.8.12",
"swagger-ui-express": "^4.0.6",
"parse-database-url": "^0.3.0",
"sinon": "^7.3.2",
"sequelize-slugify": "^0.7.0",
"yamljs": "^0.3.0"
}
}
Loading

0 comments on commit 7ccef36

Please sign in to comment.