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

View File

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

View File

@ -1,8 +1,8 @@
import User from '../models/user';
import crypto from 'crypto';
import async from 'async';
import nodemailer from 'nodemailer';
import mg from 'nodemailer-mailgun-transport';
import mail from '../utils/mail';
import auth from '../utils/auth';
export function createUser(req, res, next) {
const user = new User({
@ -24,11 +24,19 @@ export function createUser(req, res, next) {
if (loginErr) {
return next(loginErr);
}
res.json({
email: req.user.email,
username: req.user.username,
preferences: req.user.preferences,
id: req.user._id
mail.send('email-verification', {
body: {
link: `http://${req.headers.host}/verify?t=${auth.createVerificationToken(req.body.email)}`
},
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) => {
const auth = {
auth: {
api_key: process.env.MAILGUN_KEY,
domain: process.env.MAILGUN_DOMAIN
}
};
const transporter = nodemailer.createTransport(mg(auth));
const message = {
mail.send('reset-password', {
body: {
link: `http://${req.headers.host}/reset-password/${token}`,
},
to: user.email,
from: 'p5.js Web Editor <noreply@p5js.org>',
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.
\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);
});
}, done);
}
], (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) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function (err, user) {
if (!user) {

View File

@ -8,6 +8,7 @@ const userSchema = new Schema({
password: { type: String },
resetPasswordToken: String,
resetPasswordExpires: Date,
verified: { type: Number, default: -1 },
github: { type: String },
email: { type: String, unique: true },
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('/verify').get(UserController.verifyEmail);
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!