From f8c818e7e73763f7673925887ceb8aa57bf97319 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Sun, 26 Jun 2016 21:34:01 +0530 Subject: [PATCH 01/21] Implement passwordless login * Created a new rest API to create and save a temporary token that can be exchanged for a access token. * Updated the sign in view * Add email template for sign in links * Add route to request a access token and login user * Make the email views conistent --- common/models/user.js | 114 ++++++++++- common/models/user.json | 10 + server/boot/user.js | 179 ++++++++++++++++++ server/views/account/email-signin.jade | 79 ++++++-- server/views/account/email-signup.jade | 83 ++++++-- server/views/emails/a-extend-user-welcome.ejs | 20 +- server/views/emails/user-email-verify.ejs | 19 +- server/views/emails/user-request-sign-in.ejs | 15 ++ server/views/emails/user-request-sign-up.ejs | 22 +++ 9 files changed, 482 insertions(+), 59 deletions(-) create mode 100644 server/views/emails/user-request-sign-in.ejs create mode 100644 server/views/emails/user-request-sign-up.ejs diff --git a/common/models/user.js b/common/models/user.js index 526c37474e..991e5b48d2 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -5,6 +5,7 @@ import dedent from 'dedent'; import debugFactory from 'debug'; import { isEmail } from 'validator'; import path from 'path'; +import loopback from 'loopback'; import { saveUser, observeMethod } from '../../server/utils/rx.js'; import { blacklistedUsernames } from '../../server/utils/constants.js'; @@ -250,7 +251,7 @@ module.exports = function(User) { if (!user.verificationToken && !user.emailVerified) { ctx.req.flash('info', { msg: dedent`Looks like we have your email. But you haven't - verified it yet, please login and request a fresh verification + verified it yet, please sign in and request a fresh verification link.` }); return ctx.res.redirect(redirect); @@ -259,7 +260,7 @@ module.exports = function(User) { if (!user.verificationToken && user.emailVerified) { ctx.req.flash('info', { msg: dedent`Looks like you have already verified your email. - Please login to continue.` + Please sign in to continue.` }); return ctx.res.redirect(redirect); } @@ -267,7 +268,7 @@ module.exports = function(User) { if (user.verificationToken && user.verificationToken !== token) { ctx.req.flash('info', { msg: dedent`Looks like you have clicked an invalid link. - Please login and request a fresh one.` + Please sign in and request a fresh one.` }); return ctx.res.redirect(redirect); } @@ -487,6 +488,113 @@ module.exports = function(User) { } ); + User.requestAuthLink = function requestAuthLink(email, emailTemplate) { + 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 } + }; + 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 dedent` + If you entered a valid email, a magic link is on its way. + Please click that link to sign in.`; + } + + // 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.`; + } + + // create a temporary access token with ttl for 1 hour + user.createAccessToken({ ttl: 60 * 60 * 1000 }, (err, token) => { + if (err) { throw err; } + + const { id: loginToken } = token; + const loginEmail = user.email; + const renderAuthEmail = loopback.template(path.join( + __dirname, + '..', + '..', + 'server', + 'views', + 'emails', + emailTemplate + )); + const mailOptions = { + type: 'email', + to: user.email, + from: 'Team@freecodecamp.com', + subject: 'Free Code Camp - Sign in Request!', + text: renderAuthEmail({ + loginEmail, + loginToken + }) + }; + this.email.send(mailOptions, err =>{ + if (err) { throw err; } + }); + user.emailAuthLinkTTL = token.created; + user.save(err =>{ if (err) { throw err; }}); + }); + + return dedent` + If you entered a valid email, a magic link is on its way. + Please follow that link to sign in.`; + }) + .map((msg) => { + if (msg) { return msg; } + return dedent` + Oops, something is not right, please try again later.`; + }) + .catch(error => { + debug(error); + return Observable.throw( + 'Oops, something went wrong, please try again later.' + ); + }) + .toPromise(); + }; + + User.remoteMethod( + 'requestAuthLink', + { + 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' + }], + http: { + path: '/request-auth-link', verb: 'POST' + } + } + ); + User.prototype.updateEmail = function updateEmail(email) { const fiveMinutesAgo = moment().subtract(5, 'minutes'); const lastEmailSentAt = moment(new Date(this.emailVerifyTTL || null)); diff --git a/common/models/user.json b/common/models/user.json index a1d7b3f782..5874419451 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -19,6 +19,9 @@ "emailVerifyTTL": { "type": "date" }, + "emailAuthLinkTTL": { + "type": "date" + }, "password": { "type": "string" }, @@ -288,6 +291,13 @@ "principalId": "$owner", "permission": "ALLOW", "property": "updateLanguage" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW", + "property": "requestAuthLink" } ], "methods": {} diff --git a/server/boot/user.js b/server/boot/user.js index 597073bd56..eb9b0c4f41 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -3,6 +3,7 @@ import moment from 'moment-timezone'; import { Observable } from 'rx'; import debugFactory from 'debug'; import emoji from 'node-emoji'; +import uuid from 'node-uuid'; import { frontEndChallengeId, @@ -168,6 +169,10 @@ 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( '/delete-my-account', sendNonUserToMap, @@ -247,6 +252,180 @@ module.exports = function(app) { }); } + function postPasswordlessSignup(req, res) { + if (req.user) { + 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('/'); + }); + }); + }) + .subscribe( + () => {}, + next + ); + } + + function getPasswordlessSignin(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-signin'); + } + + 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( + () => {}, + next + ); + } + function signout(req, res) { req.logout(); res.redirect('/'); diff --git a/server/views/account/email-signin.jade b/server/views/account/email-signin.jade index fed5792311..c9bcdc1128 100644 --- a/server/views/account/email-signin.jade +++ b/server/views/account/email-signin.jade @@ -1,18 +1,65 @@ extends ../layout block content - .row - .col-xs-12 - h2.text-center Sign in with an email address here: - .col-sm-6.col-sm-offset-3 - form(method='POST', action='/api/users/login') - input(type='hidden', name='_csrf', value=_csrf) - .form-group - input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) - .form-group - input.input-lg.form-control(type='password', name='password', id='password', placeholder='Password') - button.btn.btn-primary.btn-lg.btn-block(type='submit') - span.ion-android-hand - | Login - .button-spacer - .button-spacer - a.btn.btn-info.btn-lg.btn-block(href='/forgot') Forgot your password? + .container + .col-xs-12 + .row + .col-sm-6.col-sm-offset-3.flashMessage.negative-30 + #flash-board.alert.fade.in(style='display: none;') + button.close(type='button', data-dismiss='alert') + span.ion-close-circled#flash-close + #flash-content + .row + .text-center + h2 Sign in with your Email here: + .button-spacer + .col-sm-6.col-sm-offset-3 + form(method='POST', action='/passwordless-signin') + input(type='hidden', name='_csrf', value=_csrf) + .form-group + input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) + .button-spacer + button.btn.btn-primary.btn-lg.btn-block(type='submit') + span.fa.fa-envelope + | Get a magic link to sign in. + .row + .col-sm-6.col-sm-offset-3 + br + p.text-center + a(href="/signup") Or Click here if you want to sign up. + + 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.error && data.error.message) + $('#flash-content').html(data.error.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(); + } + }); + }); + }); diff --git a/server/views/account/email-signup.jade b/server/views/account/email-signup.jade index 03b8614495..1e9cf67dcd 100644 --- a/server/views/account/email-signup.jade +++ b/server/views/account/email-signup.jade @@ -1,21 +1,68 @@ extends ../layout block content - script. - var challengeName = 'Email Signup' - h2.text-center Sign up with an email address here: - form.form-horizontal(method='POST', action='/api/users', name="signupForm") + .container + .col-xs-12 .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', placeholder='email', autofocus, required, autocomplete="off") - .form-group - input.input-lg.form-control(type='password', name='password', id='password', placeholder='password', required, pattern=".{8,50}", title="Must be at least 8 characters and no longer than 50 characters.") - .form-group - button.btn.btn-lg.btn-primary.btn-block(type='submit') - span.ion-person-add - | Signup - .row - .col-sm-6.col-sm-offset-3 - p.text-center - a(href="/signin") Click here if you already have an account and want to sign in. + .col-sm-6.col-sm-offset-3.flashMessage.negative-30 + #flash-board.alert.fade.in(style='display: none;') + button.close(type='button', data-dismiss='alert') + span.ion-close-circled#flash-close + #flash-content + .row + .text-center + h2 Are you new to Free Code Camp? + br + .button-spacer + | Sign up with an Email here: + .button-spacer + .col-sm-6.col-sm-offset-3 + form(method='POST', action='/passwordless-signup') + input(type='hidden', name='_csrf', value=_csrf) + .form-group + input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) + .button-spacer + button.btn.btn-primary.btn-lg.btn-block(type='submit') + span.fa.fa-envelope + | Get a magic link to sign up. + .row + .col-sm-6.col-sm-offset-3 + br + p.text-center + a(href="/signin") Click here if you already have an account and want to sign in. + + 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.error && data.error.message) + $('#flash-content').html(data.error.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(); + } + }); + }); + }); diff --git a/server/views/emails/a-extend-user-welcome.ejs b/server/views/emails/a-extend-user-welcome.ejs index ca18e34a54..a86828caf8 100644 --- a/server/views/emails/a-extend-user-welcome.ejs +++ b/server/views/emails/a-extend-user-welcome.ejs @@ -1,15 +1,11 @@ Greetings from San Francisco! -
-
+ Thank you for joining our community. -
-
+ Please verify your email by following the link below: -
-
+ <%= verifyHref %> -
-
+ Feel free to email us at this address if you have any questions about freeCodeCamp.

