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}')