Add email verification and notifications

This commit
- [x] Fixes the flash notice color (Trivial)
- [x] Adds flash message for user with no email.
- [x] Adds checks to see if user's email is verified, and displays corresponding notification.
- [x] Adds email templates.
This commit is contained in:
Mrugesh Mohapatra
2016-05-07 17:46:39 +05:30
parent bbacbd3d81
commit ff4dfb09da
8 changed files with 135 additions and 64 deletions

View File

@ -4,6 +4,7 @@ import moment from 'moment';
import dedent from 'dedent'; import dedent from 'dedent';
import debugFactory from 'debug'; import debugFactory from 'debug';
import { isEmail } from 'validator'; import { isEmail } from 'validator';
import path from 'path';
import { saveUser, observeMethod } from '../../server/utils/rx'; import { saveUser, observeMethod } from '../../server/utils/rx';
import { blacklistedUsernames } from '../../server/utils/constants'; import { blacklistedUsernames } from '../../server/utils/constants';
@ -89,7 +90,7 @@ module.exports = function(User) {
'You\'re email has been confirmed!' 'You\'re email has been confirmed!'
] ]
}); });
ctx.res.redirect('/email-signin'); ctx.res.redirect('/');
}); });
User.beforeRemote('create', function({ req, res }, _, next) { User.beforeRemote('create', function({ req, res }, _, next) {
@ -340,7 +341,52 @@ module.exports = function(User) {
new Error(`${email} is already associated with another account.`) new Error(`${email} is already associated with another account.`)
); );
} }
return this.update$({ email }).toPromise();
const emailVerified = false;
return this.update$({
email, emailVerified
})
.do(() => {
this.email = email;
this.emailVerified = emailVerified;
})
.flatMap(() => {
var mailOptions = {
type: 'email',
to: email,
from: 'Team@freecodecamp.com',
subject: 'Welcome to Free Code Camp!',
template: path.join(
__dirname,
'..',
'..',
'server',
'views',
'emails',
'user-email-verify.ejs'
)
};
return this.verify(mailOptions).then(
(data) => {
if (data) {
return Promise.resolve(
dedent`
Your email has been updated successfully, Please
follow the link we sent you, to confirm.
`);
}
return Promise.reject(
'Oops, something went wrong, please try again later'
);
},
(error) => {
debug(error);
return Promise.reject(
'Oops, something went wrong, please try again later'
);
}
);
}).toPromise();
}); });
}; };
@ -358,8 +404,8 @@ module.exports = function(User) {
], ],
returns: [ returns: [
{ {
arg: 'status', arg: 'message',
type: 'object' type: 'string'
} }
], ],
http: { http: {
@ -486,52 +532,11 @@ module.exports = function(User) {
} }
); );
User.prototype.updateEmail = function updateEmail(email) {
if (this.email && this.email === email) {
return Promise.reject(new Error(
`${email} is already associated with this account.`
));
}
return User.doesExist(null, email)
.then(exists => {
if (exists) {
return Promise.reject(
new Error(`${email} is already associated with another account.`)
);
}
return this.update$({ email }).toPromise();
});
};
User.remoteMethod(
'updateEmail',
{
isStatic: false,
description: 'updates the email of the user object',
accepts: [
{
arg: 'email',
type: 'string',
required: true
}
],
returns: [
{
arg: 'status',
type: 'object'
}
],
http: {
path: '/update-email',
verb: 'POST'
}
}
);
User.themes = { User.themes = {
night: true, night: true,
default: true default: true
}; };
User.prototype.updateTheme = function updateTheme(theme) { User.prototype.updateTheme = function updateTheme(theme) {
if (!this.constructor.themes[theme]) { if (!this.constructor.themes[theme]) {
const err = new Error( const err = new Error(

View File

@ -1,6 +1,7 @@
import { Observable } from 'rx'; import { Observable } from 'rx';
import debugFactory from 'debug'; import debugFactory from 'debug';
import { isEmail } from 'validator'; import { isEmail } from 'validator';
import path from 'path';
const debug = debugFactory('fcc:user:remote'); const debug = debugFactory('fcc:user:remote');
@ -15,7 +16,6 @@ module.exports = function(app) {
var User = app.models.User; var User = app.models.User;
var UserIdentity = app.models.UserIdentity; var UserIdentity = app.models.UserIdentity;
var UserCredential = app.models.UserCredential; var UserCredential = app.models.UserCredential;
var Email = app.models.Email;
User.observe('before delete', function(ctx, next) { User.observe('before delete', function(ctx, next) {
debug('removing user', ctx.where); debug('removing user', ctx.where);
var id = ctx.where && ctx.where.id ? ctx.where.id : null; var id = ctx.where && ctx.where.id ? ctx.where.id : null;
@ -70,21 +70,18 @@ module.exports = function(app) {
to: user.email, to: user.email,
from: 'Team@freecodecamp.com', from: 'Team@freecodecamp.com',
subject: 'Welcome to Free Code Camp!', subject: 'Welcome to Free Code Camp!',
redirect: '/', template: path.join(
text: [ __dirname,
'Greetings from San Francisco!\n\n', '..',
'Thank you for joining our community.\n', 'views',
'Feel free to email us at this address if you have ', 'emails',
'any questions about Free Code Camp.\n', 'a-extend-user-welcome.ejs'
'And if you have a moment, check out our blog: ', ),
'medium.freecodecamp.com.\n\n', redirect: '/'
'Good luck with the challenges!\n\n',
'- the Free Code Camp Team'
].join('')
}; };
debug('sending welcome email'); debug('sending welcome email');
return Email.send(mailOptions, function(err) { return user.verify(mailOptions, function(err) {
if (err) { return next(err); } if (err) { return next(err); }
return req.logIn(user, function(err) { return req.logIn(user, function(err) {
if (err) { return next(err); } if (err) { return next(err); }

View File

@ -18,7 +18,8 @@ import {
import { observeMethod } from '../utils/rx'; import { observeMethod } from '../utils/rx';
import { import {
ifNoUserSend ifNoUserSend,
flashIfNotVerified
} from '../utils/middleware'; } from '../utils/middleware';
import getFromDisk$ from '../utils/getFromDisk$'; import getFromDisk$ from '../utils/getFromDisk$';
@ -419,7 +420,10 @@ module.exports = function(app) {
redirectToNextChallenge redirectToNextChallenge
); );
router.get('/challenges/:challengeName', showChallenge); router.get('/challenges/:challengeName',
flashIfNotVerified,
showChallenge
);
app.use(router); app.use(router);

View File

@ -19,6 +19,8 @@ import {
calcLongestStreak calcLongestStreak
} from '../utils/user-stats'; } from '../utils/user-stats';
import { flashIfNotVerified } from '../utils/middleware';
const debug = debugFactory('fcc:boot:user'); const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map'); const sendNonUserToMap = ifNoUserRedirectTo('/map');
const certIds = { const certIds = {
@ -183,6 +185,7 @@ module.exports = function(app) {
router.get( router.get(
'/settings', '/settings',
sendNonUserToMap, sendNonUserToMap,
flashIfNotVerified,
getSettings getSettings
); );
router.get('/vote1', vote1); router.get('/vote1', vote1);

View File

@ -1,3 +1,5 @@
import dedent from 'dedent';
export function ifNoUserRedirectTo(url, message, type = 'errors') { export function ifNoUserRedirectTo(url, message, type = 'errors') {
return function(req, res, next) { return function(req, res, next) {
const { path } = req; const { path } = req;
@ -28,3 +30,24 @@ export function ifNoUser401(req, res, next) {
} }
return res.status(401).end(); return res.status(401).end();
} }
export function flashIfNotVerified(req, res, next) {
const user = req.user;
if (!user) {
return next();
}
const email = req.user.email;
const emailVerified = req.user.emailVerified;
if (!email) {
req.flash('info', { msg:
dedent `Please update your email address when you get a moment in
your <a href="\settings"> Settings Page.</a>`
});
} else if (!emailVerified) {
req.flash('info', { msg:
dedent `We have your email address with us, but its not yet verified.
Please follow the link we sent you, when you get a moment.`
});
}
return next();
}

View File

@ -46,10 +46,10 @@ block content
} }
}) })
.done(data =>{ .done(data =>{
if(data.status && data.status.count){ if(data && data.message){
$('#flash-content').html("Your email has been updated successfully!"); $('#flash-content').html(data.message);
$('#flash-board') $('#flash-board')
.removeClass('alert-danger') .removeClass('alert-info')
.addClass('alert-success') .addClass('alert-success')
.fadeIn(); .fadeIn();
} }

View File

@ -0,0 +1,24 @@
<h2>
Greetings from San Francisco!
</h2>
<p>
Thank you for joining our community.
</p>
<p>
Please verify your email by following the link below:
</p>
<p>
<a href="<%= verifyHref %>"><%= verifyHref %></a>
</p>
<p>
Feel free to email us at this address if you have any questions about Free Code Camp.
</p>
<p>
And if you have a moment, check out our blog: https://medium.freecodecamp.com.
</p>
<p>
Good luck with the challenges!
</p>
<p>
- the Free Code Camp Team.
</p>

View File

@ -0,0 +1,15 @@
<h3>
Thank you, for updating you contact details.
</h3>
<p>
Please verify your email by following the link below:
</p>
<p>
<a href="<%= verifyHref %>"><%= verifyHref %></a>
</p>
<p>
Feel free to email us at this address if you have any questions about Free Code Camp.
</p>
<p>
- the Free Code Camp Team.
</p>