Merge pull request #17238 from raisedadead/feat/authentication
fix(auth): Add verification route for email
This commit is contained in:
@ -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>
|
||||
|
@ -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,60 +586,60 @@ 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) {
|
||||
// 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
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
if (sameUpdate && messageOrNull) {
|
||||
// trying to update with the same newEmail and
|
||||
// confirmation email is still valid
|
||||
throw wrapHandledError(
|
||||
new Error(),
|
||||
{
|
||||
type: 'info',
|
||||
message: dedent`
|
||||
We have already sent an email confirmation request to ${newEmail}.
|
||||
Please check your inbox.`
|
||||
}
|
||||
);
|
||||
}
|
||||
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(),
|
||||
// defer prevents the promise from firing prematurely (before subscribe)
|
||||
Observable.defer(() => User.doesExist(null, newEmail))
|
||||
)
|
||||
const isOwnEmail = isTheSame(newEmail, currentEmail);
|
||||
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.
|
||||
You can update a new email address instead.`
|
||||
}
|
||||
);
|
||||
}
|
||||
if (isResendUpdateToSameEmail && isLinkSentWithinLimit) {
|
||||
// trying to update with the same newEmail and
|
||||
// confirmation email is still valid
|
||||
throw wrapHandledError(
|
||||
new Error(),
|
||||
{
|
||||
type: 'info',
|
||||
message: dedent`
|
||||
We have already sent an email confirmation request to ${newEmail}.
|
||||
${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
|
||||
if (
|
||||
!isOwnEmail ||
|
||||
(isOwnEmail && !isVerifiedEmail) ||
|
||||
(isResendUpdateToSameEmail && !isLinkSentWithinLimit)
|
||||
) {
|
||||
const updateConfig = {
|
||||
newEmail,
|
||||
emailVerified: false,
|
||||
emailVerifyTTL: new Date()
|
||||
};
|
||||
|
||||
// defer prevents the promise from firing prematurely (before subscribe)
|
||||
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(
|
||||
@ -652,17 +652,20 @@ 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));
|
||||
.flatMap(()=>{
|
||||
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() {
|
||||
|
@ -38,9 +38,37 @@ 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();
|
||||
res.redirect('/');
|
||||
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(
|
||||
@ -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(
|
||||
() => {},
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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 || '';
|
||||
};
|
||||
|
@ -58,6 +58,7 @@
|
||||
"./middlewares/jade-helpers": {},
|
||||
"./middlewares/flash-cheaters": {},
|
||||
"./middlewares/passport-login": {},
|
||||
"./middlewares/email-not-verified-notice": {},
|
||||
"./middlewares/privacy-terms-notice": {}
|
||||
},
|
||||
"files": {},
|
||||
|
35
server/middlewares/email-not-verified-notice.js
Normal file
35
server/middlewares/email-not-verified-notice.js
Normal 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();
|
||||
};
|
||||
}
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -1,12 +1,12 @@
|
||||
extends ../layout
|
||||
block content
|
||||
.container
|
||||
.row.flashMessage.negative-30
|
||||
.col-sm-6.col-sm-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.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
|
||||
.row
|
||||
.col-xs-12
|
||||
#accept-privacy-terms
|
||||
.row
|
||||
|
57
server/views/account/update-email.jade
Normal file
57
server/views/account/update-email.jade
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
|
Reference in New Issue
Block a user