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 526c37474e..47f3f992e1 100644
--- a/common/models/user.js
+++ b/common/models/user.js
@@ -5,15 +5,21 @@ 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';
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 createEmailError = () => new Error(
'Please check to make sure the email is a valid email address.'
@@ -26,6 +32,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,
@@ -44,6 +70,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
@@ -74,6 +112,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.fromNodeCallback(User.findOrCreate, User);
+ User.prototype.createAccessToken$ = Observable.fromNodeCallback(
+ User.prototype.createAccessToken
+ );
});
User.beforeRemote('create', function({ req }) {
@@ -135,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,
'..',
@@ -250,7 +292,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 +301,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 +309,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);
}
@@ -289,6 +331,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());
@@ -487,14 +561,96 @@ 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;
+ User.requestAuthLink = function requestAuthLink(email) {
+ if (!isEmail(email)) {
+ return Promise.reject(
+ new Error('The submitted email not valid.')
+ );
+ }
+ var userObj = {
+ username: 'fcc' + uuid.v4().slice(0, 8),
+ email: email,
+ emailVerified: false
+ };
+ return User.findOrCreate$({ where: { email }}, userObj)
+ .flatMap(([ user, isCreated ]) => {
+
+ const minutesLeft = getWaitPeriod(user.emailAuthLinkTTL);
+ if (minutesLeft > 0) {
+ const timeToWait = minutesLeft ?
+ `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
+ 'a few seconds';
+ debug('request before wait time : ' + timeToWait);
+ 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 15 minutes
+ return user.createAccessToken$({ ttl: 15 * 60 * 1000 })
+ .flatMap(token => {
+
+ const { id: loginToken } = token;
+ const loginEmail = user.email;
+ const host = getServerFullURL();
+ const mailOptions = {
+ type: 'email',
+ to: user.email,
+ from: getEmailSender(),
+ subject: 'freeCodeCamp - Authentication Request!',
+ text: renderAuthEmail({
+ host,
+ loginEmail,
+ loginToken
+ })
+ };
+
+ 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.
+ `;
+ });
+ });
+ });
+ })
+ .catch(err => {
+ if (err) { debug(err); }
+ return dedent`
+ Oops, something is not right, 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
+ }],
+ returns: [{
+ arg: 'message', type: 'string'
+ }],
+ http: {
+ path: '/request-auth-link', verb: 'POST'
+ }
+ }
+ );
+
+ User.prototype.updateEmail = function updateEmail(email) {
+ const ownEmail = email === this.email;
if (!isEmail('' + email)) {
return Observable.throw(createEmailError());
}
@@ -505,17 +661,15 @@ module.exports = function(User) {
));
}
- if (ownEmail && !isWaitPeriodOver) {
- const minutesLeft = 5 -
- (moment().minutes() - lastEmailSentAt.minutes());
-
+ const minutesLeft = getWaitPeriod(this.emailVerifyTTL);
+ if (ownEmail && minutesLeft > 0) {
const timeToWait = minutesLeft ?
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
'a few seconds';
-
- return Observable.throw(new Error(
- `Please wait ${timeToWait} to resend email verification.`
- ));
+ debug('request before wait time : ' + timeToWait);
+ return Observable.of(dedent`
+ Please wait ${timeToWait} to resend an authentication link.
+ `);
}
return Observable.fromPromise(User.doesExist(null, email))
@@ -543,11 +697,11 @@ module.exports = function(User) {
const mailOptions = {
type: 'email',
to: email,
- from: 'team@freecodecamp.org',
- subject: 'Welcome to freeCodeCamp!',
- protocol: isDev ? null : 'https',
- host: isDev ? devHost : 'freecodecamp.org',
- port: isDev ? null : 443,
+ from: getEmailSender(),
+ subject: 'freeCodeCamp - Email Update Requested',
+ protocol: getProtocol(),
+ host: getHost(),
+ port: getPort(),
template: path.join(
__dirname,
'..',
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..13148734f2 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -24,6 +24,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 = {
@@ -138,8 +139,9 @@ function buildDisplayChallenges(
module.exports = function(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
- const { User, Email } = app.models;
+ const { AccessToken, Email, User } = app.models;
const map$ = cachedMap(app.models);
+
function findUserByUsername$(username, fields) {
return observeQuery(
User,
@@ -151,23 +153,23 @@ module.exports = function(app) {
);
}
+ AccessToken.findOne$ = Observable.fromNodeCallback(
+ AccessToken.findOne, AccessToken
+ );
+
router.get('/login', function(req, res) {
res.redirect(301, '/signin');
});
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);
+ router.get('/passwordless-auth', invalidateAuthToken, getPasswordlessAuth);
+ api.post('/passwordless-auth', postPasswordlessAuth);
router.get(
'/delete-my-account',
sendNonUserToMap,
@@ -247,6 +249,150 @@ 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('/');
+ }
+
+ return User.requestAuthLink(req.body.email)
+ .then(msg => {
+ return res.status(200).send({ message: msg });
+ })
+ .catch(err => {
+ debug(err);
+ 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 || !user || user.email !== authEmailId) {
+ debug(err);
+ 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', {
+ msg: 'Hey, looks like you’re already signed in.'
+ });
+ return res.redirect('/');
+ }
+
+ if (!req.query || !req.query.email || !req.query.token) {
+ req.flash('info', { msg: defaultErrorMsg });
+ return res.redirect('/email-signin');
+ }
+
+ const email = req.query.email;
+
+ 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; }
+
+ 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('/');
@@ -262,26 +408,7 @@ 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('/');
- }
- 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('/');
}
@@ -290,8 +417,8 @@ module.exports = function(app) {
title: 'New sign ups are disabled'
});
}
- return res.render('account/email-signup', {
- title: 'Sign up for freeCodeCamp using your Email Address'
+ return res.render('account/email-signin', {
+ title: 'Sign in to freeCodeCamp using your Email Address'
});
}
@@ -582,74 +709,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', {
@@ -700,4 +759,5 @@ module.exports = function(app) {
return res.redirect('/');
});
}
+
};
diff --git a/server/utils/url-utils.js b/server/utils/url-utils.js
new file mode 100644
index 0000000000..b0c686233a
--- /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.org';
+}
+
+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.org' : 'freecodecamp.org';
+}
+
+export function getServerFullURL() {
+ if (!isDev) {
+ return getProtocol()
+ + '://'
+ + getHost();
+ }
+ return getProtocol()
+ + '://'
+ + getHost()
+ + ':'
+ + getPort();
+}
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 fed5792311..b90ffee1fa 100644
--- a/server/views/account/email-signin.jade
+++ b/server/views/account/email-signin.jade
@@ -1,18 +1,87 @@
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 or 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, required)
+ .button-spacer
+ button#magic-btn.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
+ | freeCodeCamp uses passwordless authentication.
+ br
+ | 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',
+ 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();
+ disableMagicButton(false);
+ }
+ })
+ .done(data =>{
+ if(data && data.message){
+ $('#flash-content').html(data.message);
+ $('#flash-board')
+ .removeClass('alert-info')
+ .addClass('alert-success')
+ .fadeIn();
+ disableMagicButton(false);
+ }
+ });
+ });
+ });
diff --git a/server/views/account/email-signup.jade b/server/views/account/email-signup.jade
deleted file mode 100644
index 03b8614495..0000000000
--- a/server/views/account/email-signup.jade
+++ /dev/null
@@ -1,21 +0,0 @@
-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")
- .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.
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
diff --git a/server/views/account/signin.jade b/server/views/account/signin.jade
index 0b1123cf89..b73e8b8271 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 freeCodeCamp!
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/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();
- }
- });
- });
- });
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..866a77c96b
--- /dev/null
+++ b/server/views/emails/user-request-sign-in.ejs
@@ -0,0 +1,17 @@
+Greetings from San Francisco!
+
+Please follow the link below, and sign in to freeCodeCamp instantly:
+
+<%= host %>/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %>
+
+This above link is valid for 15 minutes.
+
+IMPORTANT NOTE:
+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!
+
+Thanks,
+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
new file mode 100644
index 0000000000..fadeccf4bc
--- /dev/null
+++ b/server/views/emails/user-request-sign-up.ejs
@@ -0,0 +1,24 @@
+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 %>
+
+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:
+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!
+
+Thanks,
+The freeCodeCamp Team.
+team@freecodecamp.com
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}')