Skip to content

Commit

Permalink
166816202-feat(auth): Signup email verification
Browse files Browse the repository at this point in the history
- setup nodemailer
- update user model
- update user controller
- add mailer utils
- add activation controller to verify email token
- add tests
- add sinon for mocking mail
[Delivers #166816202]
  • Loading branch information
vincentayorinde committed Jul 3, 2019
1 parent 9526000 commit a3c889e
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 155 deletions.
62 changes: 42 additions & 20 deletions controllers/users/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
blackListThisToken, randomString, hashPassword, getToken
} from '../../utils';
import { sendMail } from '../../utils/mailer';
import htmlMessage from '../../utils/mailer/mails';
import { resetPasswordMessage } from '../../utils/mailer/mails';

export default {
signUp: async (req, res) => {
Expand All @@ -17,16 +17,17 @@ 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) {
console.log('message', e);
return res.status(500).json({
message: 'Something went wrong',
message: 'Something went wrong'
});
}
},
Expand All @@ -35,35 +36,34 @@ 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'
});
}
},

signOut: async (req, res) => {
const token = req.headers['x-access-token'];
await blackListThisToken(token);
return res.status(200).send({
message: 'Thank you',
message: 'Signed out successfully'
});
},
resetPassword: async (req, res) => {
Expand All @@ -78,16 +78,38 @@ export default {
await sendMail({
email: user.email,
subject: 'Password Reset LInk',
content: htmlMessage(user.email, passwordResetToken),
content: resetPasswordMessage(user.email, passwordResetToken)
});
return res.status(200).json({
message: 'Password reset successful. Check your email for password reset link!',
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',
message: 'User does not exist'
});
}
},

activate: async (req, res) => {
try {
const user = await db.User.findOne({
where: { emailVerificationToken: req.params.token, activated: false }
});
if (user) {
await user.update({ activated: true, emailVerificationToken: null });
return res.status(200).json({
message: 'Activation successful, You can now login',
user: user.response()
});
}
return res.status(400).json({
error: 'Invalid activation Link'
});
} catch (e) {
return res.status(400).json({
message: 'Bad request'
});
}
},
Expand All @@ -104,21 +126,21 @@ export default {
where: {
passwordResetToken: resetToken,
passwordResetExpire: {
[Op.gt]: new Date(),
},
},
[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.',
message: 'Password has successfully been changed.'
});
}
throw new Error('Invalid operation');
} catch (err) {
return res.status(400).json({
message: 'Bad request',
message: 'Bad request'
});
}
},
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


module.exports = {
up: (queryInterface, Sequelize) => queryInterface
.addColumn('Users', 'emailVerificationToken', {
type: Sequelize.STRING,
allowNull: true
})
.then(() => queryInterface.addColumn('Users', 'activated', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
})),
down: queryInterface => queryInterface
.removeColumn('Users', 'emailVerificationToken')
.then(() => queryInterface.removeColumn('Users', 'activated'))
};
34 changes: 25 additions & 9 deletions db/models/User.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import bcrypt from 'bcryptjs';
import { getToken, hashPassword } from '../../utils';
import { getToken, randomString, hashPassword } from '../../utils';
import { sendMail } from '../../utils/mailer';
import { activationMessage } from '../../utils/mailer/mails';

module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
Expand All @@ -14,19 +16,33 @@ module.exports = (sequelize, DataTypes) => {
password: DataTypes.STRING,
passwordResetToken: DataTypes.STRING,
passwordResetExpire: DataTypes.DATE,
emailVerificationToken: DataTypes.STRING,
activated: {
type: DataTypes.BOOLEAN,
default: false
}
},
{
hooks: {
beforeCreate: async (user) => {
user.password = await hashPassword(user.password);
beforeCreate: async user => {
user.password = await bcrypt.hash(user.password, 10);
user.emailVerificationToken = randomString();
},
},
afterCreate: async user => {
await sendMail({
email: user.email,
subject: 'Activate Account',
content: activationMessage(user.email, user.emailVerificationToken)
});
}
}
}
);
User.associate = models => User.hasMany(models.Article, {
foreignKey: 'userId',
cascade: true
});
User.associate = models =>
User.hasMany(models.Article, {
foreignKey: 'userId',
cascade: true
});

User.prototype.passwordsMatch = function match(password) {
return bcrypt.compare(password, this.password);
Expand All @@ -43,7 +59,7 @@ module.exports = (sequelize, DataTypes) => {
image: this.image,
firstName: this.firstName,
lastName: this.lastName,
id: this.id,
id: this.id
};
};
return User;
Expand Down
5 changes: 0 additions & 5 deletions env-sample

This file was deleted.

128 changes: 64 additions & 64 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,66 +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": "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"
}
"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 && NODE_ENV=test 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"
}
}
1 change: 1 addition & 0 deletions routes/v1/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ router.post('/login', Validation.logIn, User.logIn);
router.post('/signout', Middleware.authenticate, Middleware.isblackListedToken, User.signOut);
router.post('/reset-password', Validation.resetPassword, User.resetPassword);
router.put('/change-password', Validation.changePassword, User.changePassword);
router.put('/activate/:token', User.activate);

export default router;
3 changes: 3 additions & 0 deletions server/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default () => ({
secret: process.env.JWT_SECRET || 'secret'
});
Loading

0 comments on commit a3c889e

Please sign in to comment.