Merge branch 'feature/passwordless-login' into staging

This commit is contained in:
Berkeley Martinez
2017-09-01 19:07:41 -07:00
17 changed files with 543 additions and 282 deletions

View File

@ -266,7 +266,8 @@ h1, h2, h3, h4, h5, h6, p, li {
}
.btn-social {
width: 250px;
width: 100%;
max-width: 260px;
margin: auto;
}

View File

@ -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,
'..',

View File

@ -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": {}

View File

@ -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 youre 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('/');
});
}
};

37
server/utils/url-utils.js Normal file
View File

@ -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();
}

View File

@ -15,6 +15,9 @@ 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() {

View File

@ -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('<span class="fa fa-circle-o-notch fa-spin fa-fw"></span>')
.prop('disabled', true);
} else {
$('#magic-btn')
.html('<span class="fa.fa-envelope">Get a magic link to sign in.</span>')
.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);
}
});
});
});

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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');

View File

@ -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();
}
});
});
});

View File

@ -1,15 +1,11 @@
Greetings from San Francisco!
<br>
<br>
Thank you for joining our community.
<br>
<br>
Please verify your email by following the link below:
<br>
<br>
<a href="<%= verifyHref %>"><%= verifyHref %></a>
<br>
<br>
Feel free to email us at this address if you have any questions about freeCodeCamp.
<br>
<br>
@ -17,7 +13,7 @@ And if you have a moment, check out our blog: https://medium.freecodecamp.org.
<br>
<br>
Good luck with the challenges!
<br>
<br>
<br>
- the freeCodeCamp Team.
Thanks,
The freeCodeCamp Team.
team@freecodecamp.com

View File

@ -1,14 +1,13 @@
Thank you for updating your contact details.
<br>
<br>
Please verify your email by following the link below:
<br>
<br>
<a href="<%= verifyHref %>"><%= verifyHref %></a>
<br>
<br>
Please email us at this address if you have any questions about freeCodeCamp.
<br>
<br>
<br>
- the freeCodeCamp Team
Good luck with the challenges!
Thanks,
The freeCodeCamp Team.
team@freecodecamp.com

View File

@ -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

View File

@ -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

View File

@ -28,7 +28,7 @@ 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')