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 {
|
.btn-social {
|
||||||
width: 250px;
|
width: 100%;
|
||||||
|
max-width: 260px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,15 +5,21 @@ import dedent from 'dedent';
|
|||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import { isEmail } from 'validator';
|
import { isEmail } from 'validator';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import loopback from 'loopback';
|
||||||
|
|
||||||
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
||||||
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
||||||
import { wrapHandledError } from '../../server/utils/create-handled-error.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 debug = debugFactory('fcc:user:remote');
|
||||||
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
|
||||||
const devHost = process.env.HOST || 'localhost';
|
|
||||||
|
|
||||||
const createEmailError = () => new Error(
|
const createEmailError = () => new Error(
|
||||||
'Please check to make sure the email is a valid email address.'
|
'Please check to make sure the email is a valid email address.'
|
||||||
@ -26,6 +32,26 @@ function destroyAll(id, Model) {
|
|||||||
)({ userId: id });
|
)({ 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({
|
function getAboutProfile({
|
||||||
username,
|
username,
|
||||||
githubProfile: github,
|
githubProfile: github,
|
||||||
@ -44,6 +70,18 @@ function nextTick(fn) {
|
|||||||
return process.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) {
|
module.exports = function(User) {
|
||||||
// NOTE(berks): user email validation currently not needed but build in. This
|
// NOTE(berks): user email validation currently not needed but build in. This
|
||||||
// work around should let us sneak by
|
// work around should let us sneak by
|
||||||
@ -74,6 +112,10 @@ module.exports = function(User) {
|
|||||||
User.findOne$ = Observable.fromNodeCallback(User.findOne, User);
|
User.findOne$ = Observable.fromNodeCallback(User.findOne, User);
|
||||||
User.update$ = Observable.fromNodeCallback(User.updateAll, User);
|
User.update$ = Observable.fromNodeCallback(User.updateAll, User);
|
||||||
User.count$ = Observable.fromNodeCallback(User.count, 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 }) {
|
User.beforeRemote('create', function({ req }) {
|
||||||
@ -135,9 +177,9 @@ module.exports = function(User) {
|
|||||||
to: user.email,
|
to: user.email,
|
||||||
from: 'team@freecodecamp.org',
|
from: 'team@freecodecamp.org',
|
||||||
subject: 'Welcome to freeCodeCamp!',
|
subject: 'Welcome to freeCodeCamp!',
|
||||||
protocol: isDev ? null : 'https',
|
protocol: getProtocol(),
|
||||||
host: isDev ? devHost : 'freecodecamp.org',
|
host: getHost(),
|
||||||
port: isDev ? null : 443,
|
port: getPort(),
|
||||||
template: path.join(
|
template: path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
@ -250,7 +292,7 @@ module.exports = function(User) {
|
|||||||
if (!user.verificationToken && !user.emailVerified) {
|
if (!user.verificationToken && !user.emailVerified) {
|
||||||
ctx.req.flash('info', {
|
ctx.req.flash('info', {
|
||||||
msg: dedent`Looks like we have your email. But you haven't
|
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.`
|
link.`
|
||||||
});
|
});
|
||||||
return ctx.res.redirect(redirect);
|
return ctx.res.redirect(redirect);
|
||||||
@ -259,7 +301,7 @@ module.exports = function(User) {
|
|||||||
if (!user.verificationToken && user.emailVerified) {
|
if (!user.verificationToken && user.emailVerified) {
|
||||||
ctx.req.flash('info', {
|
ctx.req.flash('info', {
|
||||||
msg: dedent`Looks like you have already verified your email.
|
msg: dedent`Looks like you have already verified your email.
|
||||||
Please login to continue.`
|
Please sign in to continue.`
|
||||||
});
|
});
|
||||||
return ctx.res.redirect(redirect);
|
return ctx.res.redirect(redirect);
|
||||||
}
|
}
|
||||||
@ -267,7 +309,7 @@ module.exports = function(User) {
|
|||||||
if (user.verificationToken && user.verificationToken !== token) {
|
if (user.verificationToken && user.verificationToken !== token) {
|
||||||
ctx.req.flash('info', {
|
ctx.req.flash('info', {
|
||||||
msg: dedent`Looks like you have clicked an invalid link.
|
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);
|
return ctx.res.redirect(redirect);
|
||||||
}
|
}
|
||||||
@ -289,6 +331,38 @@ module.exports = function(User) {
|
|||||||
return ctx.res.redirect(redirect);
|
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) {
|
User.on('resetPasswordRequest', function(info) {
|
||||||
if (!isEmail(info.email)) {
|
if (!isEmail(info.email)) {
|
||||||
console.error(createEmailError());
|
console.error(createEmailError());
|
||||||
@ -487,14 +561,96 @@ module.exports = function(User) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
User.prototype.updateEmail = function updateEmail(email) {
|
User.requestAuthLink = function requestAuthLink(email) {
|
||||||
const fiveMinutesAgo = moment().subtract(5, 'minutes');
|
if (!isEmail(email)) {
|
||||||
const lastEmailSentAt = moment(new Date(this.emailVerifyTTL || null));
|
return Promise.reject(
|
||||||
const ownEmail = email === this.email;
|
new Error('The submitted email not valid.')
|
||||||
const isWaitPeriodOver = this.emailVerifyTTL ?
|
);
|
||||||
lastEmailSentAt.isBefore(fiveMinutesAgo) :
|
}
|
||||||
true;
|
|
||||||
|
|
||||||
|
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)) {
|
if (!isEmail('' + email)) {
|
||||||
return Observable.throw(createEmailError());
|
return Observable.throw(createEmailError());
|
||||||
}
|
}
|
||||||
@ -505,17 +661,15 @@ module.exports = function(User) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ownEmail && !isWaitPeriodOver) {
|
const minutesLeft = getWaitPeriod(this.emailVerifyTTL);
|
||||||
const minutesLeft = 5 -
|
if (ownEmail && minutesLeft > 0) {
|
||||||
(moment().minutes() - lastEmailSentAt.minutes());
|
|
||||||
|
|
||||||
const timeToWait = minutesLeft ?
|
const timeToWait = minutesLeft ?
|
||||||
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
|
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
|
||||||
'a few seconds';
|
'a few seconds';
|
||||||
|
debug('request before wait time : ' + timeToWait);
|
||||||
return Observable.throw(new Error(
|
return Observable.of(dedent`
|
||||||
`Please wait ${timeToWait} to resend email verification.`
|
Please wait ${timeToWait} to resend an authentication link.
|
||||||
));
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.fromPromise(User.doesExist(null, email))
|
return Observable.fromPromise(User.doesExist(null, email))
|
||||||
@ -543,11 +697,11 @@ module.exports = function(User) {
|
|||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
to: email,
|
to: email,
|
||||||
from: 'team@freecodecamp.org',
|
from: getEmailSender(),
|
||||||
subject: 'Welcome to freeCodeCamp!',
|
subject: 'freeCodeCamp - Email Update Requested',
|
||||||
protocol: isDev ? null : 'https',
|
protocol: getProtocol(),
|
||||||
host: isDev ? devHost : 'freecodecamp.org',
|
host: getHost(),
|
||||||
port: isDev ? null : 443,
|
port: getPort(),
|
||||||
template: path.join(
|
template: path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'..',
|
'..',
|
||||||
|
@ -19,6 +19,9 @@
|
|||||||
"emailVerifyTTL": {
|
"emailVerifyTTL": {
|
||||||
"type": "date"
|
"type": "date"
|
||||||
},
|
},
|
||||||
|
"emailAuthLinkTTL": {
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -288,6 +291,13 @@
|
|||||||
"principalId": "$owner",
|
"principalId": "$owner",
|
||||||
"permission": "ALLOW",
|
"permission": "ALLOW",
|
||||||
"property": "updateLanguage"
|
"property": "updateLanguage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessType": "EXECUTE",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "requestAuthLink"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"methods": {}
|
"methods": {}
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
import supportedLanguages from '../../common/utils/supported-languages';
|
import supportedLanguages from '../../common/utils/supported-languages';
|
||||||
import { getChallengeInfo, cachedMap } from '../utils/map';
|
import { getChallengeInfo, cachedMap } from '../utils/map';
|
||||||
|
|
||||||
|
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
|
||||||
const debug = debugFactory('fcc:boot:user');
|
const debug = debugFactory('fcc:boot:user');
|
||||||
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
const sendNonUserToMap = ifNoUserRedirectTo('/map');
|
||||||
const certIds = {
|
const certIds = {
|
||||||
@ -138,8 +139,9 @@ function buildDisplayChallenges(
|
|||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const { User, Email } = app.models;
|
const { AccessToken, Email, User } = app.models;
|
||||||
const map$ = cachedMap(app.models);
|
const map$ = cachedMap(app.models);
|
||||||
|
|
||||||
function findUserByUsername$(username, fields) {
|
function findUserByUsername$(username, fields) {
|
||||||
return observeQuery(
|
return observeQuery(
|
||||||
User,
|
User,
|
||||||
@ -151,23 +153,23 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccessToken.findOne$ = Observable.fromNodeCallback(
|
||||||
|
AccessToken.findOne, AccessToken
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/login', function(req, res) {
|
router.get('/login', function(req, res) {
|
||||||
res.redirect(301, '/signin');
|
res.redirect(301, '/signin');
|
||||||
});
|
});
|
||||||
router.get('/logout', function(req, res) {
|
router.get('/logout', function(req, res) {
|
||||||
res.redirect(301, '/signout');
|
res.redirect(301, '/signout');
|
||||||
});
|
});
|
||||||
router.get('/signup', getEmailSignup);
|
router.get('/signup', getSignin);
|
||||||
router.get('/signin', getSignin);
|
router.get('/signin', getSignin);
|
||||||
router.get('/signout', signout);
|
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('/email-signin', getEmailSignin);
|
||||||
router.get('/deprecated-signin', getDepSignin);
|
router.get('/deprecated-signin', getDepSignin);
|
||||||
router.get('/update-email', getUpdateEmail);
|
router.get('/passwordless-auth', invalidateAuthToken, getPasswordlessAuth);
|
||||||
|
api.post('/passwordless-auth', postPasswordlessAuth);
|
||||||
router.get(
|
router.get(
|
||||||
'/delete-my-account',
|
'/delete-my-account',
|
||||||
sendNonUserToMap,
|
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) {
|
function signout(req, res) {
|
||||||
req.logout();
|
req.logout();
|
||||||
res.redirect('/');
|
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) {
|
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) {
|
if (req.user) {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
@ -290,8 +417,8 @@ module.exports = function(app) {
|
|||||||
title: 'New sign ups are disabled'
|
title: 'New sign ups are disabled'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res.render('account/email-signup', {
|
return res.render('account/email-signin', {
|
||||||
title: 'Sign up for freeCodeCamp using your Email Address'
|
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) {
|
function getReportUserProfile(req, res) {
|
||||||
const username = req.params.username.toLowerCase();
|
const username = req.params.username.toLowerCase();
|
||||||
return res.render('account/report-profile', {
|
return res.render('account/report-profile', {
|
||||||
@ -700,4 +759,5 @@ module.exports = function(app) {
|
|||||||
return res.redirect('/');
|
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')
|
a.btn.btn-lg.btn-block.btn-social.btn-twitter(href='/auth/twitter')
|
||||||
i.fa.fa-twitter
|
i.fa.fa-twitter
|
||||||
| Sign in with Twitter
|
| Sign in with Twitter
|
||||||
|
br
|
||||||
|
p
|
||||||
|
a(href="/signin") Or click here to go back.
|
||||||
|
|
||||||
script.
|
script.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
@ -1,18 +1,87 @@
|
|||||||
extends ../layout
|
extends ../layout
|
||||||
block content
|
block content
|
||||||
.row
|
.container
|
||||||
.col-xs-12
|
.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
|
.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)
|
input(type='hidden', name='_csrf', value=_csrf)
|
||||||
.form-group
|
.form-group
|
||||||
input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true)
|
input.input-lg.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true, required)
|
||||||
.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
|
||||||
.button-spacer
|
button#magic-btn.btn.btn-primary.btn-lg.btn-block(type='submit')
|
||||||
a.btn.btn-info.btn-lg.btn-block(href='/forgot') Forgot your password?
|
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
|
extends ../layout
|
||||||
block content
|
block content
|
||||||
.text-center
|
.text-center
|
||||||
h2 Are you a returning camper?
|
h2 Welcome to freeCodeCamp!
|
||||||
br
|
br
|
||||||
.button-spacer
|
.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')
|
a.btn.btn-lg.btn-block.btn-social.btn-primary(href='/email-signin')
|
||||||
i.fa.fa-envelope
|
i.fa.fa-envelope
|
||||||
| Sign in with Email
|
| Continue with Email
|
||||||
a.btn.btn-lg.btn-block.btn-social.btn-github(href='/auth/github')
|
a.btn.btn-lg.btn-block.btn-social.btn-github(href='/auth/github')
|
||||||
i.fa.fa-github
|
i.fa.fa-github
|
||||||
| Sign in with GitHub
|
| Continue with GitHub
|
||||||
br
|
br
|
||||||
p
|
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.
|
script.
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
@ -43,7 +43,7 @@ block content
|
|||||||
$(this).removeClass('active');
|
$(this).removeClass('active');
|
||||||
obj.methodClass = $(this).attr('class').split(' ').pop();
|
obj.methodClass = $(this).attr('class').split(' ').pop();
|
||||||
obj.method = $(this).text();
|
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));
|
localStorage.setItem('lastSigninMethod', JSON.stringify(obj));
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('lastSigninMethod');
|
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!
|
Greetings from San Francisco!
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Thank you for joining our community.
|
Thank you for joining our community.
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Please verify your email by following the link below:
|
Please verify your email by following the link below:
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<a href="<%= verifyHref %>"><%= verifyHref %></a>
|
<a href="<%= verifyHref %>"><%= verifyHref %></a>
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Feel free to email us at this address if you have any questions about freeCodeCamp.
|
Feel free to email us at this address if you have any questions about freeCodeCamp.
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
@ -17,7 +13,7 @@ And if you have a moment, check out our blog: https://medium.freecodecamp.org.
|
|||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
Good luck with the challenges!
|
Good luck with the challenges!
|
||||||
<br>
|
|
||||||
<br>
|
Thanks,
|
||||||
<br>
|
The freeCodeCamp Team.
|
||||||
- the freeCodeCamp Team.
|
team@freecodecamp.com
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
Thank you for updating your contact details.
|
Thank you for updating your contact details.
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Please verify your email by following the link below:
|
Please verify your email by following the link below:
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<a href="<%= verifyHref %>"><%= verifyHref %></a>
|
<a href="<%= verifyHref %>"><%= verifyHref %></a>
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Please email us at this address if you have any questions about freeCodeCamp.
|
Please email us at this address if you have any questions about freeCodeCamp.
|
||||||
<br>
|
|
||||||
<br>
|
Good luck with the challenges!
|
||||||
<br>
|
|
||||||
- the freeCodeCamp Team
|
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
|
a(href='https://www.freecodecamp.org/donate') Donate
|
||||||
if !user
|
if !user
|
||||||
li
|
li
|
||||||
a(href='/signup') Sign Up
|
a(href='/signin') Sign In
|
||||||
else
|
else
|
||||||
li.avatar-points
|
li.avatar-points
|
||||||
a(href='/settings')
|
a(href='/settings')
|
||||||
|
Reference in New Issue
Block a user