@@ -17,7 +13,7 @@ And if you have a moment, check out our blog: https://medium.freecodecamp.org.

Good luck with the challenges! -
-
-
-- the freeCodeCamp Team. + +Thanks, +The freeCodeCamp Team. +team@freecodecamp.com diff --git a/server/views/emails/user-email-verify.ejs b/server/views/emails/user-email-verify.ejs index b858b5b090..db8df54f39 100644 --- a/server/views/emails/user-email-verify.ejs +++ b/server/views/emails/user-email-verify.ejs @@ -1,14 +1,13 @@ Thank you for updating your contact details. -
-
+ Please verify your email by following the link below: -
-
+ <%= verifyHref %> -
-
+ Please email us at this address if you have any questions about freeCodeCamp. -
-
-
-- the freeCodeCamp Team + +Good luck with the challenges! + +Thanks, +The freeCodeCamp Team. +team@freecodecamp.com diff --git a/server/views/emails/user-request-sign-in.ejs b/server/views/emails/user-request-sign-in.ejs new file mode 100644 index 0000000000..7543b67712 --- /dev/null +++ b/server/views/emails/user-request-sign-in.ejs @@ -0,0 +1,15 @@ +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 %> + +IMPORTANT NOTE: +You can simply delete or ignore this email, if you did not make any such request. +Do not share this email with anyone, doing so may give them access to your account. + +Good luck with the challenges! + +Thanks, +The Free Code Camp Team. +team@freecodecamp.com diff --git a/server/views/emails/user-request-sign-up.ejs b/server/views/emails/user-request-sign-up.ejs new file mode 100644 index 0000000000..b5f59aeee7 --- /dev/null +++ b/server/views/emails/user-request-sign-up.ejs @@ -0,0 +1,22 @@ +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 %> + +Next steps: +1. Visit the settings page and link your account to Github. +2. Follow our Medium Publication: https://medium.freecodecamp.com +3. Checkout our forum: https://forum.freecodecamp.com +4. Join the conversation: https://gitter.im/FreeCodeCamp/FreeCodeCamp + +IMPORTANT NOTE: +You can simply delete or ignore this email, if you did not make any such request. +Do not share this email with anyone, doing so may give them access to your account. + +Good luck with the challenges! + +Thanks, +The Free Code Camp Team. +team@freecodecamp.com From e006f5c97d891f0e848bd479edf55726ea17c9f6 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Wed, 26 Oct 2016 13:22:33 +0000 Subject: [PATCH 02/21] 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 --- common/models/user.js | 106 +++++++------ server/boot/user.js | 159 ++++++------------- server/views/account/email-signin.jade | 2 +- server/views/account/email-signup.jade | 2 +- server/views/emails/user-request-sign-in.ejs | 2 +- server/views/emails/user-request-sign-up.ejs | 2 +- 6 files changed, 106 insertions(+), 167 deletions(-) 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. From 09174c9687eb88cc4ba8c698b51ab8f65dfd957a Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Tue, 1 Nov 2016 01:19:05 +0530 Subject: [PATCH 03/21] Add invalidate token handler --- server/boot/user.js | 64 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/server/boot/user.js b/server/boot/user.js index 412b8d3d6e..2e1dfac87e 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -180,7 +180,7 @@ module.exports = function(app) { router.get('/email-signin', getEmailSignin); router.get('/deprecated-signin', getDepSignin); router.get('/update-email', getUpdateEmail); - router.get('/passwordless-auth', getPasswordlessAuth); + router.get('/passwordless-auth', invalidateAuthToken, getPasswordlessAuth); api.post('/passwordless-auth', postPasswordlessAuth); router.get( '/delete-my-account', @@ -261,6 +261,9 @@ module.exports = function(app) { }); } + const defaultErrorMsg = [ 'Oops, something is not right, please request a ', + 'fresh link to sign in / sign up.' ].join(''); + function postPasswordlessAuth(req, res) { if (req.user || !(req.body && req.body.email)) { return res.redirect('/'); @@ -272,12 +275,62 @@ module.exports = function(app) { }) .catch(err => { debug(err); - return res.status(200).send({ - message: 'Oops, something is not right, please try again later.' - }); + return res.status(200).send({ message: defaultErrorMsg }); }); } + function invalidateAuthToken(req, res, next) { + if (req.user) { + res.redirect('/'); + } + + if (!req.query || !req.query.email || !req.query.token) { + req.flash('info', { msg: defaultErrorMsg }); + return res.redirect('/email-signin'); + } + + const authTokenId = req.query.token; + const authEmailId = req.query.email; + + return AccessToken.findOne$({ where: {id: authTokenId} }) + .map(authToken => { + if (!authToken) { + req.flash('info', { msg: defaultErrorMsg }); + return res.redirect('/email-signin'); + } + + const userId = authToken.userId; + return User.findById(userId, (err, user) => { + if (err) { + debug(err); + req.flash('info', { msg: defaultErrorMsg }); + return res.redirect('/email-signin'); + } + if (user.email !== authEmailId) { + req.flash('info', { msg: defaultErrorMsg }); + return res.redirect('/email-signin'); + } + return authToken.validate((err, isValid) => { + if (err) { throw err; } + if (!isValid) { + req.flash('info', { msg: [ 'Looks like the link you clicked has', + 'expired, please request a fresh link, to sign in.'].join('') + }); + return res.redirect('/email-signin'); + } + return authToken.destroy((err) => { + if (err) { debug(err); } + next(); + }); + }); + }); + }) + .subscribe( + () => {}, + next + ); + } + function getPasswordlessAuth(req, res, next) { if (req.user) { req.flash('info', { @@ -286,9 +339,6 @@ module.exports = function(app) { return res.redirect('/'); } - 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 }); return res.redirect('/email-signin'); From 19e5ff537eb0458758d74977c4eeaff23095a7ff Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Sun, 20 Nov 2016 16:21:13 +0530 Subject: [PATCH 04/21] Fix indenting and remove isDev from wait time. --- common/models/user.js | 52 +++++++++++++++++++++---------------------- server/boot/user.js | 3 ++- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 405357c095..0099f73146 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -505,7 +505,7 @@ module.exports = function(User) { if (!isEmail(email)) { return Promise.reject( new Error('The submitted email not valid.') - ); + ); } var userObj = { @@ -517,19 +517,19 @@ module.exports = function(User) { .map(([ err, user, isCreated ]) => { if (err) { return dedent` - Oops, something is not right, please try again later.`; + Oops, something is not right, please try again later. + `; } - 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.`; - } + 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'; @@ -574,13 +574,15 @@ module.exports = function(User) { }); return dedent` - If you entered a valid email, a magic link is on its way. - Please follow that link to sign in.`; + If you entered a valid email, a magic link is on its way. + Please follow that link to sign in. + `; }) .map((msg) => { if (msg) { return msg; } return dedent` - Oops, something is not right, please try again later.`; + Oops, something is not right, please try again later. + `; }) .catch(error => { debug(error); @@ -619,17 +621,15 @@ module.exports = function(User) { )); } - 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.` - )); - } + 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 2e1dfac87e..461ca49386 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -161,7 +161,8 @@ module.exports = function(app) { } AccessToken.findOne$ = Observable.fromNodeCallback( - AccessToken.findOne, AccessToken); + AccessToken.findOne, AccessToken + ); router.get('/login', function(req, res) { res.redirect(301, '/signin'); From 26c1389c3ac5acef60f31658a61533ba1b5fa7ca Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Wed, 23 Nov 2016 19:54:08 +0530 Subject: [PATCH 05/21] Donot render per request email --- common/models/user.js | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 0099f73146..83a23eaae3 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -27,6 +27,26 @@ function destroyAll(id, Model) { )({ userId: id }); } +const renderSignUpEmail = loopback.template(path.join( + __dirname, + '..', + '..', + 'server', + 'views', + 'emails', + 'user-request-sign-up.ejs' +)); + +const renderSignInEmail = loopback.template(path.join( + __dirname, + '..', + '..', + 'server', + 'views', + 'emails', + 'user-request-sign-in.ejs' +)); + function getAboutProfile({ username, githubProfile: github, @@ -513,7 +533,7 @@ module.exports = function(User) { email: email, emailVerified: false }; - return User.findOrCreate$({ where: { email: userObj.email }}, userObj) + return User.findOrCreate$({ where: { email }}, userObj) .map(([ err, user, isCreated ]) => { if (err) { return dedent` @@ -532,10 +552,8 @@ module.exports = function(User) { `; } - let emailTemplate = 'user-request-sign-in.ejs'; - if (isCreated) { - emailTemplate = 'user-request-sign-up.ejs'; - } + const renderAuthEmail = isCreated ? + renderSignUpEmail : renderSignInEmail; // create a temporary access token with ttl for 1 hour user.createAccessToken({ ttl: 60 * 60 * 1000 }, (err, token) => { @@ -543,15 +561,7 @@ module.exports = function(User) { const { id: loginToken } = token; const loginEmail = user.email; - const renderAuthEmail = loopback.template(path.join( - __dirname, - '..', - '..', - 'server', - 'views', - 'emails', - emailTemplate - )); + const mailOptions = { type: 'email', to: user.email, From fd442c55bb6006268de16dc4eaabf3a31ab51c4c Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Mon, 28 Nov 2016 22:56:07 +0530 Subject: [PATCH 06/21] Remove error catch and use update method --- common/models/user.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 83a23eaae3..1cc5081ea0 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -556,7 +556,7 @@ module.exports = function(User) { renderSignUpEmail : renderSignInEmail; // create a temporary access token with ttl for 1 hour - user.createAccessToken({ ttl: 60 * 60 * 1000 }, (err, token) => { + return user.createAccessToken({ ttl: 60 * 60 * 1000 }, (err, token) => { if (err) { throw err; } const { id: loginToken } = token; @@ -579,14 +579,19 @@ module.exports = function(User) { } else { console.log('~~~~\n' + mailOptions.text + '~~~~\n'); } - user.emailAuthLinkTTL = token.created; - user.save(err =>{ if (err) { throw err; }}); - }); + const emailAuthLinkTTL = token.created; + this.update$({ + emailAuthLinkTTL + }) + .do(() => { + this.emailAuthLinkTTL = emailAuthLinkTTL; + }); - return dedent` - If you entered a valid email, a magic link is on its way. - Please follow that link to sign in. - `; + return dedent` + If you entered a valid email, a magic link is on its way. + Please follow that link to sign in. + `; + }); }) .map((msg) => { if (msg) { return msg; } @@ -594,12 +599,6 @@ module.exports = function(User) { Oops, something is not right, please try again later. `; }) - .catch(error => { - debug(error); - return Observable.throw( - 'Oops, something went wrong, please try again later.' - ); - }) .toPromise(); }; From e941545d2e165a35dfdd189208b2e586773309f8 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Wed, 30 Nov 2016 23:52:11 +0530 Subject: [PATCH 07/21] Unify signin and signup with lesser clicks --- server/views/account/deprecated-signin.jade | 5 ++++- server/views/account/email-signin.jade | 12 +++++++++--- server/views/account/signin.jade | 12 ++++++------ server/views/partials/navbar.jade | 4 ++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/server/views/account/deprecated-signin.jade b/server/views/account/deprecated-signin.jade index 5401b419e3..648dbf4649 100644 --- a/server/views/account/deprecated-signin.jade +++ b/server/views/account/deprecated-signin.jade @@ -15,7 +15,10 @@ block content a.btn.btn-lg.btn-block.btn-social.btn-twitter(href='/auth/twitter') i.fa.fa-twitter | Sign in with Twitter - + br + p + a(href="/signin") Or click here to go back. + script. $(document).ready(function() { var method = localStorage.getItem('lastSigninMethodDeprecated'), diff --git a/server/views/account/email-signin.jade b/server/views/account/email-signin.jade index 1616b64a0a..1b78fe6809 100644 --- a/server/views/account/email-signin.jade +++ b/server/views/account/email-signin.jade @@ -10,8 +10,8 @@ block content #flash-content .row .text-center - h2 Sign in with your Email here: - .button-spacer + h2 Sign in with an Email here: + .button-spacer .col-sm-6.col-sm-offset-3 form(method='POST', action='/passwordless-auth') input(type='hidden', name='_csrf', value=_csrf) @@ -25,7 +25,13 @@ block content .col-sm-6.col-sm-offset-3 br p.text-center - a(href="/signup") Or Click here if you want to sign up. + | Free Code Camp uses passwordless authentication. + br + | If you are a new camper use any email, and sign up instantly, + | or use your existing email with us, if you already have an account. + br + p.text-center + a(href="/signin") Or click here if you want to sign in with other options. script. $(document).ready(function() { diff --git a/server/views/account/signin.jade b/server/views/account/signin.jade index 0b1123cf89..44283ca74f 100644 --- a/server/views/account/signin.jade +++ b/server/views/account/signin.jade @@ -1,19 +1,19 @@ extends ../layout block content .text-center - h2 Are you a returning camper? + h2 Welcome to Free Code Camp. br .button-spacer - | Sign in with one of these options: + | Sign in or Sign up with one of these options: a.btn.btn-lg.btn-block.btn-social.btn-primary(href='/email-signin') i.fa.fa-envelope - | Sign in with Email + | Continue with Email a.btn.btn-lg.btn-block.btn-social.btn-github(href='/auth/github') i.fa.fa-github - | Sign in with GitHub + | Continue with GitHub br p - a(href="/deprecated-signin") Click here if you previously signed in using a different method. + a(href="/deprecated-signin") Or click here if you previously signed up using a different method. script. $(document).ready(function() { @@ -43,7 +43,7 @@ block content $(this).removeClass('active'); obj.methodClass = $(this).attr('class').split(' ').pop(); obj.method = $(this).text(); - if(obj.method === "Sign in with Email" || obj.method === "Sign in with GitHub") { + if(obj.method === "Continue with Email" || obj.method === "Continue in with GitHub") { localStorage.setItem('lastSigninMethod', JSON.stringify(obj)); } else { localStorage.removeItem('lastSigninMethod'); diff --git a/server/views/partials/navbar.jade b/server/views/partials/navbar.jade index dd38b4d40d..a71f23ca24 100644 --- a/server/views/partials/navbar.jade +++ b/server/views/partials/navbar.jade @@ -28,12 +28,12 @@ nav.navbar.navbar-default.navbar-static-top.nav-height a(href='https://www.freecodecamp.org/donate') Donate if !user li - a(href='/signup') Sign Up + a(href='/signin') Sign In else li.avatar-points a(href='/settings') span.brownie-points-nav - span.hidden-md.hidden-lg #{user.username} + span.hidden-md.hidden-lg #{user.username} span.brownie-points [ #{user.points} ] span.hidden-xs.hidden-sm.avatar img.profile-picture.float-right(src='#{user.picture}') From 239313cb0b550ee3ddb10c56b3b33732e756ffaf Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Fri, 16 Dec 2016 00:32:07 +0530 Subject: [PATCH 08/21] Remove sign up page and update button styles --- client/less/main.less | 3 +- common/models/user.js | 32 ++++++++++++ server/boot/user.js | 12 +++-- server/views/account/email-signin.jade | 2 +- server/views/account/email-signup.jade | 68 -------------------------- 5 files changed, 43 insertions(+), 74 deletions(-) delete mode 100644 server/views/account/email-signup.jade diff --git a/client/less/main.less b/client/less/main.less index 6cf56077b9..060e1c0a62 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -266,7 +266,8 @@ h1, h2, h3, h4, h5, h6, p, li { } .btn-social { - width: 250px; + width: 100%; + max-width: 260px; margin: auto; } diff --git a/common/models/user.js b/common/models/user.js index 1cc5081ea0..b92b766f74 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -323,6 +323,38 @@ module.exports = function(User) { return ctx.res.redirect(redirect); }); + User.beforeRemote('create', function({ req, res }, _, next) { + req.body.username = 'fcc' + uuid.v4().slice(0, 8); + if (!req.body.email) { + return next(); + } + if (!isEmail(req.body.email)) { + return next(new Error('Email format is not valid')); + } + return User.doesExist(null, req.body.email) + .then(exists => { + if (!exists) { + return next(); + } + + req.flash('error', { + msg: dedent` + The ${req.body.email} email address is already associated with an account. + Try signing in with it here instead. + ` + }); + + return res.redirect('/email-signin'); + }) + .catch(err => { + console.error(err); + req.flash('error', { + msg: 'Oops, something went wrong, please try again later' + }); + return res.redirect('/email-signin'); + }); + }); + User.on('resetPasswordRequest', function(info) { if (!isEmail(info.email)) { console.error(createEmailError()); diff --git a/server/boot/user.js b/server/boot/user.js index 461ca49386..11f1fdf671 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -25,6 +25,7 @@ import { import supportedLanguages from '../../common/utils/supported-languages'; import { getChallengeInfo, cachedMap } from '../utils/map'; +const isSignUpDisabled = !!process.env.DISABLE_SIGNUP; const debug = debugFactory('fcc:boot:user'); const sendNonUserToMap = ifNoUserRedirectTo('/map'); const certIds = { @@ -170,14 +171,13 @@ module.exports = function(app) { router.get('/logout', function(req, res) { res.redirect(301, '/signout'); }); - router.get('/signup', getEmailSignup); + router.get('/signup', getSignin); router.get('/signin', getSignin); router.get('/signout', signout); router.get('/forgot', getForgot); api.post('/forgot', postForgot); router.get('/reset-password', getReset); api.post('/reset-password', postReset); - router.get('/email-signup', getEmailSignup); router.get('/email-signin', getEmailSignin); router.get('/deprecated-signin', getDepSignin); router.get('/update-email', getUpdateEmail); @@ -438,12 +438,16 @@ module.exports = function(app) { if (req.user) { return res.redirect('/'); } + if (isSignUpDisabled) { + return res.render('account/beta', { + title: 'New sign ups are disabled' + }); + } return res.render('account/email-signin', { title: 'Sign in to freeCodeCamp using your Email Address' }); } - - const isSignUpDisabled = !!process.env.DISABLE_SIGNUP; + function getEmailSignup(req, res) { if (req.user) { return res.redirect('/'); diff --git a/server/views/account/email-signin.jade b/server/views/account/email-signin.jade index 1b78fe6809..7f49c73dfb 100644 --- a/server/views/account/email-signin.jade +++ b/server/views/account/email-signin.jade @@ -10,7 +10,7 @@ block content #flash-content .row .text-center - h2 Sign in with an Email here: + h2 Sign in or Sign Up with an Email here: .button-spacer .col-sm-6.col-sm-offset-3 form(method='POST', action='/passwordless-auth') diff --git a/server/views/account/email-signup.jade b/server/views/account/email-signup.jade deleted file mode 100644 index b431d99283..0000000000 --- a/server/views/account/email-signup.jade +++ /dev/null @@ -1,68 +0,0 @@ -extends ../layout -block content - .container - .col-xs-12 - .row - .col-sm-6.col-sm-offset-3.flashMessage.negative-30 - #flash-board.alert.fade.in(style='display: none;') - button.close(type='button', data-dismiss='alert') - span.ion-close-circled#flash-close - #flash-content - .row - .text-center - h2 Are you new to Free Code Camp? - br - .button-spacer - | Sign up with an Email here: - .button-spacer - .col-sm-6.col-sm-offset-3 - 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) - .button-spacer - button.btn.btn-primary.btn-lg.btn-block(type='submit') - span.fa.fa-envelope - | Get a magic link to sign up. - .row - .col-sm-6.col-sm-offset-3 - br - p.text-center - a(href="/signin") Click here if you already have an account and want to sign in. - - 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.error && data.error.message) - $('#flash-content').html(data.error.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(); - } - }); - }); - }); From b32e2102acdd6385297cd8eb44cd1eb89647ea59 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Fri, 16 Dec 2016 10:35:38 +0530 Subject: [PATCH 09/21] Remove reset-password logic --- common/models/user.js | 3 ++ server/boot/user.js | 73 +------------------------------- server/views/account/forgot.jade | 14 ------ server/views/account/reset.jade | 17 -------- 4 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 server/views/account/forgot.jade delete mode 100644 server/views/account/reset.jade diff --git a/common/models/user.js b/common/models/user.js index b92b766f74..3fe90c9a02 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -355,6 +355,7 @@ module.exports = function(User) { }); }); +<<<<<<< HEAD User.on('resetPasswordRequest', function(info) { if (!isEmail(info.email)) { console.error(createEmailError()); @@ -398,6 +399,8 @@ module.exports = function(User) { }); }); +======= +>>>>>>> Remove reset-password logic User.beforeRemote('login', function(ctx, notUsed, next) { const { body } = ctx.req; if (body && typeof body.email === 'string') { diff --git a/server/boot/user.js b/server/boot/user.js index 11f1fdf671..f27762b893 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -174,10 +174,6 @@ module.exports = function(app) { router.get('/signup', getSignin); router.get('/signin', getSignin); router.get('/signout', signout); - router.get('/forgot', getForgot); - api.post('/forgot', postForgot); - router.get('/reset-password', getReset); - api.post('/reset-password', postReset); router.get('/email-signin', getEmailSignin); router.get('/deprecated-signin', getDepSignin); router.get('/update-email', getUpdateEmail); @@ -749,74 +745,6 @@ module.exports = function(app) { }); } - function getReset(req, res) { - if (!req.accessToken) { - req.flash('errors', { msg: 'access token invalid' }); - return res.render('account/forgot'); - } - return res.render('account/reset', { - title: 'Reset your Password', - accessToken: req.accessToken.id - }); - } - - function postReset(req, res, next) { - const errors = req.validationErrors(); - const { password } = req.body; - - if (errors) { - req.flash('errors', errors); - return res.redirect('back'); - } - - return User.findById(req.accessToken.userId, function(err, user) { - if (err) { return next(err); } - return user.updateAttribute('password', password, function(err) { - if (err) { return next(err); } - - debug('password reset processed successfully'); - req.flash('info', { msg: 'You\'ve successfully reset your password.' }); - return res.redirect('/'); - }); - }); - } - - function getForgot(req, res) { - if (req.isAuthenticated()) { - return res.redirect('/'); - } - return res.render('account/forgot', { - title: 'Forgot Password' - }); - } - - function postForgot(req, res) { - req.validate('email', 'Email format is not valid').isEmail(); - const errors = req.validationErrors(); - const email = req.body.email.toLowerCase(); - - if (errors) { - req.flash('errors', errors); - return res.redirect('/forgot'); - } - - return User.resetPassword({ - email: email - }, function(err) { - if (err) { - req.flash('errors', err.message); - return res.redirect('/forgot'); - } - - req.flash('info', { - msg: 'An e-mail has been sent to ' + - email + - ' with further instructions.' - }); - return res.render('account/forgot'); - }); - } - function getReportUserProfile(req, res) { const username = req.params.username.toLowerCase(); return res.render('account/report-profile', { @@ -867,4 +795,5 @@ module.exports = function(app) { return res.redirect('/'); }); } + }; diff --git a/server/views/account/forgot.jade b/server/views/account/forgot.jade deleted file mode 100644 index 4b1e94c606..0000000000 --- a/server/views/account/forgot.jade +++ /dev/null @@ -1,14 +0,0 @@ -extends ../layout -block content - .col-sm-6.col-sm-offset-3 - form(method='POST', action="/forgot") - h2.text-center Forgot Password Reset - input(type='hidden', name='_csrf', value=_csrf) - .form-group - p.large-p Enter your email address. We'll send you password reset instructions. - input.form-control.input-lg(type='email', name='email', id='email', placeholder='Email', autofocus=true required) - .form-group - button.btn.btn-primary.btn-lg.btn-block(type='submit') - i.fa.fa-key - | Reset Password - diff --git a/server/views/account/reset.jade b/server/views/account/reset.jade deleted file mode 100644 index c82eeb1cca..0000000000 --- a/server/views/account/reset.jade +++ /dev/null @@ -1,17 +0,0 @@ -extends ../layout - -block content - .col-sm-8.col-sm-offset-2.jumbotron - form(action='/reset-password?access_token=#{accessToken}', method='POST') - h1 Reset Password - input(type='hidden', name='_csrf', value=_csrf) - .form-group - label(for='password') New Password - input.form-control(type='password', name='password', value='', placeholder='New password', autofocus=true) - .form-group - label(for='confirm') Confirm Password - input.form-control(type='password', name='confirm', value='', placeholder='Confirm password') - .form-group - button.btn.btn-primary.btn-reset(type='submit') - i.fa.fa-keyboard-o - | Change Password From 7f657f7bd5b26d97c32dd5fbd6691fb5740e1c09 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Sat, 17 Dec 2016 01:44:06 +0530 Subject: [PATCH 10/21] Remove non-react Update email views and routes --- server/boot/user.js | 23 ++--------- server/views/account/update-email.jade | 57 -------------------------- 2 files changed, 3 insertions(+), 77 deletions(-) delete mode 100644 server/views/account/update-email.jade diff --git a/server/boot/user.js b/server/boot/user.js index f27762b893..53a334ef96 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -140,16 +140,9 @@ function buildDisplayChallenges( module.exports = function(app) { const router = app.loopback.Router(); const api = app.loopback.Router(); -<<<<<<< HEAD - const { User, Email } = app.models; + const { AccessToken, Email, User } = 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, @@ -176,7 +169,6 @@ module.exports = function(app) { router.get('/signout', signout); router.get('/email-signin', getEmailSignin); router.get('/deprecated-signin', getDepSignin); - router.get('/update-email', getUpdateEmail); router.get('/passwordless-auth', invalidateAuthToken, getPasswordlessAuth); api.post('/passwordless-auth', postPasswordlessAuth); router.get( @@ -421,15 +413,6 @@ module.exports = function(app) { }); } - function getUpdateEmail(req, res) { - if (!req.user) { - return res.redirect('/'); - } - return res.render('account/update-email', { - title: 'Update your Email' - }); - } - function getEmailSignin(req, res) { if (req.user) { return res.redirect('/'); @@ -443,7 +426,7 @@ module.exports = function(app) { title: 'Sign in to freeCodeCamp using your Email Address' }); } - + function getEmailSignup(req, res) { if (req.user) { return res.redirect('/'); diff --git a/server/views/account/update-email.jade b/server/views/account/update-email.jade deleted file mode 100644 index cb45c9d937..0000000000 --- a/server/views/account/update-email.jade +++ /dev/null @@ -1,57 +0,0 @@ -extends ../layout -block content - .container - .row.flashMessage.negative-30 - .col-xs-12 - #flash-board.alert.fade.in(style='display: none;') - button.close(type='button', data-dismiss='alert') - span.ion-close-circled#flash-close - #flash-content - h2.text-center Update your email address here: - form.form-horizontal.update-email(method='POST', action='/api/users/#{user.id}/update-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='/settings') - | Go back to Settings - - 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.error && data.error.message) - $('#flash-content').html(data.error.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(); - } - }); - }); - }); From f3804bb553c535bcc49270a564a9540aee82378e Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Sat, 14 Jan 2017 18:30:20 +0530 Subject: [PATCH 11/21] chore(passwordless): Update to freeCodeCamp --- common/models/user.js | 2 +- server/views/account/email-signin.jade | 4 ++-- server/views/account/signin.jade | 2 +- server/views/emails/user-request-sign-in.ejs | 4 ++-- server/views/emails/user-request-sign-up.ejs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 3fe90c9a02..c32786942e 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -601,7 +601,7 @@ module.exports = function(User) { type: 'email', to: user.email, from: 'Team@freecodecamp.com', - subject: 'Free Code Camp - Authentication Request!', + subject: 'freeCodeCamp - Authentication Request!', text: renderAuthEmail({ loginEmail, loginToken diff --git a/server/views/account/email-signin.jade b/server/views/account/email-signin.jade index 7f49c73dfb..b5386d40d8 100644 --- a/server/views/account/email-signin.jade +++ b/server/views/account/email-signin.jade @@ -25,9 +25,9 @@ block content .col-sm-6.col-sm-offset-3 br p.text-center - | Free Code Camp uses passwordless authentication. + | freeCodeCamp uses passwordless authentication. br - | If you are a new camper use any email, and sign up instantly, + | If you are a new camper just use any email, and sign up instantly, | or use your existing email with us, if you already have an account. br p.text-center diff --git a/server/views/account/signin.jade b/server/views/account/signin.jade index 44283ca74f..b73e8b8271 100644 --- a/server/views/account/signin.jade +++ b/server/views/account/signin.jade @@ -1,7 +1,7 @@ extends ../layout block content .text-center - h2 Welcome to Free Code Camp. + h2 Welcome to freeCodeCamp! br .button-spacer | Sign in or Sign up with one of these options: diff --git a/server/views/emails/user-request-sign-in.ejs b/server/views/emails/user-request-sign-in.ejs index 74967a86cb..bdd9e5c921 100644 --- a/server/views/emails/user-request-sign-in.ejs +++ b/server/views/emails/user-request-sign-in.ejs @@ -1,6 +1,6 @@ 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 freeCodeCamp, without a password. Just follow the link below: https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> @@ -11,5 +11,5 @@ Do not share this email with anyone, doing so may give them access to your accou Good luck with the challenges! Thanks, -The Free Code Camp Team. +The freeCodeCamp Team. team@freecodecamp.com diff --git a/server/views/emails/user-request-sign-up.ejs b/server/views/emails/user-request-sign-up.ejs index 19b9468968..be92535b80 100644 --- a/server/views/emails/user-request-sign-up.ejs +++ b/server/views/emails/user-request-sign-up.ejs @@ -1,6 +1,6 @@ Greetings from San Francisco! -Welcome to Free Code Camp. We've created a new account for you. +Welcome to freeCodeCamp. We've created a new account for you. To verify and start using your profile just follow the link below: https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> @@ -18,5 +18,5 @@ Do not share this email with anyone, doing so may give them access to your accou Good luck with the challenges! Thanks, -The Free Code Camp Team. +The freeCodeCamp Team. team@freecodecamp.com From 5c56a1e78cbad925f3605e09410224ca2d24f04c Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Tue, 7 Feb 2017 01:33:09 +0530 Subject: [PATCH 12/21] Update email views and notifications --- common/models/user.js | 24 ++++++++++---------- server/boot/user.js | 2 +- server/views/emails/user-request-sign-in.ejs | 10 ++++---- server/views/emails/user-request-sign-up.ejs | 14 +++++++----- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index c32786942e..11e90d2c46 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -570,11 +570,7 @@ module.exports = function(User) { }; return User.findOrCreate$({ where: { email }}, userObj) .map(([ err, user, isCreated ]) => { - if (err) { - return dedent` - Oops, something is not right, please try again later. - `; - } + if (err) { throw err; } const minutesLeft = getWaitPeriod(user.emailAuthLinkTTL); if (minutesLeft) { @@ -596,13 +592,15 @@ module.exports = function(User) { const { id: loginToken } = token; const loginEmail = user.email; - + const host = isDev ? + 'http://localhost:3000' : 'https://freecodecamp.com'; const mailOptions = { type: 'email', to: user.email, from: 'Team@freecodecamp.com', subject: 'freeCodeCamp - Authentication Request!', text: renderAuthEmail({ + host, loginEmail, loginToken }) @@ -622,14 +620,16 @@ module.exports = function(User) { this.emailAuthLinkTTL = emailAuthLinkTTL; }); - return dedent` - If you entered a valid email, a magic link is on its way. - Please follow that link to sign in. - `; }); }) - .map((msg) => { - if (msg) { return msg; } + .map(() => { + return dedent` + If you entered a valid email, a magic link is on its way. + Please follow that link to sign in. + `; + }) + .catch(err => { + if (err) { debug(err); } return dedent` Oops, something is not right, please try again later. `; diff --git a/server/boot/user.js b/server/boot/user.js index 53a334ef96..9d1f16c39b 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -258,7 +258,7 @@ module.exports = function(app) { return res.redirect('/'); } - return User.requestAuthLink(req.body.email, 'user-request-sign-in.ejs') + return User.requestAuthLink(req.body.email) .then(msg => { return res.status(200).send({ message: msg }); }) diff --git a/server/views/emails/user-request-sign-in.ejs b/server/views/emails/user-request-sign-in.ejs index bdd9e5c921..e5cec71ae7 100644 --- a/server/views/emails/user-request-sign-in.ejs +++ b/server/views/emails/user-request-sign-in.ejs @@ -1,12 +1,14 @@ Greetings from San Francisco! -You can now sign in to freeCodeCamp, without a password. Just follow the link below: +Please follow the link below, and sign in to freeCodeCamp instantly: -https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> + +This above link is valid for 15 minutes. IMPORTANT NOTE: -You can simply delete or ignore this email, if you did not make any such request. -Do not share this email with anyone, doing so may give them access to your account. +If you did not make any such request, simply delete or ignore this email. +Do not share this email with anyone, doing so will give them access to your account. Good luck with the challenges! diff --git a/server/views/emails/user-request-sign-up.ejs b/server/views/emails/user-request-sign-up.ejs index be92535b80..514b81ae39 100644 --- a/server/views/emails/user-request-sign-up.ejs +++ b/server/views/emails/user-request-sign-up.ejs @@ -1,19 +1,21 @@ Greetings from San Francisco! Welcome to freeCodeCamp. We've created a new account for you. -To verify and start using your profile just follow the link below: +Please verify and start using your profile by following the link below: -https://freecodecamp.com/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> -Next steps: -1. Visit the settings page and link your account to Github. +This above link is valid for 15 minutes. + +And when you have a moment: +1. Visit the settings page and link your account to GitHub. 2. Follow our Medium Publication: https://medium.freecodecamp.com 3. Checkout our forum: https://forum.freecodecamp.com 4. Join the conversation: https://gitter.im/FreeCodeCamp/FreeCodeCamp IMPORTANT NOTE: -You can simply delete or ignore this email, if you did not make any such request. -Do not share this email with anyone, doing so may give them access to your account. +If you did not make any such request, simply delete or ignore this email. +Do not share this email with anyone, doing so will give them access to your account. Good luck with the challenges! From 0ce30409c07e69f45c6b4d0a173cb2250cb01339 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Sat, 22 Apr 2017 21:25:43 +0530 Subject: [PATCH 13/21] refactor(code): cleanup and rebase --- common/models/user.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 11e90d2c46..b92b766f74 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -355,7 +355,6 @@ module.exports = function(User) { }); }); -<<<<<<< HEAD User.on('resetPasswordRequest', function(info) { if (!isEmail(info.email)) { console.error(createEmailError()); @@ -399,8 +398,6 @@ module.exports = function(User) { }); }); -======= ->>>>>>> Remove reset-password logic User.beforeRemote('login', function(ctx, notUsed, next) { const { body } = ctx.req; if (body && typeof body.email === 'string') { @@ -570,7 +567,11 @@ module.exports = function(User) { }; return User.findOrCreate$({ where: { email }}, userObj) .map(([ err, user, isCreated ]) => { - if (err) { throw err; } + if (err) { + return dedent` + Oops, something is not right, please try again later. + `; + } const minutesLeft = getWaitPeriod(user.emailAuthLinkTTL); if (minutesLeft) { @@ -592,15 +593,13 @@ module.exports = function(User) { const { id: loginToken } = token; const loginEmail = user.email; - const host = isDev ? - 'http://localhost:3000' : 'https://freecodecamp.com'; + const mailOptions = { type: 'email', to: user.email, from: 'Team@freecodecamp.com', - subject: 'freeCodeCamp - Authentication Request!', + subject: 'Free Code Camp - Authentication Request!', text: renderAuthEmail({ - host, loginEmail, loginToken }) @@ -620,16 +619,14 @@ module.exports = function(User) { this.emailAuthLinkTTL = emailAuthLinkTTL; }); + return dedent` + If you entered a valid email, a magic link is on its way. + Please follow that link to sign in. + `; }); }) - .map(() => { - return dedent` - If you entered a valid email, a magic link is on its way. - Please follow that link to sign in. - `; - }) - .catch(err => { - if (err) { debug(err); } + .map((msg) => { + if (msg) { return msg; } return dedent` Oops, something is not right, please try again later. `; From a426ff7ef94721b5bd710cddec4fa2c0e6b30a9f Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Sun, 23 Apr 2017 14:18:33 +0530 Subject: [PATCH 14/21] fix(email): allow sender email var for development Allows setting the SES_ID, SES_SECRET and EMAIL_SENDER values --- common/models/user.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index b92b766f74..b7a3ceb81a 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -597,20 +597,23 @@ module.exports = function(User) { const mailOptions = { type: 'email', to: user.email, +<<<<<<< HEAD from: 'Team@freecodecamp.com', subject: 'Free Code Camp - Authentication Request!', +======= + from: isDev ? + process.env.EMAIL_SENDER : 'team@freecodecamp.com', + subject: 'freeCodeCamp - Authentication Request!', +>>>>>>> fix(email): allow sender email var for development text: renderAuthEmail({ loginEmail, loginToken }) }; - if (!isDev) { - this.email.send(mailOptions, err =>{ - if (err) { throw err; } - }); - } else { - console.log('~~~~\n' + mailOptions.text + '~~~~\n'); - } + + this.email.send(mailOptions, err =>{ + if (err) { throw err; } + }); const emailAuthLinkTTL = token.created; this.update$({ emailAuthLinkTTL From 3f332fc1f0f83efde2cb26d9b5c496b0f2de924e Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Mon, 24 Apr 2017 00:37:10 +0530 Subject: [PATCH 15/21] fix: Add error handling and refactoring of Observable methods --- common/models/user.js | 54 ++++++++++++++++++++----------------------- server/boot/user.js | 6 +---- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index b7a3ceb81a..5e68250197 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -107,7 +107,10 @@ 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.findOrCreate$ = Observable.fromNodeCallback(User.findOrCreate, User); + User.prototype.createAccessToken$ = Observable.fromNodeCallback( + User.prototype.createAccessToken + ); }); User.beforeRemote('create', function({ req }) { @@ -566,30 +569,25 @@ module.exports = function(User) { emailVerified: false }; return User.findOrCreate$({ where: { email }}, userObj) - .map(([ err, user, isCreated ]) => { - if (err) { - return dedent` - Oops, something is not right, please try again later. - `; - } + .flatMap(([ user, isCreated ]) => { const minutesLeft = getWaitPeriod(user.emailAuthLinkTTL); - if (minutesLeft) { + if (minutesLeft > 0) { const timeToWait = minutesLeft ? `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : 'a few seconds'; debug('request before wait time : ' + timeToWait); - return dedent` + return Observable.of(dedent` Please wait ${timeToWait} to resend an authentication link. - `; + `); } const renderAuthEmail = isCreated ? renderSignUpEmail : renderSignInEmail; - // create a temporary access token with ttl for 1 hour - return user.createAccessToken({ ttl: 60 * 60 * 1000 }, (err, token) => { - if (err) { throw err; } + // create a temporary access token with ttl for 15 minutes + return user.createAccessToken$({ ttl: 15 * 60 * 1000 }) + .flatMap(token => { const { id: loginToken } = token; const loginEmail = user.email; @@ -597,29 +595,27 @@ module.exports = function(User) { const mailOptions = { type: 'email', to: user.email, -<<<<<<< HEAD - from: 'Team@freecodecamp.com', - subject: 'Free Code Camp - Authentication Request!', -======= from: isDev ? process.env.EMAIL_SENDER : 'team@freecodecamp.com', subject: 'freeCodeCamp - Authentication Request!', ->>>>>>> fix(email): allow sender email var for development text: renderAuthEmail({ loginEmail, loginToken }) }; - this.email.send(mailOptions, err =>{ - if (err) { throw err; } - }); - const emailAuthLinkTTL = token.created; - this.update$({ - emailAuthLinkTTL - }) - .do(() => { - this.emailAuthLinkTTL = emailAuthLinkTTL; + return this.email.send$(mailOptions) + .flatMap(() => { + const emailAuthLinkTTL = token.created; + return this.update$({ + emailAuthLinkTTL + }) + .map(() => { + return dedent` + If you entered a valid email, a magic link is on its way. + Please follow that link to sign in. + `; + }); }); return dedent` @@ -628,8 +624,8 @@ module.exports = function(User) { `; }); }) - .map((msg) => { - if (msg) { return msg; } + .catch(err => { + if (err) { debug(err); } return dedent` Oops, something is not right, please try again later. `; diff --git a/server/boot/user.js b/server/boot/user.js index 9d1f16c39b..b0e108510e 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -290,15 +290,11 @@ module.exports = function(app) { const userId = authToken.userId; return User.findById(userId, (err, user) => { - if (err) { + if (err || !user || user.email !== authEmailId) { debug(err); req.flash('info', { msg: defaultErrorMsg }); return res.redirect('/email-signin'); } - if (user.email !== authEmailId) { - req.flash('info', { msg: defaultErrorMsg }); - return res.redirect('/email-signin'); - } return authToken.validate((err, isValid) => { if (err) { throw err; } if (!isValid) { From d313ab68e4c12e3c26a4944670870ab176a28a50 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Mon, 24 Apr 2017 13:06:17 +0530 Subject: [PATCH 16/21] fix(email): Error messages for updateEmail API --- common/models/user.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 5e68250197..9e3beb9862 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -15,6 +15,7 @@ const debug = debugFactory('fcc:user:remote'); const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; const isDev = process.env.NODE_ENV !== 'production'; const devHost = process.env.HOST || 'localhost'; +const emailSender = process.env.EMAIL_SENDER || 'team@freecodecamp.org'; const createEmailError = () => new Error( 'Please check to make sure the email is a valid email address.' @@ -595,8 +596,7 @@ module.exports = function(User) { const mailOptions = { type: 'email', to: user.email, - from: isDev ? - process.env.EMAIL_SENDER : 'team@freecodecamp.com', + from: emailSender, subject: 'freeCodeCamp - Authentication Request!', text: renderAuthEmail({ loginEmail, @@ -662,14 +662,14 @@ module.exports = function(User) { } const minutesLeft = getWaitPeriod(this.emailVerifyTTL); - if (ownEmail && minutesLeft) { + if (ownEmail && minutesLeft > 0) { 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.of(dedent` + Please wait ${timeToWait} to resend an authentication link. + `); } return Observable.fromPromise(User.doesExist(null, email)) @@ -697,8 +697,8 @@ module.exports = function(User) { const mailOptions = { type: 'email', to: email, - from: 'team@freecodecamp.org', - subject: 'Welcome to freeCodeCamp!', + from: emailSender, + subject: 'freeCodeCamp - Email Update Requested', protocol: isDev ? null : 'https', host: isDev ? devHost : 'freecodecamp.org', port: isDev ? null : 443, From 2321f7b32620fa5c65e28ba8845fe81ca3e26b5f Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Thu, 27 Apr 2017 01:54:56 +0530 Subject: [PATCH 17/21] feat(server): Add URL utils to compute server URL, HOST, PORT, etc. --- common/models/user.js | 22 +++++++++++++--------- server/utils/url-utils.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 server/utils/url-utils.js diff --git a/common/models/user.js b/common/models/user.js index 9e3beb9862..ffdf8b288a 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -10,12 +10,16 @@ import loopback from 'loopback'; import { saveUser, observeMethod } from '../../server/utils/rx.js'; import { blacklistedUsernames } from '../../server/utils/constants.js'; import { wrapHandledError } from '../../server/utils/create-handled-error.js'; +import { + getServerFullURL, + getEmailSender, + getProtocol, + getHost, + getPort +} from '../../server/utils/url-utils.js'; const debug = debugFactory('fcc:user:remote'); const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; -const isDev = process.env.NODE_ENV !== 'production'; -const devHost = process.env.HOST || 'localhost'; -const emailSender = process.env.EMAIL_SENDER || 'team@freecodecamp.org'; const createEmailError = () => new Error( 'Please check to make sure the email is a valid email address.' @@ -592,11 +596,11 @@ module.exports = function(User) { const { id: loginToken } = token; const loginEmail = user.email; - + const host = getServerFullURL(); const mailOptions = { type: 'email', to: user.email, - from: emailSender, + from: getEmailSender(), subject: 'freeCodeCamp - Authentication Request!', text: renderAuthEmail({ loginEmail, @@ -697,11 +701,11 @@ module.exports = function(User) { const mailOptions = { type: 'email', to: email, - from: emailSender, + from: getEmailSender(), subject: 'freeCodeCamp - Email Update Requested', - protocol: isDev ? null : 'https', - host: isDev ? devHost : 'freecodecamp.org', - port: isDev ? null : 443, + protocol: getProtocol(), + host: getHost(), + port: getPort(), template: path.join( __dirname, '..', diff --git a/server/utils/url-utils.js b/server/utils/url-utils.js new file mode 100644 index 0000000000..df65a80fcb --- /dev/null +++ b/server/utils/url-utils.js @@ -0,0 +1,37 @@ +const isDev = process.env.NODE_ENV !== 'production'; +const isBeta = !!process.env.BETA; + +export function getEmailSender() { + return process.env.EMAIL_SENDER || 'team@freecodecamp.com'; +} + +export function getPort() { + if (!isDev) { + return '443'; + } + return process.env.SYNC_PORT || '3000'; +} + +export function getProtocol() { + return isDev ? 'http' : 'https'; +} + +export function getHost() { + if (isDev) { + return process.env.HOST || 'localhost'; + } + return isBeta ? 'beta.freecodecamp.com' : 'freecodecamp.com'; +} + +export function getServerFullURL() { + if (!isDev) { + return getProtocol() + + '://' + + getHost(); + } + return getProtocol() + + '://' + + getHost() + + ':' + + getPort(); +} From 5adff6a292b0569c9053d2bfa2e08f5d3e1a9795 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Thu, 27 Apr 2017 12:11:06 +0530 Subject: [PATCH 18/21] fix(email): Add a trailing slash to avoid redirects --- server/views/emails/user-request-sign-in.ejs | 2 +- server/views/emails/user-request-sign-up.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/views/emails/user-request-sign-in.ejs b/server/views/emails/user-request-sign-in.ejs index e5cec71ae7..866a77c96b 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! Please follow the link below, and sign in to freeCodeCamp instantly: -<%= host %>/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> This above link is valid for 15 minutes. diff --git a/server/views/emails/user-request-sign-up.ejs b/server/views/emails/user-request-sign-up.ejs index 514b81ae39..fadeccf4bc 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 freeCodeCamp. We've created a new account for you. Please verify and start using your profile by following the link below: -<%= host %>/passwordless-auth?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> This above link is valid for 15 minutes. From 4ae52983f63b2df6379cd405827f9aa7bd7e858c Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Sun, 4 Jun 2017 10:40:12 +0530 Subject: [PATCH 19/21] fix(email-auth): add disable and loading indication to button --- server/views/account/email-signin.jade | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/server/views/account/email-signin.jade b/server/views/account/email-signin.jade index b5386d40d8..b90ffee1fa 100644 --- a/server/views/account/email-signin.jade +++ b/server/views/account/email-signin.jade @@ -16,9 +16,9 @@ block content 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) + input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true, required) .button-spacer - button.btn.btn-primary.btn-lg.btn-block(type='submit') + button#magic-btn.btn.btn-primary.btn-lg.btn-block(type='submit') span.fa.fa-envelope | Get a magic link to sign in. .row @@ -27,17 +27,31 @@ block content p.text-center | freeCodeCamp uses passwordless authentication. br - | If you are a new camper just use any email, and sign up instantly, - | or use your existing email with us, if you already have an account. + | Sign up instantly, using a valid email address, or Sign in + | using your existing email with us, if you already have an account. br p.text-center a(href="/signin") Or click here if you want to sign in with other options. script. $(document).ready(function() { + + function disableMagicButton (isDisabled) { + if (isDisabled) { + $('#magic-btn') + .html('') + .prop('disabled', true); + } else { + $('#magic-btn') + .html('Get a magic link to sign in.') + .prop('disabled', false); + } + } + $('form').submit(function(event){ event.preventDefault(); $('#flash-board').hide(); + disableMagicButton(true); var $form = $(event.target); $.ajax({ type : 'POST', @@ -56,6 +70,7 @@ block content .removeClass('alert-success') .addClass('alert-info') .fadeIn(); + disableMagicButton(false); } }) .done(data =>{ @@ -65,6 +80,7 @@ block content .removeClass('alert-info') .addClass('alert-success') .fadeIn(); + disableMagicButton(false); } }); }); From ab9310982827fb9ff38168fcc26535e0159c01cf Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra Date: Thu, 20 Jul 2017 14:57:08 +0000 Subject: [PATCH 20/21] fix(auth): Fix merge errors from rebase --- common/models/user.js | 12 ++++-------- server/boot/user.js | 15 --------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index ffdf8b288a..47f3f992e1 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -177,9 +177,9 @@ module.exports = function(User) { to: user.email, from: 'team@freecodecamp.org', subject: 'Welcome to freeCodeCamp!', - protocol: isDev ? null : 'https', - host: isDev ? devHost : 'freecodecamp.org', - port: isDev ? null : 443, + protocol: getProtocol(), + host: getHost(), + port: getPort(), template: path.join( __dirname, '..', @@ -603,6 +603,7 @@ module.exports = function(User) { from: getEmailSender(), subject: 'freeCodeCamp - Authentication Request!', text: renderAuthEmail({ + host, loginEmail, loginToken }) @@ -621,11 +622,6 @@ module.exports = function(User) { `; }); }); - - return dedent` - If you entered a valid email, a magic link is on its way. - Please follow that link to sign in. - `; }); }) .catch(err => { diff --git a/server/boot/user.js b/server/boot/user.js index b0e108510e..13148734f2 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -3,7 +3,6 @@ import moment from 'moment-timezone'; import { Observable } from 'rx'; import debugFactory from 'debug'; import emoji from 'node-emoji'; -import uuid from 'node-uuid'; import { frontEndChallengeId, @@ -423,20 +422,6 @@ module.exports = function(app) { }); } - function getEmailSignup(req, res) { - if (req.user) { - return res.redirect('/'); - } - if (isSignUpDisabled) { - return res.render('account/beta', { - title: 'New sign ups are disabled' - }); - } - return res.render('account/email-signup', { - title: 'Sign up for freeCodeCamp using your Email Address' - }); - } - function getAccount(req, res) { const { username } = req.user; return res.redirect('/' + username); From 9bb9cbe1bc82852398a03ea84cf7b1b51c187e83 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 1 Sep 2017 17:38:37 -0700 Subject: [PATCH 21/21] fix(url-utils): S%/com/org/g --- server/utils/url-utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/utils/url-utils.js b/server/utils/url-utils.js index df65a80fcb..b0c686233a 100644 --- a/server/utils/url-utils.js +++ b/server/utils/url-utils.js @@ -2,7 +2,7 @@ const isDev = process.env.NODE_ENV !== 'production'; const isBeta = !!process.env.BETA; export function getEmailSender() { - return process.env.EMAIL_SENDER || 'team@freecodecamp.com'; + return process.env.EMAIL_SENDER || 'team@freecodecamp.org'; } export function getPort() { @@ -20,7 +20,7 @@ export function getHost() { if (isDev) { return process.env.HOST || 'localhost'; } - return isBeta ? 'beta.freecodecamp.com' : 'freecodecamp.com'; + return isBeta ? 'beta.freecodecamp.org' : 'freecodecamp.org'; } export function getServerFullURL() {