diff --git a/common/models/user.js b/common/models/user.js index 991e5b48d2..405357c095 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -45,6 +45,18 @@ function 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) { // NOTE(berks): user email validation currently not needed but build in. This // work around should let us sneak by @@ -75,6 +87,7 @@ module.exports = function(User) { User.findOne$ = Observable.fromNodeCallback(User.findOne, User); User.update$ = Observable.fromNodeCallback(User.updateAll, User); User.count$ = Observable.fromNodeCallback(User.count, User); + User.findOrCreate$ = Observable.fromCallback(User.findOrCreate, User); }); 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)) { return Promise.reject( new Error('The submitted email not valid.') ); } - const filter = { - where: { email }, - // remove password from the query - fields: { password: null } + var userObj = { + username: 'fcc' + uuid.v4().slice(0, 8), + email: email, + emailVerified: false }; - return User.findOne$(filter) - .map(user => { - if (!user) { - 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 User.findOrCreate$({ where: { email: userObj.email }}, userObj) + .map(([ err, user, isCreated ]) => { + if (err) { return dedent` - If you entered a valid email, a magic link is on its way. - Please click that link to sign in.`; + Oops, something is not right, please try again later.`; } - // Todo : Break this below chunk to a separate function - const fiveMinutesAgo = moment().subtract(5, 'minutes'); - const lastEmailSentAt = moment(new Date(user.emailAuthLinkTTL || null)); - const isWaitPeriodOver = user.emailAuthLinkTTL ? - lastEmailSentAt.isBefore(fiveMinutesAgo) : true; - if (!isWaitPeriodOver) { - const minutesLeft = 5 - - (moment().minutes() - lastEmailSentAt.minutes()); - const timeToWait = minutesLeft ? - `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : - 'a few seconds'; - debug('request before wait time : ' + timeToWait); - return dedent` - Please wait ${timeToWait} to resend email verification.`; + if (!isDev) { + const minutesLeft = getWaitPeriod(user.emailAuthLinkTTL); + if (minutesLeft) { + const timeToWait = minutesLeft ? + `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : + 'a few seconds'; + debug('request before wait time : ' + timeToWait); + return dedent` + 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 @@ -546,15 +556,19 @@ module.exports = function(User) { type: 'email', to: user.email, from: 'Team@freecodecamp.com', - subject: 'Free Code Camp - Sign in Request!', + subject: 'Free Code Camp - Authentication Request!', text: renderAuthEmail({ loginEmail, loginToken }) }; - this.email.send(mailOptions, err =>{ - if (err) { throw err; } - }); + if (!isDev) { + this.email.send(mailOptions, err =>{ + if (err) { throw err; } + }); + } else { + console.log('~~~~\n' + mailOptions.text + '~~~~\n'); + } user.emailAuthLinkTTL = token.created; 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', accepts: [{ arg: 'email', type: 'string', required: true - }, { - arg: 'emailTemplate', type: 'string', required: true }], returns: [{ arg: 'message', type: 'string' @@ -596,13 +608,7 @@ module.exports = function(User) { ); 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 isWaitPeriodOver = this.emailVerifyTTL ? - lastEmailSentAt.isBefore(fiveMinutesAgo) : - true; - if (!isEmail('' + email)) { return Observable.throw(createEmailError()); } @@ -613,17 +619,17 @@ module.exports = function(User) { )); } - if (ownEmail && !isWaitPeriodOver) { - const minutesLeft = 5 - - (moment().minutes() - lastEmailSentAt.minutes()); - - const timeToWait = minutesLeft ? - `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : - 'a few seconds'; - - return Observable.throw(new Error( - `Please wait ${timeToWait} to resend email verification.` - )); + if (!isDev) { + const minutesLeft = getWaitPeriod(this.emailVerifyTTL); + if (ownEmail && minutesLeft) { + const timeToWait = minutesLeft ? + `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : + 'a few seconds'; + debug('request before wait time : ' + timeToWait); + return Observable.throw(new Error( + `Please wait ${timeToWait} to resend email verification.` + )); + } } return Observable.fromPromise(User.doesExist(null, email)) diff --git a/server/boot/user.js b/server/boot/user.js index eb9b0c4f41..412b8d3d6e 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -139,8 +139,16 @@ function buildDisplayChallenges( module.exports = function(app) { const router = app.loopback.Router(); const api = app.loopback.Router(); +<<<<<<< HEAD const { User, Email } = 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) { return observeQuery( User, @@ -152,6 +160,9 @@ module.exports = function(app) { ); } + AccessToken.findOne$ = Observable.fromNodeCallback( + AccessToken.findOne, AccessToken); + router.get('/login', function(req, res) { res.redirect(301, '/signin'); }); @@ -169,10 +180,8 @@ module.exports = function(app) { router.get('/email-signin', getEmailSignin); router.get('/deprecated-signin', getDepSignin); router.get('/update-email', getUpdateEmail); - router.get('/passwordless-signin', getPasswordlessSignin); - router.get('/passwordless-signup', getPasswordlessSignup); - api.post('/passwordless-signin', postPasswordlessSignin); - api.post('/passwordless-signup', postPasswordlessSignup); + router.get('/passwordless-auth', getPasswordlessAuth); + api.post('/passwordless-auth', postPasswordlessAuth); router.get( '/delete-my-account', sendNonUserToMap, @@ -252,117 +261,24 @@ module.exports = function(app) { }); } - function postPasswordlessSignup(req, res) { - if (req.user) { + function postPasswordlessAuth(req, res) { + if (req.user || !(req.body && req.body.email)) { return res.redirect('/'); } - if (req.body && req.body.email) { - var userObj = { - username: 'fcc' + uuid.v4().slice(0, 8), - 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 you’re 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('/'); - }); + return User.requestAuthLink(req.body.email, 'user-request-sign-in.ejs') + .then(msg => { + return res.status(200).send({ message: msg }); + }) + .catch(err => { + debug(err); + return res.status(200).send({ + message: 'Oops, something is not right, please try again later.' }); - }) - .subscribe( - () => {}, - next - ); + }); } - function getPasswordlessSignin(req, res, next) { + function getPasswordlessAuth(req, res, next) { if (req.user) { req.flash('info', { msg: 'Hey, looks like you’re already signed in.' @@ -370,9 +286,8 @@ module.exports = function(app) { return res.redirect('/'); } - const defaultErrorMsg = [ - 'Oops, something is not right, ', - 'please request a fresh link to sign in.'].join(''); + const defaultErrorMsg = [ 'Oops, something is not right, please request a ', + 'fresh link to sign in / sign up.' ].join(''); if (!req.query || !req.query.email || !req.query.token) { req.flash('info', { msg: defaultErrorMsg }); @@ -380,10 +295,28 @@ module.exports = function(app) { } const email = req.query.email; - /* const tokenId = req.query.token; */ return User.findOne$({ where: { email }}) .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( { ttl: User.settings.ttl }, (err, accessToken) => { if (err) { throw err; } diff --git a/server/views/account/email-signin.jade b/server/views/account/email-signin.jade index c9bcdc1128..1616b64a0a 100644 --- a/server/views/account/email-signin.jade +++ b/server/views/account/email-signin.jade @@ -13,7 +13,7 @@ block content h2 Sign in with your Email here: .button-spacer .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) .form-group input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) diff --git a/server/views/account/email-signup.jade b/server/views/account/email-signup.jade index 1e9cf67dcd..b431d99283 100644 --- a/server/views/account/email-signup.jade +++ b/server/views/account/email-signup.jade @@ -16,7 +16,7 @@ block content | Sign up with an Email here: .button-spacer .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) .form-group input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) diff --git a/server/views/emails/user-request-sign-in.ejs b/server/views/emails/user-request-sign-in.ejs index 7543b67712..74967a86cb 100644 --- a/server/views/emails/user-request-sign-in.ejs +++ b/server/views/emails/user-request-sign-in.ejs @@ -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: -https://freecodecamp.com/passwordless-signin?email=<%= loginEmail %>&token=<%= loginToken %> +https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> IMPORTANT NOTE: You can simply delete or ignore this email, if you did not make any such request. diff --git a/server/views/emails/user-request-sign-up.ejs b/server/views/emails/user-request-sign-up.ejs index b5f59aeee7..19b9468968 100644 --- a/server/views/emails/user-request-sign-up.ejs +++ b/server/views/emails/user-request-sign-up.ejs @@ -3,7 +3,7 @@ Greetings from San Francisco! 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: -https://freecodecamp.com/passwordless-signup?email=<%= loginEmail %>&token=<%= loginToken %> +https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> Next steps: 1. Visit the settings page and link your account to Github.