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>
|
<FullWidthRow>
|
||||||
<HelpBlock>
|
<HelpBlock>
|
||||||
<Alert bsStyle='info'>
|
<Alert bsStyle='info'>
|
||||||
A change of email address has not been verified.
|
Your email has not been verified.
|
||||||
To use your new email, you must verify it first using the link
|
To use your email, you must
|
||||||
we sent you.
|
<a href='/update-email'> verify it here first</a>.
|
||||||
</Alert>
|
</Alert>
|
||||||
</HelpBlock>
|
</HelpBlock>
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
|
@ -407,10 +407,15 @@ module.exports = function(User) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
User.afterRemote('logout', function(ctx, result, next) {
|
User.afterRemote('logout', function({req, res}, result, next) {
|
||||||
var res = ctx.res;
|
const config = {
|
||||||
res.clearCookie('access_token');
|
signed: !!req.signedCookies,
|
||||||
res.clearCookie('userId');
|
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();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -571,14 +576,9 @@ module.exports = function(User) {
|
|||||||
this.update$({ emailAuthLinkTTL })
|
this.update$({ emailAuthLinkTTL })
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map(() => isSignUp ?
|
.map(() =>
|
||||||
dedent`
|
dedent`
|
||||||
We created a new account for you!
|
Check your email and click the link we sent you to confirm you email.
|
||||||
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.
|
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -586,60 +586,60 @@ module.exports = function(User) {
|
|||||||
User.prototype.requestAuthEmail = requestAuthEmail;
|
User.prototype.requestAuthEmail = requestAuthEmail;
|
||||||
|
|
||||||
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
|
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
|
||||||
|
|
||||||
const currentEmail = this.email;
|
const currentEmail = this.email;
|
||||||
return Observable.defer(() => {
|
const isOwnEmail = isTheSame(newEmail, currentEmail);
|
||||||
const isOwnEmail = isTheSame(newEmail, currentEmail);
|
const isResendUpdateToSameEmail = isTheSame(newEmail, this.newEmail);
|
||||||
const sameUpdate = isTheSame(newEmail, this.newEmail);
|
const isLinkSentWithinLimit = getWaitMessage(this.emailVerifyTTL);
|
||||||
const messageOrNull = getWaitMessage(this.emailVerifyTTL);
|
const isVerifiedEmail = this.emailVerified;
|
||||||
if (isOwnEmail) {
|
|
||||||
if (this.emailVerified) {
|
if (isOwnEmail && isVerifiedEmail) {
|
||||||
// email is already associated and verified with this account
|
// email is already associated and verified with this account
|
||||||
throw wrapHandledError(
|
throw wrapHandledError(
|
||||||
new Error('email is already verified'),
|
new Error('email is already verified'),
|
||||||
{
|
{
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: `${newEmail} is already associated with this account.`
|
message: `
|
||||||
}
|
${newEmail} is already associated with this account.
|
||||||
);
|
You can update a new email address instead.`
|
||||||
} else if (!this.emailVerified && messageOrNull) {
|
}
|
||||||
// email is associated but unverified and
|
);
|
||||||
// email is within time limit
|
}
|
||||||
throw wrapHandledError(
|
if (isResendUpdateToSameEmail && isLinkSentWithinLimit) {
|
||||||
new Error(),
|
// trying to update with the same newEmail and
|
||||||
{
|
// confirmation email is still valid
|
||||||
type: 'info',
|
throw wrapHandledError(
|
||||||
message: messageOrNull
|
new Error(),
|
||||||
}
|
{
|
||||||
);
|
type: 'info',
|
||||||
}
|
message: dedent`
|
||||||
}
|
We have already sent an email confirmation request to ${newEmail}.
|
||||||
if (sameUpdate && messageOrNull) {
|
${isLinkSentWithinLimit}`
|
||||||
// trying to update with the same newEmail and
|
}
|
||||||
// confirmation email is still valid
|
);
|
||||||
throw wrapHandledError(
|
}
|
||||||
new Error(),
|
if (!isEmail('' + newEmail)) {
|
||||||
{
|
throw createEmailError();
|
||||||
type: 'info',
|
}
|
||||||
message: dedent`
|
|
||||||
We have already sent an email confirmation request to ${newEmail}.
|
// newEmail is not associated with this user, and
|
||||||
Please check your inbox.`
|
// this attempt to change email is the first or
|
||||||
}
|
// previous attempts have expired
|
||||||
);
|
if (
|
||||||
}
|
!isOwnEmail ||
|
||||||
if (!isEmail('' + newEmail)) {
|
(isOwnEmail && !isVerifiedEmail) ||
|
||||||
throw createEmailError();
|
(isResendUpdateToSameEmail && !isLinkSentWithinLimit)
|
||||||
}
|
) {
|
||||||
// newEmail is not associated with this user, and
|
const updateConfig = {
|
||||||
// this attempt to change email is the first or
|
newEmail,
|
||||||
// previous attempts have expired
|
emailVerified: false,
|
||||||
return Observable.if(
|
emailVerifyTTL: new Date()
|
||||||
() => isOwnEmail || (sameUpdate && messageOrNull),
|
};
|
||||||
Observable.empty(),
|
|
||||||
// defer prevents the promise from firing prematurely (before subscribe)
|
// defer prevents the promise from firing prematurely (before subscribe)
|
||||||
Observable.defer(() => User.doesExist(null, newEmail))
|
return Observable.defer(() => User.doesExist(null, newEmail))
|
||||||
)
|
|
||||||
.do(exists => {
|
.do(exists => {
|
||||||
if (exists) {
|
if (exists && !isOwnEmail) {
|
||||||
// newEmail is not associated with this account,
|
// newEmail is not associated with this account,
|
||||||
// but is associated with different account
|
// but is associated with different account
|
||||||
throw wrapHandledError(
|
throw wrapHandledError(
|
||||||
@ -652,17 +652,20 @@ module.exports = function(User) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flatMap(() => {
|
.flatMap(()=>{
|
||||||
const update = {
|
return Observable.forkJoin(
|
||||||
newEmail,
|
this.update$(updateConfig),
|
||||||
emailVerified: false,
|
this.requestAuthEmail(false, newEmail),
|
||||||
emailVerifyTTL: new Date()
|
(_, message) => message
|
||||||
};
|
)
|
||||||
return this.update$(update)
|
.do(() => {
|
||||||
.do(() => Object.assign(this, update))
|
Object.assign(this, updateConfig);
|
||||||
.flatMap(() => this.requestAuthEmail(false, newEmail));
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
} else {
|
||||||
|
return 'Something unexpected happened whilst updating your email.';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function requestCompletedChallenges() {
|
function requestCompletedChallenges() {
|
||||||
|
@ -38,9 +38,37 @@ module.exports = function enableAuthentication(app) {
|
|||||||
ifUserRedirect,
|
ifUserRedirect,
|
||||||
(req, res) => res.redirect(301, '/auth/auth0'));
|
(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) => {
|
router.get('/signout', (req, res) => {
|
||||||
req.logout();
|
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(
|
router.get(
|
||||||
@ -165,21 +193,11 @@ module.exports = function enableAuthentication(app) {
|
|||||||
// update user and log them in
|
// update user and log them in
|
||||||
.map(user => user.loginByRequest(req, res))
|
.map(user => user.loginByRequest(req, res))
|
||||||
.do(() => {
|
.do(() => {
|
||||||
let redirectTo = '/';
|
|
||||||
|
|
||||||
if (
|
|
||||||
req.session &&
|
|
||||||
req.session.returnTo
|
|
||||||
) {
|
|
||||||
redirectTo = req.session.returnTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.flash(
|
req.flash(
|
||||||
'success',
|
'success',
|
||||||
'Success! You have signed in to your account. Happy Coding!'
|
'Success! You have signed in to your account. Happy Coding!'
|
||||||
);
|
);
|
||||||
|
return res.redirect('/');
|
||||||
return res.redirect(redirectTo);
|
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {},
|
() => {},
|
||||||
|
@ -5,12 +5,12 @@ import { curry } from 'lodash';
|
|||||||
import {
|
import {
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
ifNoUserRedirectTo,
|
ifNoUserRedirectTo,
|
||||||
ifNotVerifiedRedirectToSettings
|
ifNotVerifiedRedirectToUpdateEmail
|
||||||
} from '../utils/middleware';
|
} from '../utils/middleware';
|
||||||
|
|
||||||
const debug = debugFactory('fcc:boot:user');
|
const debug = debugFactory('fcc:boot:user');
|
||||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
const sendNonUserToHome = ifNoUserRedirectTo('/');
|
||||||
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
|
const sendNonUserToHomeWithMessage = curry(ifNoUserRedirectTo, 2)('/');
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
@ -24,7 +24,7 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
api.get(
|
api.get(
|
||||||
'/account',
|
'/account',
|
||||||
sendNonUserToMap,
|
sendNonUserToHome,
|
||||||
getAccount
|
getAccount
|
||||||
);
|
);
|
||||||
api.post(
|
api.post(
|
||||||
@ -34,15 +34,15 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
api.get(
|
api.get(
|
||||||
'/account/unlink/:social',
|
'/account/unlink/:social',
|
||||||
sendNonUserToMap,
|
sendNonUserToHome,
|
||||||
getUnlinkSocial
|
getUnlinkSocial
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure these are the last routes!
|
// Ensure these are the last routes!
|
||||||
router.get(
|
router.get(
|
||||||
'/user/:username/report-user/',
|
'/user/:username/report-user/',
|
||||||
sendNonUserToMapWithMessage('You must be signed in to report a user'),
|
sendNonUserToHomeWithMessage('You must be signed in to report a user'),
|
||||||
ifNotVerifiedRedirectToSettings,
|
ifNotVerifiedRedirectToUpdateEmail,
|
||||||
getReportUserProfile
|
getReportUserProfile
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -119,6 +119,14 @@ module.exports = function(app) {
|
|||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
req.logout();
|
req.logout();
|
||||||
req.flash('success', 'You have successfully deleted your account.');
|
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();
|
return res.status(200).end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -106,9 +106,8 @@ export default function setupPassport(app) {
|
|||||||
// https://stackoverflow.com/q/37430452
|
// https://stackoverflow.com/q/37430452
|
||||||
let successRedirect = (req) => {
|
let successRedirect = (req) => {
|
||||||
if (!!req && req.session && req.session.returnTo) {
|
if (!!req && req.session && req.session.returnTo) {
|
||||||
let returnTo = req.session.returnTo;
|
|
||||||
delete req.session.returnTo;
|
delete req.session.returnTo;
|
||||||
return returnTo;
|
return '/';
|
||||||
}
|
}
|
||||||
return config.successRedirect || '';
|
return config.successRedirect || '';
|
||||||
};
|
};
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"./middlewares/jade-helpers": {},
|
"./middlewares/jade-helpers": {},
|
||||||
"./middlewares/flash-cheaters": {},
|
"./middlewares/flash-cheaters": {},
|
||||||
"./middlewares/passport-login": {},
|
"./middlewares/passport-login": {},
|
||||||
|
"./middlewares/email-not-verified-notice": {},
|
||||||
"./middlewares/privacy-terms-notice": {}
|
"./middlewares/privacy-terms-notice": {}
|
||||||
},
|
},
|
||||||
"files": {},
|
"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 = [
|
const EXCLUDED_PATHS = [
|
||||||
'/api/flyers/findOne',
|
'/api/flyers/findOne',
|
||||||
'/signout',
|
'/signout',
|
||||||
'/accept-privacy-terms'
|
'/accept-privacy-terms',
|
||||||
|
'/update-email',
|
||||||
|
'/passwordless-change',
|
||||||
|
'/external/services/user'
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function privacyTermsNotAcceptedNotice() {
|
export default function privacyTermsNotAcceptedNotice() {
|
||||||
|
@ -32,7 +32,7 @@ export function ifNoUser401(req, res, next) {
|
|||||||
return res.status(401).end();
|
return res.status(401).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ifNotVerifiedRedirectToSettings(req, res, next) {
|
export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return next();
|
return next();
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
extends ../layout
|
extends ../layout
|
||||||
block content
|
block content
|
||||||
.container
|
.row.flashMessage.negative-30
|
||||||
.row.flashMessage.negative-30
|
.col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
|
||||||
.col-sm-6.col-sm-offset-3
|
#flash-board.alert.fade.in(style='display: none;')
|
||||||
#flash-board.alert.fade.in(style='display: none;')
|
button.close(type='button', data-dismiss='alert')
|
||||||
button.close(type='button', data-dismiss='alert')
|
span.ion-close-circled#flash-close
|
||||||
span.ion-close-circled#flash-close
|
#flash-content
|
||||||
#flash-content
|
.row
|
||||||
.col-xs-12
|
.col-xs-12
|
||||||
#accept-privacy-terms
|
#accept-privacy-terms
|
||||||
.row
|
.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
|
a(href='https://forum.freecodecamp.org', target='_blank' rel='noopener') Forum
|
||||||
if !user
|
if !user
|
||||||
li
|
li
|
||||||
a(href='/signin') Start Coding
|
a(href='/signin') Sign in
|
||||||
else
|
else
|
||||||
li
|
li
|
||||||
a(href='/settings') Settings
|
a(href='/settings') Settings
|
||||||
|
Reference in New Issue
Block a user