Email verification (#230)

* Making the email separate for future enhancements

* email-verification added

* Github users are verified

* update package

* Bug fixes and improvements

* jade to pug

* Bug fix

* changed route
This commit is contained in:
Akarshit Wal 2017-01-13 20:54:09 +05:30 committed by Cassie Tarakajian
parent ac9e65bb30
commit 2d781e22fb
9 changed files with 252 additions and 28 deletions

View file

@ -10,7 +10,7 @@
"build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress", "build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"fetch-examples": "node fetch-examples.js", "fetch-examples": "node fetch-examples.js",
"postinstall" : "git submodule update --remote --recursive" "postinstall": "git submodule update --remote --recursive"
}, },
"main": "index.js", "main": "index.js",
"author": "Cassie Tarakajian", "author": "Cassie Tarakajian",
@ -83,10 +83,13 @@
"express": "^4.13.4", "express": "^4.13.4",
"express-session": "^1.13.0", "express-session": "^1.13.0",
"file-type": "^3.8.0", "file-type": "^3.8.0",
"fs-promise": "^1.0.0",
"htmlhint": "^0.9.13", "htmlhint": "^0.9.13",
"is_js": "^0.9.0",
"js-beautify": "^1.6.4", "js-beautify": "^1.6.4",
"jsdom": "^9.8.3", "jsdom": "^9.8.3",
"jshint": "^2.9.2", "jshint": "^2.9.2",
"jsonwebtoken": "^7.2.1",
"lodash": "^4.16.4", "lodash": "^4.16.4",
"loop-protect": "git+https://git@github.com/catarak/loop-protect.git", "loop-protect": "git+https://git@github.com/catarak/loop-protect.git",
"moment": "^2.14.1", "moment": "^2.14.1",
@ -97,8 +100,9 @@
"passport": "^0.3.2", "passport": "^0.3.2",
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"q": "^1.4.1",
"project-name-generator": "^2.1.3", "project-name-generator": "^2.1.3",
"pug": "^2.0.0-beta6",
"q": "^1.4.1",
"react": "^15.1.0", "react": "^15.1.0",
"react-dom": "^15.1.0", "react-dom": "^15.1.0",
"react-inlinesvg": "^0.4.2", "react-inlinesvg": "^0.4.2",

View file

