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:
parent
ac9e65bb30
commit
2d781e22fb
9 changed files with 252 additions and 28 deletions
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,6 +24,13 @@ export function createUser(req, res, next) {
|
||||||
if (loginErr) {
|
if (loginErr) {
|
||||||
return next(loginErr);
|
return next(loginErr);
|
||||||
}
|
}
|
||||||
|
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({
|
res.json({
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
|
@ -33,6 +40,7 @@ export function createUser(req, res, next) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function duplicateUserCheck(req, res) {
|
export function duplicateUserCheck(req, res) {
|
||||||
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
37
server/utils/auth.js
Normal 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
80
server/utils/mail.js
Normal 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();
|
41
server/views/mailTemplates/email-verification.pug
Normal file
41
server/views/mailTemplates/email-verification.pug
Normal 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.
|
41
server/views/mailTemplates/reset-password.pug
Normal file
41
server/views/mailTemplates/reset-password.pug
Normal 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!
|
Loading…
Reference in a new issue