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
This commit is contained in:
		
				
					committed by
					
						 Berkeley Martinez
						Berkeley Martinez
					
				
			
			
				
	
			
			
			
						parent
						
							900617613f
						
					
				
				
					commit
					f8c818e7e7
				
			| @@ -5,6 +5,7 @@ import dedent from 'dedent'; | |||||||
| import debugFactory from 'debug'; | import debugFactory from 'debug'; | ||||||
| import { isEmail } from 'validator'; | import { isEmail } from 'validator'; | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
|  | import loopback from 'loopback'; | ||||||
|  |  | ||||||
| import { saveUser, observeMethod } from '../../server/utils/rx.js'; | import { saveUser, observeMethod } from '../../server/utils/rx.js'; | ||||||
| import { blacklistedUsernames } from '../../server/utils/constants.js'; | import { blacklistedUsernames } from '../../server/utils/constants.js'; | ||||||
| @@ -250,7 +251,7 @@ module.exports = function(User) { | |||||||
|         if (!user.verificationToken && !user.emailVerified) { |         if (!user.verificationToken && !user.emailVerified) { | ||||||
|           ctx.req.flash('info', { |           ctx.req.flash('info', { | ||||||
|             msg: dedent`Looks like we have your email. But you haven't |             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.` |              link.` | ||||||
|           }); |           }); | ||||||
|           return ctx.res.redirect(redirect); |           return ctx.res.redirect(redirect); | ||||||
| @@ -259,7 +260,7 @@ module.exports = function(User) { | |||||||
|         if (!user.verificationToken && user.emailVerified) { |         if (!user.verificationToken && user.emailVerified) { | ||||||
|           ctx.req.flash('info', { |           ctx.req.flash('info', { | ||||||
|             msg: dedent`Looks like you have already verified your email. |             msg: dedent`Looks like you have already verified your email. | ||||||
|              Please login to continue.` |              Please sign in to continue.` | ||||||
|           }); |           }); | ||||||
|           return ctx.res.redirect(redirect); |           return ctx.res.redirect(redirect); | ||||||
|         } |         } | ||||||
| @@ -267,7 +268,7 @@ module.exports = function(User) { | |||||||
|         if (user.verificationToken && user.verificationToken !== token) { |         if (user.verificationToken && user.verificationToken !== token) { | ||||||
|           ctx.req.flash('info', { |           ctx.req.flash('info', { | ||||||
|             msg: dedent`Looks like you have clicked an invalid link. |             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); |           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) { |   User.prototype.updateEmail = function updateEmail(email) { | ||||||
|     const fiveMinutesAgo = moment().subtract(5, 'minutes'); |     const fiveMinutesAgo = moment().subtract(5, 'minutes'); | ||||||
|     const lastEmailSentAt = moment(new Date(this.emailVerifyTTL || null)); |     const lastEmailSentAt = moment(new Date(this.emailVerifyTTL || null)); | ||||||
|   | |||||||
| @@ -19,6 +19,9 @@ | |||||||
|     "emailVerifyTTL": { |     "emailVerifyTTL": { | ||||||
|       "type": "date" |       "type": "date" | ||||||
|     }, |     }, | ||||||
|  |     "emailAuthLinkTTL": { | ||||||
|  |       "type": "date" | ||||||
|  |     }, | ||||||
|     "password": { |     "password": { | ||||||
|       "type": "string" |       "type": "string" | ||||||
|     }, |     }, | ||||||
| @@ -288,6 +291,13 @@ | |||||||
|       "principalId": "$owner", |       "principalId": "$owner", | ||||||
|       "permission": "ALLOW", |       "permission": "ALLOW", | ||||||
|       "property": "updateLanguage" |       "property": "updateLanguage" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "accessType": "EXECUTE", | ||||||
|  |       "principalType": "ROLE", | ||||||
|  |       "principalId": "$everyone", | ||||||
|  |       "permission": "ALLOW", | ||||||
|  |       "property": "requestAuthLink" | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "methods": {} |   "methods": {} | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import moment from 'moment-timezone'; | |||||||
| import { Observable } from 'rx'; | import { Observable } from 'rx'; | ||||||
| import debugFactory from 'debug'; | import debugFactory from 'debug'; | ||||||
| import emoji from 'node-emoji'; | import emoji from 'node-emoji'; | ||||||
|  | import uuid from 'node-uuid'; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|   frontEndChallengeId, |   frontEndChallengeId, | ||||||
| @@ -168,6 +169,10 @@ module.exports = function(app) { | |||||||
|   router.get('/email-signin', getEmailSignin); |   router.get('/email-signin', getEmailSignin); | ||||||
|   router.get('/deprecated-signin', getDepSignin); |   router.get('/deprecated-signin', getDepSignin); | ||||||
|   router.get('/update-email', getUpdateEmail); |   router.get('/update-email', getUpdateEmail); | ||||||
|  |   router.get('/passwordless-signin', getPasswordlessSignin); | ||||||
|  |   router.get('/passwordless-signup', getPasswordlessSignup); | ||||||
|  |   api.post('/passwordless-signin', postPasswordlessSignin); | ||||||
|  |   api.post('/passwordless-signup', postPasswordlessSignup); | ||||||
|   router.get( |   router.get( | ||||||
|     '/delete-my-account', |     '/delete-my-account', | ||||||
|     sendNonUserToMap, |     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) { |   function signout(req, res) { | ||||||
|     req.logout(); |     req.logout(); | ||||||
|     res.redirect('/'); |     res.redirect('/'); | ||||||
|   | |||||||
| @@ -1,18 +1,65 @@ | |||||||
| extends ../layout | extends ../layout | ||||||
| block content | block content | ||||||
|     .row |     .container | ||||||
|       .col-xs-12 |       .col-xs-12 | ||||||
|             h2.text-center Sign in with an email address here: |         .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 |           .col-sm-6.col-sm-offset-3 | ||||||
|                 form(method='POST', action='/api/users/login') |               form(method='POST', action='/passwordless-signin') | ||||||
|                   input(type='hidden', name='_csrf', value=_csrf) |                   input(type='hidden', name='_csrf', value=_csrf) | ||||||
|                   .form-group |                   .form-group | ||||||
|                       input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) |                       input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) | ||||||
|                     .form-group |                   .button-spacer | ||||||
|                         input.input-lg.form-control(type='password', name='password', id='password', placeholder='Password') |  | ||||||
|                   button.btn.btn-primary.btn-lg.btn-block(type='submit') |                   button.btn.btn-primary.btn-lg.btn-block(type='submit') | ||||||
|                         span.ion-android-hand |                     span.fa.fa-envelope | ||||||
|                         | Login |                     | Get a magic link to sign in. | ||||||
|                     .button-spacer |         .row | ||||||
|                     .button-spacer |           .col-sm-6.col-sm-offset-3 | ||||||
|                 a.btn.btn-info.btn-lg.btn-block(href='/forgot') Forgot your password? |               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(); | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |             }); | ||||||
|  |            }); | ||||||
|   | |||||||
| @@ -1,21 +1,68 @@ | |||||||
| extends ../layout | extends ../layout | ||||||
| block content | block content | ||||||
|     script. |     .container | ||||||
|       var challengeName = 'Email Signup' |       .col-xs-12 | ||||||
|     h2.text-center Sign up with an email address here: |  | ||||||
|     form.form-horizontal(method='POST', action='/api/users', name="signupForm") |  | ||||||
|         .row |         .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 |           .col-sm-6.col-sm-offset-3 | ||||||
|  |               form(method='POST', action='/passwordless-signup') | ||||||
|                   input(type='hidden', name='_csrf', value=_csrf) |                   input(type='hidden', name='_csrf', value=_csrf) | ||||||
|                   .form-group |                   .form-group | ||||||
|                   input.input-lg.form-control(type='email', name='email', id='email', placeholder='email', autofocus, required, autocomplete="off") |                       input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true) | ||||||
|               .form-group |                   .button-spacer | ||||||
|                   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.") |                   button.btn.btn-primary.btn-lg.btn-block(type='submit') | ||||||
|               .form-group |                     span.fa.fa-envelope | ||||||
|                   button.btn.btn-lg.btn-primary.btn-block(type='submit') |                     | Get a magic link to sign up. | ||||||
|                     span.ion-person-add |  | ||||||
|                     | Signup |  | ||||||
|         .row |         .row | ||||||
|           .col-sm-6.col-sm-offset-3 |           .col-sm-6.col-sm-offset-3 | ||||||
|  |               br | ||||||
|               p.text-center |               p.text-center | ||||||
|                   a(href="/signin") Click here if you already have an account and want to sign in. |                   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(); | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |             }); | ||||||
|  |            }); | ||||||
|   | |||||||
| @@ -1,15 +1,11 @@ | |||||||
| Greetings from San Francisco! | Greetings from San Francisco! | ||||||
| <br> |  | ||||||
| <br> |  | ||||||
| Thank you for joining our community. | Thank you for joining our community. | ||||||
| <br> |  | ||||||
| <br> |  | ||||||
| Please verify your email by following the link below: | Please verify your email by following the link below: | ||||||
| <br> |  | ||||||
| <br> |  | ||||||
| <a href="<%= verifyHref %>"><%= verifyHref %></a> | <a href="<%= verifyHref %>"><%= verifyHref %></a> | ||||||
| <br> |  | ||||||
| <br> |  | ||||||
| Feel free to email us at this address if you have any questions about freeCodeCamp. | Feel free to email us at this address if you have any questions about freeCodeCamp. | ||||||
| <br> | <br> | ||||||
| <br> | <br> | ||||||
| @@ -17,7 +13,7 @@ And if you have a moment, check out our blog: https://medium.freecodecamp.org. | |||||||
| <br> | <br> | ||||||
| <br> | <br> | ||||||
| Good luck with the challenges! | Good luck with the challenges! | ||||||
| <br> |  | ||||||
| <br> | Thanks, | ||||||
| <br> | The freeCodeCamp Team. | ||||||
| - the freeCodeCamp Team. | team@freecodecamp.com | ||||||
|   | |||||||
| @@ -1,14 +1,13 @@ | |||||||
| Thank you for updating your contact details. | Thank you for updating your contact details. | ||||||
| <br> |  | ||||||
| <br> |  | ||||||
| Please verify your email by following the link below: | Please verify your email by following the link below: | ||||||
| <br> |  | ||||||
| <br> |  | ||||||
| <a href="<%= verifyHref %>"><%= verifyHref %></a> | <a href="<%= verifyHref %>"><%= verifyHref %></a> | ||||||
| <br> |  | ||||||
| <br> |  | ||||||
| Please email us at this address if you have any questions about freeCodeCamp. | Please email us at this address if you have any questions about freeCodeCamp. | ||||||
| <br> |  | ||||||
| <br> | Good luck with the challenges! | ||||||
| <br> |  | ||||||
| - the freeCodeCamp Team | Thanks, | ||||||
|  | The freeCodeCamp Team. | ||||||
|  | team@freecodecamp.com | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								server/views/emails/user-request-sign-in.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								server/views/emails/user-request-sign-in.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										22
									
								
								server/views/emails/user-request-sign-up.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								server/views/emails/user-request-sign-up.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
		Reference in New Issue
	
	Block a user