Optimize code and streamline flow.

- Optimize code and streamline flow.
- Add return statements for error display.
- Invalidate status vars and TTLs on User Model.
- Build Observable for findOrCreate
This commit is contained in:
Mrugesh Mohapatra
2016-10-26 13:22:33 +00:00
committed by Berkeley Martinez
parent f8c818e7e7
commit e006f5c97d
6 changed files with 106 additions and 167 deletions

View File

@ -45,6 +45,18 @@ function nextTick(fn) {
return process.nextTick(fn); return process.nextTick(fn);
} }
function getWaitPeriod(ttl) {
const fiveMinutesAgo = moment().subtract(5, 'minutes');
const lastEmailSentAt = moment(new Date(ttl || null));
const isWaitPeriodOver = ttl ?
lastEmailSentAt.isBefore(fiveMinutesAgo) : true;
if (!isWaitPeriodOver) {
const minutesLeft = 5 -
(moment().minutes() - lastEmailSentAt.minutes());
return minutesLeft;
}
return 0;
}
module.exports = function(User) { module.exports = function(User) {
// NOTE(berks): user email validation currently not needed but build in. This // NOTE(berks): user email validation currently not needed but build in. This
// work around should let us sneak by // work around should let us sneak by
@ -75,6 +87,7 @@ module.exports = function(User) {
User.findOne$ = Observable.fromNodeCallback(User.findOne, User); User.findOne$ = Observable.fromNodeCallback(User.findOne, User);
User.update$ = Observable.fromNodeCallback(User.updateAll, User); User.update$ = Observable.fromNodeCallback(User.updateAll, User);
User.count$ = Observable.fromNodeCallback(User.count, User); User.count$ = Observable.fromNodeCallback(User.count, User);
User.findOrCreate$ = Observable.fromCallback(User.findOrCreate, User);
}); });
User.beforeRemote('create', function({ req }) { User.beforeRemote('create', function({ req }) {
@ -488,43 +501,40 @@ module.exports = function(User) {
} }
); );
User.requestAuthLink = function requestAuthLink(email, emailTemplate) { User.requestAuthLink = function requestAuthLink(email) {
if (!isEmail(email)) { if (!isEmail(email)) {
return Promise.reject( return Promise.reject(
new Error('The submitted email not valid.') new Error('The submitted email not valid.')
); );
} }
const filter = { var userObj = {
where: { email }, username: 'fcc' + uuid.v4().slice(0, 8),
// remove password from the query email: email,
fields: { password: null } emailVerified: false
}; };
return User.findOne$(filter) return User.findOrCreate$({ where: { email: userObj.email }}, userObj)
.map(user => { .map(([ err, user, isCreated ]) => {
if (!user) { if (err) {
debug(`no user found with the email ${email}.`);
// do not let the user know if an email is not found
// this is to avoid sending spam requests to valid users
return dedent` return dedent`
If you entered a valid email, a magic link is on its way. Oops, something is not right, please try again later.`;
Please click that link to sign in.`;
} }
// Todo : Break this below chunk to a separate function if (!isDev) {
const fiveMinutesAgo = moment().subtract(5, 'minutes'); const minutesLeft = getWaitPeriod(user.emailAuthLinkTTL);
const lastEmailSentAt = moment(new Date(user.emailAuthLinkTTL || null)); if (minutesLeft) {
const isWaitPeriodOver = user.emailAuthLinkTTL ?
lastEmailSentAt.isBefore(fiveMinutesAgo) : true;
if (!isWaitPeriodOver) {
const minutesLeft = 5 -
(moment().minutes() - lastEmailSentAt.minutes());
const timeToWait = minutesLeft ? const timeToWait = minutesLeft ?
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
'a few seconds'; 'a few seconds';
debug('request before wait time : ' + timeToWait); debug('request before wait time : ' + timeToWait);
return dedent` return dedent`
Please wait ${timeToWait} to resend email verification.`; Please wait ${timeToWait} to resend an authentication link.`;
}
}
let emailTemplate = 'user-request-sign-in.ejs';
if (isCreated) {
emailTemplate = 'user-request-sign-up.ejs';
} }
// create a temporary access token with ttl for 1 hour // create a temporary access token with ttl for 1 hour
@ -546,15 +556,19 @@ module.exports = function(User) {
type: 'email', type: 'email',
to: user.email, to: user.email,
from: 'Team@freecodecamp.com', from: 'Team@freecodecamp.com',
subject: 'Free Code Camp - Sign in Request!', subject: 'Free Code Camp - Authentication Request!',
text: renderAuthEmail({ text: renderAuthEmail({
loginEmail, loginEmail,
loginToken loginToken
}) })
}; };
if (!isDev) {
this.email.send(mailOptions, err =>{ this.email.send(mailOptions, err =>{
if (err) { throw err; } if (err) { throw err; }
}); });
} else {
console.log('~~~~\n' + mailOptions.text + '~~~~\n');
}
user.emailAuthLinkTTL = token.created; user.emailAuthLinkTTL = token.created;
user.save(err =>{ if (err) { throw err; }}); user.save(err =>{ if (err) { throw err; }});
}); });
@ -583,8 +597,6 @@ module.exports = function(User) {
description: 'request a link on email with temporary token to sign in', description: 'request a link on email with temporary token to sign in',
accepts: [{ accepts: [{
arg: 'email', type: 'string', required: true arg: 'email', type: 'string', required: true
}, {
arg: 'emailTemplate', type: 'string', required: true
}], }],
returns: [{ returns: [{
arg: 'message', type: 'string' arg: 'message', type: 'string'
@ -596,13 +608,7 @@ module.exports = function(User) {
); );
User.prototype.updateEmail = function updateEmail(email) { User.prototype.updateEmail = function updateEmail(email) {
const fiveMinutesAgo = moment().subtract(5, 'minutes');
const lastEmailSentAt = moment(new Date(this.emailVerifyTTL || null));
const ownEmail = email === this.email; const ownEmail = email === this.email;
const isWaitPeriodOver = this.emailVerifyTTL ?
lastEmailSentAt.isBefore(fiveMinutesAgo) :
true;
if (!isEmail('' + email)) { if (!isEmail('' + email)) {
return Observable.throw(createEmailError()); return Observable.throw(createEmailError());
} }
@ -613,18 +619,18 @@ module.exports = function(User) {
)); ));
} }
if (ownEmail && !isWaitPeriodOver) { if (!isDev) {
const minutesLeft = 5 - const minutesLeft = getWaitPeriod(this.emailVerifyTTL);
(moment().minutes() - lastEmailSentAt.minutes()); if (ownEmail && minutesLeft) {
const timeToWait = minutesLeft ? const timeToWait = minutesLeft ?
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
'a few seconds'; 'a few seconds';
debug('request before wait time : ' + timeToWait);
return Observable.throw(new Error( return Observable.throw(new Error(
`Please wait ${timeToWait} to resend email verification.` `Please wait ${timeToWait} to resend email verification.`
)); ));
} }
}
return Observable.fromPromise(User.doesExist(null, email)) return Observable.fromPromise(User.doesExist(null, email))
.flatMap(exists => { .flatMap(exists => {

View File

@ -139,8 +139,16 @@ function buildDisplayChallenges(
module.exports = function(app) { module.exports = function(app) {
const router = app.loopback.Router(); const router = app.loopback.Router();
const api = app.loopback.Router(); const api = app.loopback.Router();
<<<<<<< HEAD
const { User, Email } = app.models; const { User, Email } = app.models;
const map$ = cachedMap(app.models); const map$ = cachedMap(app.models);
=======
const User = app.models.User;
const AccessToken = app.models.AccessToken;
const Block = app.models.Block;
const { Email } = app.models;
const map$ = cachedMap(Block);
>>>>>>> Optimize code and streamline flow.
function findUserByUsername$(username, fields) { function findUserByUsername$(username, fields) {
return observeQuery( return observeQuery(
User, User,
@ -152,6 +160,9 @@ module.exports = function(app) {
); );
} }
AccessToken.findOne$ = Observable.fromNodeCallback(
AccessToken.findOne, AccessToken);
router.get('/login', function(req, res) { router.get('/login', function(req, res) {
res.redirect(301, '/signin'); res.redirect(301, '/signin');
}); });
@ -169,10 +180,8 @@ module.exports = function(app) {
router.get('/email-signin', getEmailSignin); router.get('/email-signin', getEmailSignin);
router.get('/deprecated-signin', getDepSignin); router.get('/deprecated-signin', getDepSignin);
router.get('/update-email', getUpdateEmail); router.get('/update-email', getUpdateEmail);
router.get('/passwordless-signin', getPasswordlessSignin); router.get('/passwordless-auth', getPasswordlessAuth);
router.get('/passwordless-signup', getPasswordlessSignup); api.post('/passwordless-auth', postPasswordlessAuth);
api.post('/passwordless-signin', postPasswordlessSignin);
api.post('/passwordless-signup', postPasswordlessSignup);
router.get( router.get(
'/delete-my-account', '/delete-my-account',
sendNonUserToMap, sendNonUserToMap,
@ -252,117 +261,24 @@ module.exports = function(app) {
}); });
} }
function postPasswordlessSignup(req, res) { function postPasswordlessAuth(req, res) {
if (req.user) { if (req.user || !(req.body && req.body.email)) {
return res.redirect('/'); return res.redirect('/');
} }
if (req.body && req.body.email) { return User.requestAuthLink(req.body.email, 'user-request-sign-in.ejs')
var userObj = { .then(msg => {
username: 'fcc' + uuid.v4().slice(0, 8), return res.status(200).send({ message: msg });
email: req.body.email,
emailVerified: false
};
var data = { or: [
{ username: userObj.username },
{ email: userObj.email },
{ emailVerified: userObj.emailVerified }
]};
return User.findOrCreate({where: data}, userObj, function(err, user) {
if (err) {
throw err;
}
User.requestAuthLink(user.email, 'user-request-sign-up.ejs');
});
} else {
return res.redirect('/');
}
}
function postPasswordlessSignin(req, res) {
if (req.user) {
return res.redirect('/');
}
if (req.body && req.body.email) {
var data = { or: [
{ email: req.body.email },
{ emailVerified: true }
]};
return User.findOne$({ where: { data }})
.map(user => {
User.requestAuthLink(user.email, 'user-request-sign-in.ejs');
});
} else {
return res.redirect('/');
}
}
function getPasswordlessSignup(req, res, next) {
if (req.user) {
req.flash('info', {
msg: 'Hey, looks like youre already signed in.'
});
return res.redirect('/');
}
const defaultErrorMsg = [
'Oops, something is not right, ',
'please request a fresh link to sign in.'].join('');
if (!req.query || !req.query.email || !req.query.token) {
req.flash('info', { msg: defaultErrorMsg });
return res.redirect('/email-signup');
}
const email = req.query.email;
/* const tokenId = req.query.token; */
return User.findOne$({ where: { email }})
.map(user => {
return user.createAccessToken(
{ ttl: User.settings.ttl }, (err, accessToken) => {
if (err) { throw err; }
var config = {
signed: !!req.signedCookies,
maxAge: accessToken.ttl
};
if (accessToken && accessToken.id) {
debug('setting cookies');
res.cookie('access_token', accessToken.id, config);
res.cookie('userId', accessToken.userId, config);
}
return req.logIn({
id: accessToken.userId.toString() }, err => {
if (err) { return next(err); }
debug('user logged in');
if (req.session && req.session.returnTo) {
var redirectTo = req.session.returnTo;
if (redirectTo === '/map-aside') {
redirectTo = '/map';
}
return res.redirect(redirectTo);
}
req.flash('success', { msg:
'Success! You have signed in to your account. Happy Coding!'
});
return res.redirect('/');
});
});
}) })
.subscribe( .catch(err => {
() => {}, debug(err);
next return res.status(200).send({
); message: 'Oops, something is not right, please try again later.'
});
});
} }
function getPasswordlessSignin(req, res, next) { function getPasswordlessAuth(req, res, next) {
if (req.user) { if (req.user) {
req.flash('info', { req.flash('info', {
msg: 'Hey, looks like youre already signed in.' msg: 'Hey, looks like youre already signed in.'
@ -370,9 +286,8 @@ module.exports = function(app) {
return res.redirect('/'); return res.redirect('/');
} }
const defaultErrorMsg = [ const defaultErrorMsg = [ 'Oops, something is not right, please request a ',
'Oops, something is not right, ', 'fresh link to sign in / sign up.' ].join('');
'please request a fresh link to sign in.'].join('');
if (!req.query || !req.query.email || !req.query.token) { if (!req.query || !req.query.email || !req.query.token) {
req.flash('info', { msg: defaultErrorMsg }); req.flash('info', { msg: defaultErrorMsg });
@ -380,10 +295,28 @@ module.exports = function(app) {
} }
const email = req.query.email; const email = req.query.email;
/* const tokenId = req.query.token; */
return User.findOne$({ where: { email }}) return User.findOne$({ where: { email }})
.map(user => { .map(user => {
if (!user) {
debug(`did not find a valid user with email: ${email}`);
req.flash('info', { msg: defaultErrorMsg });
return res.redirect('/email-signin');
}
const emailVerified = true;
const emailAuthLinkTTL = null;
const emailVerifyTTL = null;
user.update$({
emailVerified, emailAuthLinkTTL, emailVerifyTTL
})
.do((user) => {
user.emailVerified = emailVerified;
user.emailAuthLinkTTL = emailAuthLinkTTL;
user.emailVerifyTTL = emailVerifyTTL;
});
return user.createAccessToken( return user.createAccessToken(
{ ttl: User.settings.ttl }, (err, accessToken) => { { ttl: User.settings.ttl }, (err, accessToken) => {
if (err) { throw err; } if (err) { throw err; }

View File

@ -13,7 +13,7 @@ block content
h2 Sign in with your Email here: h2 Sign in with your Email here:
.button-spacer .button-spacer
.col-sm-6.col-sm-offset-3 .col-sm-6.col-sm-offset-3
form(method='POST', action='/passwordless-signin') form(method='POST', action='/passwordless-auth')
input(type='hidden', name='_csrf', value=_csrf) input(type='hidden', name='_csrf', value=_csrf)
.form-group .form-group
input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true)

View File

@ -16,7 +16,7 @@ block content
| Sign up with an Email here: | Sign up with an Email here:
.button-spacer .button-spacer
.col-sm-6.col-sm-offset-3 .col-sm-6.col-sm-offset-3
form(method='POST', action='/passwordless-signup') form(method='POST', action='/passwordless-auth')
input(type='hidden', name='_csrf', value=_csrf) input(type='hidden', name='_csrf', value=_csrf)
.form-group .form-group
input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true)

View File

@ -2,7 +2,7 @@ Greetings from San Francisco!
You can now sign in to Free Code Camp, without a password. Just follow the link below: You can now sign in to Free Code Camp, without a password. Just follow the link below:
https://freecodecamp.com/passwordless-signin?email=<%= loginEmail %>&token=<%= loginToken %> https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %>
IMPORTANT NOTE: IMPORTANT NOTE:
You can simply delete or ignore this email, if you did not make any such request. You can simply delete or ignore this email, if you did not make any such request.

View File

@ -3,7 +3,7 @@ Greetings from San Francisco!
Welcome to Free Code Camp. We've created a new account for you. Welcome to Free Code Camp. We've created a new account for you.
To verify and start using your profile just follow the link below: To verify and start using your profile just follow the link below:
https://freecodecamp.com/passwordless-signup?email=<%= loginEmail %>&token=<%= loginToken %> https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %>
Next steps: Next steps:
1. Visit the settings page and link your account to Github. 1. Visit the settings page and link your account to Github.