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