Merge branch 'feature/passwordless-login' into staging
This commit is contained in:
@ -266,7 +266,8 @@ h1, h2, h3, h4, h5, h6, p, li {
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
width: 250px;
|
||||
width: 100%;
|
||||
max-width: 260px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
'..',
|
||||
|
@ -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": {}
|
||||
|
@ -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('/');
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
37
server/utils/url-utils.js
Normal file
37
server/utils/url-utils.js
Normal 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();
|
||||
}
|
@ -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() {
|
||||
|
@ -1,18 +1,87 @@
|
||||
extends ../layout
|
||||
block content
|
||||
.row
|
||||
.container
|
||||
.col-xs-12
|
||||
h2.text-center Sign in with an email address here:
|
||||
.row
|
||||
.col-sm-6.col-sm-offset-3.flashMessage.negative-30
|
||||
#flash-board.alert.fade.in(style='display: none;')
|
||||
button.close(type='button', data-dismiss='alert')
|
||||
span.ion-close-circled#flash-close
|
||||
#flash-content
|
||||
.row
|
||||
.text-center
|
||||
h2 Sign in or Sign Up with an Email here:
|
||||
.button-spacer
|
||||
.col-sm-6.col-sm-offset-3
|
||||
form(method='POST', action='/api/users/login')
|
||||
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)
|
||||
.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
|
||||
input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true, required)
|
||||
.button-spacer
|
||||
.button-spacer
|
||||
a.btn.btn-info.btn-lg.btn-block(href='/forgot') Forgot your password?
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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.
|
@ -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
|
||||
|
@ -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
|
@ -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');
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
|
@ -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
|
||||
|
17
server/views/emails/user-request-sign-in.ejs
Normal file
17
server/views/emails/user-request-sign-in.ejs
Normal 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
|
24
server/views/emails/user-request-sign-up.ejs
Normal file
24
server/views/emails/user-request-sign-up.ejs
Normal 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
|
@ -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')
|
||||
|
Reference in New Issue
Block a user