@ -53,6 +53,7 @@ passport.use(new GitHubStrategy({
existingEmailUser.username = existingEmailUser.username || profile.username; existingEmailUser.username = existingEmailUser.username || profile.username;
existingEmailUser.tokens.push({ kind: 'github', accessToken }); existingEmailUser.tokens.push({ kind: 'github', accessToken });
existingEmailUser.name = existingEmailUser.name || profile.displayName; existingEmailUser.name = existingEmailUser.name || profile.displayName;
existingEmailUser.verified = 0;
existingEmailUser.save((err) => { existingEmailUser.save((err) => {
return done(null, existingEmailUser); return done(null, existingEmailUser);
}); });
@ -63,6 +64,7 @@ passport.use(new GitHubStrategy({
user.username = profile.username; user.username = profile.username;
user.tokens.push({ kind: 'github', accessToken }); user.tokens.push({ kind: 'github', accessToken });
user.name = profile.displayName; user.name = profile.displayName;
user.verified = 0;
user.save((err) => { user.save((err) => {
return done(null, user); return done(null, user);
}); });

View file

@ -1,8 +1,8 @@
import User from '../models/user'; import User from '../models/user';
import crypto from 'crypto'; import crypto from 'crypto';
import async from 'async'; import async from 'async';
import nodemailer from 'nodemailer'; import mail from '../utils/mail';
import mg from 'nodemailer-mailgun-transport'; import auth from '../utils/auth';
export function createUser(req, res, next) { export function createUser(req, res, next) {
const user = new User({ const user = new User({
@ -24,11 +24,19 @@ export function createUser(req, res, next) {
if (loginErr) { if (loginErr) {
return next(loginErr); return next(loginErr);
} }
res.json({ mail.send('email-verification', {
email: req.user.email, body: {
username: req.user.username, link: `http://${req.headers.host}/verify?t=${auth.createVerificationToken(req.body.email)}`
preferences: req.user.preferences, },
id: req.user._id to: req.body.email,
subject: 'Email Verification',
}, (result) => { // eslint-disable-line no-unused-vars
res.json({
email: req.user.email,
username: req.user.username,
preferences: req.user.preferences,
id: req.user._id
});
}); });
}); });
}); });
@ -99,27 +107,13 @@ export function resetPasswordInitiate(req, res) {
}); });
}, },
(token, user, done) => { (token, user, done) => {
const auth = { mail.send('reset-password', {
auth: { body: {
api_key: process.env.MAILGUN_KEY, link: `http://${req.headers.host}/reset-password/${token}`,
domain: process.env.MAILGUN_DOMAIN },
}
};
const transporter = nodemailer.createTransport(mg(auth));
const message = {
to: user.email, to: user.email,
from: 'p5.js Web Editor <noreply@p5js.org>',
subject: 'p5.js Web Editor Password Reset', subject: 'p5.js Web Editor Password Reset',
text: `You are receiving this email because you (or someone else) have requested the reset of the password for your account. }, done);
\n\nPlease click on the following link, or paste this into your browser to complete the process:
\n\nhttp://${req.headers.host}/reset-password/${token}
\n\nIf you did not request this, please ignore this email and your password will remain unchanged.
\n\nThanks for using the p5.js Web Editor!\n`
};
transporter.sendMail(message, (error) => {
done(error);
});
} }
], (err) => { ], (err) => {
if (err) { if (err) {
@ -140,6 +134,28 @@ export function validateResetPasswordToken(req, res) {
}); });
} }
export function verifyEmail(req, res) {
const token = req.query.t;
// verify the token
auth.verifyEmailToken(token)
.then((data) => {
const email = data.email;
// change the verified field for the user or throw if the user is not found
User.findOne({ email })
.then((user) => {
// change the field for the user, and send the new cookie
user.verified = 0; // eslint-disable-line
user.save()
.then((result) => { // eslint-disable-line
res.json({ user });
});
});
})
.catch((err) => {
res.json(err);
});
}
export function updatePassword(req, res) { export function updatePassword(req, res) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function (err, user) { User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function (err, user) {
if (!user) { if (!user) {

View file

@ -8,6 +8,7 @@ const userSchema = new Schema({
password: { type: String }, password: { type: String },
resetPasswordToken: String, resetPasswordToken: String,
resetPasswordExpires: Date, resetPasswordExpires: Date,
verified: { type: Number, default: -1 },
github: { type: String }, github: { type: String },
email: { type: String, unique: true }, email: { type: String, unique: true },
tokens: Array, tokens: Array,

View file

@ -14,4 +14,6 @@ router.route('/reset-password/:token').get(UserController.validateResetPasswordT
router.route('/reset-password/:token').post(UserController.updatePassword); router.route('/reset-password/:token').post(UserController.updatePassword);
router.route('/verify').get(UserController.verifyEmail);
export default router; export default router;

37
server/utils/auth.js Normal file
View file

@ -0,0 +1,37 @@
const jwt = require('jsonwebtoken');
class Auth {
/**
* Create a verification token using jwt
*/
createVerificationToken(email, fromEmail) {
return jwt.sign({
email,
fromEmail,
}, process.env.SECRET_TOKEN, {
expiresIn: '1 day',
subject: 'email-verification',
});
}
/**
* Verify token
*/
verifyEmailToken(token) {
return new Promise((resolve, reject) => {
jwt.verify(token, process.env.SECRET_TOKEN, (err, data) => {
if (err) {
if (err.name === 'TokenExpiredError') {
reject('The verification link has expired');
} else if (err.name === 'JsonWebTokenError') {
reject('Verification link is malformend');
}
}
resolve(data);
});
});
}
}
export default new Auth();

80
server/utils/mail.js Normal file
View file

@ -0,0 +1,80 @@
'use strict';
/**
* Mail service wrapping around mailgun
*/
import fsp from 'fs-promise';
import pug from 'pug';
import is from 'is_js';
import nodemailer from 'nodemailer';
import mg from 'nodemailer-mailgun-transport';
const auth = {
api_key: process.env.MAILGUN_KEY,
domain: process.env.MAILGUN_DOMAIN,
};
class Mail {
constructor() {
this.client = nodemailer.createTransport(mg({ auth }));
this.sendOptions = {
from: process.env.EMAIL_SENDER,
replyTo: process.env.EMAIL_REPLY_TO,
};
}
getMailTemplate(type) {
let mailTemp;
switch (type) {
case 'reset-password':
mailTemp = 'server/views/mailTemplates/reset-password.pug';
break;
case 'email-verification':
mailTemp = 'server/views/mailTemplates/email-verification.pug';
break;
}
return mailTemp;
}
sendMail(mailOptions) {
return new Promise((resolve, reject) => {
this.client.sendMail(mailOptions, (err, info) => {
resolve(err, info);
});
});
}
dispatchMail(template, data, callback) {
const self = this;
return fsp.readFile(template, 'utf8')
.then((file) => {
const compiled = pug.compile(file, {
filename: template,
});
const body = compiled(data.body);
const mailOptions = {
to: data.to,
subject: data.subject,
from: self.sendOptions.from,
'h:Reply-To': self.sendOptions.replyTo,
html: body,
};
return self.sendMail(mailOptions);
})
.then((err, res) => {
callback(err, res);
});
}
send(type, data, callback) {
let template = null;
if (is.existy(data.template)) {
template = data.template;
} else {
template = this.getMailTemplate(type);
}
return this.dispatchMail(template, data, callback);
}
}
export default new Mail();

View file

@ -0,0 +1,41 @@
doctype html
html(xmlns='http://www.w3.org/1999/xhtml')
head
meta(http-equiv='Content-Type', content='text/html; charset=utf-8')
body(paddingwidth='0', paddingheight='0', style='padding-top: 0; padding-bottom: 0; background-repeat: repeat; width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased;background-color:white color:black;', offset='0', toppadding='0', leftpadding='0')
table.tableContent(border='0', cellspacing='0', cellpadding='0', align='center', bgcolor='white', style='font-family: Helvetica, Arial,serif;')
tr
td
table(width='650', border='0', cellspacing='0', cellpadding='0', align='center')
tr
td
table(width='650', border='0', cellspacing='0', cellpadding='0', align='center', )
tr
td(valign='top', align='center')
//.contentEditableContainer.contentImageEditable
.contentEditable
//- img(src='/img/mail-header-new.png', width='100%',alt='', data-default='placeholder', data-max-width='650')
tr
td(height='20')
tr
td.movableContentContainer(valign='top')
table(cellspacing="15")
tbody(style="font-size : 15px")
tr
td(style='text-align: center;font-family: sans-serif;font-size: 18px;')
h3 Email Verification
tr
td(style="color:black;line-height: 130%;padding: 10px;white-space:pre;")
| Hello,
| To verify you email, click on the button below:
tr(style="color:black;text-align:center")
td
a(href="#{link}" style="text-align:center;font-size:20px;font-family:Helvetica,arial,sans-serif;color:white;font-weight:bold; padding-left: 10px;display:inline-block;min-height:27px;padding : 4px 25px 4px 25px;line-height:27px;border-radius:2px;border-width:1px; background-color:black;" target="_blank") Verify Email
tr
td(style="color:black;padding:10px 10px 0 10px") Or copy and paste the URL into your browser:
tr(style="color:black")
td(width="560px" style='padding:10px;') #{link}
tr(style="color:black;padding:0 10px")
td This link is only valid for the next 24 hours.

View file

@ -0,0 +1,41 @@
doctype html
html(xmlns='http://www.w3.org/1999/xhtml')
head
meta(http-equiv='Content-Type', content='text/html; charset=utf-8')
body(paddingwidth='0', paddingheight='0', style='padding-top: 0; padding-bottom: 0; background-repeat: repeat; width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; -webkit-font-smoothing: antialiased;background-color:white color:black;', offset='0', toppadding='0', leftpadding='0')
table.tableContent(border='0', cellspacing='0', cellpadding='0', align='center', bgcolor='white', style='font-family: Helvetica, Arial,serif;')
tr
td
table(width='650', border='0', cellspacing='0', cellpadding='0', align='center')
tr
td
table(width='650', border='0', cellspacing='0', cellpadding='0', align='center', )
tr
td(valign='top', align='center')
//.contentEditableContainer.contentImageEditable
.contentEditable
//- img(src='/img/mail-header-new.png', width='100%',alt='', data-default='placeholder', data-max-width='650')
tr
td(height='20')
tr
td.movableContentContainer(valign='top')
table(cellspacing="15")
tbody(style="font-size : 15px")
tr
td(style='text-align: center;font-family: sans-serif;font-size: 18px;')
h3 Reset your password
tr
td(style="color:black;line-height: 130%;padding: 10px;white-space:pre;")
| Hello,
| We received a request to reset the password for your account. To reset your password, click on the button below:
tr(style="color:black;text-align:center")
td
a(href="#{link}" style="text-align:center;font-size:20px;font-family:Helvetica,arial,sans-serif;color:white;font-weight:bold; padding-left: 10px;display:inline-block;min-height:27px;padding : 4px 25px 4px 25px;line-height:27px;border-radius:2px;border-width:1px; background-color:black;" target="_blank") Reset password
tr
td(style="color:black;padding:10px 10px 0 10px") Or copy and paste the URL into your browser:
tr(style="color:black")
td(width="560px" style='padding:10px;') #{link}
tr(style="color:black;padding:0 10px")
td If you did not request this, please ignore this email and your password will remain unchanged. Thanks for using the p5.js Web Editor!