Merge pull request #17238 from raisedadead/feat/authentication

fix(auth): Add verification route for email
This commit is contained in:
Stuart Taylor
2018-05-28 22:34:56 +01:00
committed by GitHub
12 changed files with 231 additions and 107 deletions

View File

@ -132,9 +132,9 @@ class EmailSettings extends PureComponent {
<FullWidthRow>
<HelpBlock>
<Alert bsStyle='info'>
A change of email address has not been verified.
To use your new email, you must verify it first using the link
we sent you.
Your email has not been verified.
To use your email, you must
<a href='/update-email'> verify it here first</a>.
</Alert>
</HelpBlock>
</FullWidthRow>

View File

@ -407,10 +407,15 @@ module.exports = function(User) {
);
};
User.afterRemote('logout', function(ctx, result, next) {
var res = ctx.res;
res.clearCookie('access_token');
res.clearCookie('userId');
User.afterRemote('logout', function({req, res}, result, next) {
const config = {
signed: !!req.signedCookies,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
res.clearCookie('jwt_access_token', config);
res.clearCookie('access_token', config);
res.clearCookie('userId', config);
res.clearCookie('_csrf', config);
next();
});
@ -571,14 +576,9 @@ module.exports = function(User) {
this.update$({ emailAuthLinkTTL })
);
})
.map(() => isSignUp ?
.map(() =>
dedent`
We created a new account for you!
Check your email and click the sign in link we sent you.
` :
dedent`
We found your existing account.
Check your email and click the sign in link we sent you.
Check your email and click the link we sent you to confirm you email.
`
);
}
@ -586,34 +586,26 @@ module.exports = function(User) {
User.prototype.requestAuthEmail = requestAuthEmail;
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
const currentEmail = this.email;
return Observable.defer(() => {
const isOwnEmail = isTheSame(newEmail, currentEmail);
const sameUpdate = isTheSame(newEmail, this.newEmail);
const messageOrNull = getWaitMessage(this.emailVerifyTTL);
if (isOwnEmail) {
if (this.emailVerified) {
const isResendUpdateToSameEmail = isTheSame(newEmail, this.newEmail);
const isLinkSentWithinLimit = getWaitMessage(this.emailVerifyTTL);
const isVerifiedEmail = this.emailVerified;
if (isOwnEmail && isVerifiedEmail) {
// email is already associated and verified with this account
throw wrapHandledError(
new Error('email is already verified'),
{
type: 'info',
message: `${newEmail} is already associated with this account.`
}
);
} else if (!this.emailVerified && messageOrNull) {
// email is associated but unverified and
// email is within time limit
throw wrapHandledError(
new Error(),
{
type: 'info',
message: messageOrNull
message: `
${newEmail} is already associated with this account.
You can update a new email address instead.`
}
);
}
}
if (sameUpdate && messageOrNull) {
if (isResendUpdateToSameEmail && isLinkSentWithinLimit) {
// trying to update with the same newEmail and
// confirmation email is still valid
throw wrapHandledError(
@ -622,24 +614,32 @@ module.exports = function(User) {
type: 'info',
message: dedent`
We have already sent an email confirmation request to ${newEmail}.
Please check your inbox.`
${isLinkSentWithinLimit}`
}
);
}
if (!isEmail('' + newEmail)) {
throw createEmailError();
}
// newEmail is not associated with this user, and
// this attempt to change email is the first or
// previous attempts have expired
return Observable.if(
() => isOwnEmail || (sameUpdate && messageOrNull),
Observable.empty(),
if (
!isOwnEmail ||
(isOwnEmail && !isVerifiedEmail) ||
(isResendUpdateToSameEmail && !isLinkSentWithinLimit)
) {
const updateConfig = {
newEmail,
emailVerified: false,
emailVerifyTTL: new Date()
};
// defer prevents the promise from firing prematurely (before subscribe)
Observable.defer(() => User.doesExist(null, newEmail))
)
return Observable.defer(() => User.doesExist(null, newEmail))
.do(exists => {
if (exists) {
if (exists && !isOwnEmail) {
// newEmail is not associated with this account,
// but is associated with different account
throw wrapHandledError(
@ -653,16 +653,19 @@ module.exports = function(User) {
}
})
.flatMap(()=>{
const update = {
newEmail,
emailVerified: false,
emailVerifyTTL: new Date()
};
return this.update$(update)
.do(() => Object.assign(this, update))
.flatMap(() => this.requestAuthEmail(false, newEmail));
return Observable.forkJoin(
this.update$(updateConfig),
this.requestAuthEmail(false, newEmail),
(_, message) => message
)
.do(() => {
Object.assign(this, updateConfig);
});
});
} else {
return 'Something unexpected happened whilst updating your email.';
}
};
function requestCompletedChallenges() {

View File

@ -38,10 +38,38 @@ module.exports = function enableAuthentication(app) {
ifUserRedirect,
(req, res) => res.redirect(301, '/auth/auth0'));
router.get(
'/update-email',
ifNoUserRedirectHome,
(req, res) => res.render('account/update-email', {
title: 'Update your email'
})
);
router.get('/signout', (req, res) => {
req.logout();
req.session.destroy( (err) => {
if (err) {
throw wrapHandledError(
new Error('could not destroy session'),
{
type: 'info',
message: 'Oops, something is not right.',
redirectTo: '/'
}
);
}
const config = {
signed: !!req.signedCookies,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
res.clearCookie('jwt_access_token', config);
res.clearCookie('access_token', config);
res.clearCookie('userId', config);
res.clearCookie('_csrf', config);
res.redirect('/');
});
});
router.get(
'/deprecated-signin',
@ -165,21 +193,11 @@ module.exports = function enableAuthentication(app) {
// update user and log them in
.map(user => user.loginByRequest(req, res))
.do(() => {
let redirectTo = '/';
if (
req.session &&
req.session.returnTo
) {
redirectTo = req.session.returnTo;
}
req.flash(
'success',
'Success! You have signed in to your account. Happy Coding!'
);
return res.redirect(redirectTo);
return res.redirect('/');
})
.subscribe(
() => {},

View File

@ -5,12 +5,12 @@ import { curry } from 'lodash';
import {
ifNoUser401,
ifNoUserRedirectTo,
ifNotVerifiedRedirectToSettings
ifNotVerifiedRedirectToUpdateEmail
} from '../utils/middleware';
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
const sendNonUserToHome = ifNoUserRedirectTo('/');
const sendNonUserToHomeWithMessage = curry(ifNoUserRedirectTo, 2)('/');
module.exports = function(app) {
const router = app.loopback.Router();
@ -24,7 +24,7 @@ module.exports = function(app) {
);
api.get(
'/account',
sendNonUserToMap,
sendNonUserToHome,
getAccount
);
api.post(
@ -34,15 +34,15 @@ module.exports = function(app) {
);
api.get(
'/account/unlink/:social',
sendNonUserToMap,
sendNonUserToHome,
getUnlinkSocial
);
// Ensure these are the last routes!
router.get(
'/user/:username/report-user/',
sendNonUserToMapWithMessage('You must be signed in to report a user'),
ifNotVerifiedRedirectToSettings,
sendNonUserToHomeWithMessage('You must be signed in to report a user'),
ifNotVerifiedRedirectToUpdateEmail,
getReportUserProfile
);
@ -119,6 +119,14 @@ module.exports = function(app) {
if (err) { return next(err); }
req.logout();
req.flash('success', 'You have successfully deleted your account.');
const config = {
signed: !!req.signedCookies,
domain: process.env.COOKIE_DOMAIN || 'localhost'
};
res.clearCookie('jwt_access_token', config);
res.clearCookie('access_token', config);
res.clearCookie('userId', config);
res.clearCookie('_csrf', config);
return res.status(200).end();
});
}

View File

@ -106,9 +106,8 @@ export default function setupPassport(app) {
// https://stackoverflow.com/q/37430452
let successRedirect = (req) => {
if (!!req && req.session && req.session.returnTo) {
let returnTo = req.session.returnTo;
delete req.session.returnTo;
return returnTo;
return '/';
}
return config.successRedirect || '';
};

View File

@ -58,6 +58,7 @@
"./middlewares/jade-helpers": {},
"./middlewares/flash-cheaters": {},
"./middlewares/passport-login": {},
"./middlewares/email-not-verified-notice": {},
"./middlewares/privacy-terms-notice": {}
},
"files": {},

View File

@ -0,0 +1,35 @@
import dedent from 'dedent';
const ALLOWED_METHODS = ['GET'];
const EXCLUDED_PATHS = [
'/api/flyers/findOne',
'/signout',
'/accept-privacy-terms',
'/update-email',
'/passwordless-change',
'/external/services/user'
];
export default function emailNotVerifiedNotice() {
return function(req, res, next) {
if (
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
EXCLUDED_PATHS.indexOf(req.path) === -1
) {
const { user } = req;
if (user && (!user.email || user.email === '' || !user.emailVerified)) {
req.flash(
'danger',
dedent`
New privacy laws now require that we have an email address where we can reach
you. Please verify your email address below and click the link we send you to
confirm.
`
);
res.redirect('/update-email');
return next;
}
}
return next();
};
}

View File

@ -2,7 +2,10 @@ const ALLOWED_METHODS = ['GET'];
const EXCLUDED_PATHS = [
'/api/flyers/findOne',
'/signout',
'/accept-privacy-terms'
'/accept-privacy-terms',
'/update-email',
'/passwordless-change',
'/external/services/user'
];
export default function privacyTermsNotAcceptedNotice() {

View File

@ -32,7 +32,7 @@ export function ifNoUser401(req, res, next) {
return res.status(401).end();
}
export function ifNotVerifiedRedirectToSettings(req, res, next) {
export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
const { user } = req;
if (!user) {
return next();

View File

@ -1,12 +1,12 @@
extends ../layout
block content
.container
.row.flashMessage.negative-30
.col-sm-6.col-sm-offset-3
.col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
#flash-board.alert.fade.in(style='display: none;')
button.close(type='button', data-dismiss='alert')
span.ion-close-circled#flash-close
#flash-content
.row
.col-xs-12
#accept-privacy-terms
.row

View File

@ -0,0 +1,57 @@
extends ../layout
block content
.row.flashMessage.negative-30
.col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
#flash-board.alert.fade.in(style='display: none;')
button.close(type='button', data-dismiss='alert')
span.ion-close-circled#flash-close
#flash-content
.container
h2.text-center Update your email address here:
form.form-horizontal.update-email(method='POST', action='/update-my-email', name="updateEmailForm")
.row
.col-sm-6.col-sm-offset-3
input(type='hidden', name='_csrf', value=_csrf)
.form-group
input.input-lg.form-control(type='email', name='email', id='email', value=user.email || '', placeholder=user.email || 'Enter your new email', autofocus, required, autocomplete="off")
.form-group
button.btn.btn-lg.btn-primary.btn-block(type='submit')= !user.email || user.emailVerified ? 'Update my Email' : 'Verify Email'
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/signout')
| Sign out
script.
$(document).ready(function() {
$('form').submit(function(event){
event.preventDefault();
$('#flash-board').hide();
var $form = $(event.target);
$.ajax({
type : 'POST',
url : $form.attr('action'),
data : $form.serialize(),
dataType : 'json',
encode : true,
xhrFields : { withCredentials: true }
})
.fail(error => {
if (error.responseText){
var data = JSON.parse(error.responseText);
if(data.message)
$('#flash-content').html(data.message);
$('#flash-board')
.removeClass('alert-success')
.addClass('alert-info')
.fadeIn();
}
})
.done(data =>{
if(data && data.message){
$('#flash-content').html(data.message);
$('#flash-board')
.removeClass('alert-info')
.addClass('alert-success')
.fadeIn();
}
});
});
});

View File

@ -14,7 +14,7 @@ nav.navbar.navbar-default.navbar-static-top.nav-height
a(href='https://forum.freecodecamp.org', target='_blank' rel='noopener') Forum
if !user
li
a(href='/signin') Start Coding
a(href='/signin') Sign in
else
li
a(href='/settings') Settings