Merge branch 'staging' into fix/normalize-flash-type

This commit is contained in:
Cassidy Pignatello
2018-01-10 14:19:00 -05:00
committed by GitHub
117 changed files with 42131 additions and 2328 deletions

View File

@@ -0,0 +1,15 @@
import { Observable } from 'rx';
export default function extendEmail(app) {
const { AccessToken, Email } = app.models;
Email.send$ = Observable.fromNodeCallback(Email.send, Email);
AccessToken.findOne$ = Observable.fromNodeCallback(
AccessToken.findOne.bind(AccessToken)
);
AccessToken.prototype.validate$ = Observable.fromNodeCallback(
AccessToken.prototype.validate
);
AccessToken.prototype.destroy$ = Observable.fromNodeCallback(
AccessToken.prototype.destroy
);
}

View File

@@ -1,6 +0,0 @@
import { Observable } from 'rx';
export default function extendEmail(app) {
const { Email } = app.models;
Email.send$ = Observable.fromNodeCallback(Email.send, Email);
}

View File

@@ -1,4 +1,229 @@
import _ from 'lodash';
import { Observable } from 'rx';
import dedent from 'dedent';
// import debugFactory from 'debug';
import { isEmail } from 'validator';
import { check, validationResult } from 'express-validator/check';
import { ifUserRedirectTo } from '../utils/middleware';
import {
wrapHandledError,
createValidatorErrorFormatter
} from '../utils/create-handled-error.js';
const isSignUpDisabled = !!process.env.DISABLE_SIGNUP;
// const debug = debugFactory('fcc:boot:auth');
if (isSignUpDisabled) {
console.log('fcc:boot:auth - Sign up is disabled');
}
module.exports = function enableAuthentication(app) {
// enable authentication
// enable loopback access control authentication. see:
// loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html
app.enableAuth();
const ifUserRedirect = ifUserRedirectTo();
const router = app.loopback.Router();
const api = app.loopback.Router();
const { AuthToken, User } = app.models;
router.get('/login', (req, res) => res.redirect(301, '/signin'));
router.get('/logout', (req, res) => res.redirect(301, '/signout'));
function getEmailSignin(req, res) {
if (isSignUpDisabled) {
return res.render('account/beta', {
title: 'New sign ups are disabled'
});
}
return res.render('account/email-signin', {
title: 'Sign in to freeCodeCamp using your Email Address'
});
}
router.get('/signup', ifUserRedirect, getEmailSignin);
router.get('/signin', ifUserRedirect, getEmailSignin);
router.get('/email-signin', ifUserRedirect, getEmailSignin);
router.get('/signout', (req, res) => {
req.logout();
res.redirect('/');
});
router.get(
'/deprecated-signin',
ifUserRedirect,
(req, res) => res.render('account/deprecated-signin', {
title: 'Sign in to freeCodeCamp using a Deprecated Login'
})
);
const defaultErrorMsg = dedent`
Oops, something is not right,
please request a fresh link to sign in / sign up.
`;
const passwordlessGetValidators = [
check('email')
.isBase64()
.withMessage('Email should be a base64 encoded string.'),
check('token')
.exists()
.withMessage('Token should exist.')
// based on strongloop/loopback/common/models/access-token.js#L15
.isLength({ min: 64, max: 64 })
.withMessage('Token is not the right length.')
];
function getPasswordlessAuth(req, res, next) {
const {
query: {
email: encodedEmail,
token: authTokenId
} = {}
} = req;
const validation = validationResult(req)
.formatWith(createValidatorErrorFormatter('errors', '/email-signup'));
if (!validation.isEmpty()) {
const errors = validation.array();
return next(errors.pop());
}
const email = User.decodeEmail(encodedEmail);
if (!isEmail(email)) {
return next(wrapHandledError(
new TypeError('decoded email is invalid'),
{
type: 'info',
message: 'The email encoded in the link is incorrectly formatted',
redirectTo: '/email-sign'
}
));
}
// first find
return AuthToken.findOne$({ where: { id: authTokenId } })
.flatMap(authToken => {
if (!authToken) {
throw wrapHandledError(
new Error(`no token found for id: ${authTokenId}`),
{
type: 'info',
message: defaultErrorMsg,
redirectTo: '/email-signin'
}
);
}
// find user then validate and destroy email validation token
// finally retun user instance
return User.findOne$({ where: { id: authToken.userId } })
.flatMap(user => {
if (!user) {
throw wrapHandledError(
new Error(`no user found for token: ${authTokenId}`),
{
type: 'info',
message: defaultErrorMsg,
redirectTo: '/email-signin'
}
);
}
if (user.email !== email) {
throw wrapHandledError(
new Error('user email does not match'),
{
type: 'info',
message: defaultErrorMsg,
redirectTo: '/email-signin'
}
);
}
return authToken.validate()
.map(isValid => {
if (!isValid) {
throw wrapHandledError(
new Error('token is invalid'),
{
type: 'info',
message: `
Looks like the link you clicked has expired,
please request a fresh link, to sign in.
`,
redirectTo: '/email-signin'
}
);
}
return authToken.destroy();
})
.map(() => user);
});
})
// at this point token has been validated and destroyed
// update user and log them in
.map(user => user.loginByRequest(req, res))
.do(() => {
let redirectTo = '/';
if (
req.session &&
req.session.returnTo
) {
redirectTo = req.session.returnTo;
}
req.flash('success', { msg:
'Success! You have signed in to your account. Happy Coding!'
});
return res.redirect(redirectTo);
})
.subscribe(
() => {},
next
);
}
router.get(
'/passwordless-auth',
ifUserRedirect,
passwordlessGetValidators,
getPasswordlessAuth
);
const passwordlessPostValidators = [
check('email')
.isEmail()
.withMessage('Email is not a valid email address.')
];
function postPasswordlessAuth(req, res, next) {
const { body: { email } = {} } = req;
const validation = validationResult(req)
.formatWith(createValidatorErrorFormatter('errors', '/email-signup'));
if (!validation.isEmpty()) {
const errors = validation.array();
return next(errors.pop());
}
return User.findOne$({ where: { email } })
.flatMap(_user => Observable.if(
// if no user found create new user and save to db
_.constant(_user),
Observable.of(_user),
User.create$({ email })
)
.flatMap(user => user.requestAuthEmail(!_user))
)
.do(msg => res.status(200).send({ message: msg }))
.subscribe(_.noop, next);
}
api.post(
'/passwordless-auth',
ifUserRedirect,
passwordlessPostValidators,
postPasswordlessAuth
);
app.use('/:lang', router);
app.use(api);
};

View File

@@ -14,9 +14,14 @@ import {
import { observeQuery } from '../utils/rx';
import {
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
frontEndChallengeId,
dataVisChallengeId,
backEndChallengeId
dataVisId,
apisMicroservicesId,
backEndChallengeId,
infosecQaId
} from '../utils/constantStrings.json';
import {
@@ -60,9 +65,12 @@ function getIdsForCert$(id, Challenge) {
// {
// email: String,
// username: String,
// isFrontEndCert: Boolean,
// isBackEndCert: Boolean,
// isDataVisCert: Boolean
// isRespWebDesignCert: Boolean,
// isFrontEndLibsCert: Boolean,
// isJsAlgoDataStructCert: Boolean,
// isDataVisCert: Boolean,
// isApisMicroservicesCert: Boolean,
// isInfosecQaCert: Boolean
// },
// send$: Observable
// ) => Observable
@@ -71,17 +79,23 @@ function sendCertifiedEmail(
email,
name,
username,
isFrontEndCert,
isBackEndCert,
isDataVisCert
isRespWebDesignCert,
isFrontEndLibsCert,
isJsAlgoDataStructCert,
isDataVisCert,
isApisMicroservicesCert,
isInfosecQaCert
},
send$
) {
if (
!isEmail(email) ||
!isFrontEndCert ||
!isBackEndCert ||
!isDataVisCert
!isRespWebDesignCert ||
!isFrontEndLibsCert ||
!isJsAlgoDataStructCert ||
!isDataVisCert ||
!isApisMicroservicesCert ||
!isInfosecQaCert
) {
return Observable.just(false);
}
@@ -107,8 +121,16 @@ export default function certificate(app) {
const certTypeIds = {
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge)
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
[certTypes.frontEndLibs]: getIdsForCert$(frontEndLibsId, Challenge),
[certTypes.jsAlgoDataStruct]: getIdsForCert$(jsAlgoDataStructId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
[certTypes.apisMicroservices]: getIdsForCert$(
apisMicroservicesId,
Challenge
),
[certTypes.infosecQa]: getIdsForCert$(infosecQaId, Challenge)
};
router.post(
@@ -123,12 +145,42 @@ export default function certificate(app) {
verifyCert.bind(null, certTypes.backEnd)
);
router.post(
'/certificate/verify/responsive-web-design',
ifNoUser401,
verifyCert.bind(null, certTypes.respWebDesign)
);
router.post(
'/certificate/verify/front-end-libraries',
ifNoUser401,
verifyCert.bind(null, certTypes.frontEndLibs)
);
router.post(
'/certificate/verify/javascript-algorithms-data-structures',
ifNoUser401,
verifyCert.bind(null, certTypes.jsAlgoDataStruct)
);
router.post(
'/certificate/verify/data-visualization',
ifNoUser401,
verifyCert.bind(null, certTypes.dataVis)
);
router.post(
'/certificate/verify/apis-microservices',
ifNoUser401,
verifyCert.bind(null, certTypes.apisMicroservices)
);
router.post(
'/certificate/verify/information-security-quality-assurance',
ifNoUser401,
verifyCert.bind(null, certTypes.infosecQa)
);
router.post(
'/certificate/honest',
sendMessageToNonUser,

View File

@@ -132,7 +132,7 @@ export default function commit(app) {
const {
nonprofit: nonprofitName = 'girl develop it',
amount = '5',
goal = commitGoals.frontEndCert
goal = commitGoals.respWebDesignCert
} = req.query;
const nonprofit = findNonprofit(nonprofitName);

View File

@@ -1,15 +1,19 @@
const createDebugger = require('debug');
const log = createDebugger('fcc:boot:explorer');
module.exports = function mountLoopBackExplorer(app) {
if (process.env.NODE_ENV === 'production') {
return;
}
var explorer;
let explorer;
try {
explorer = require('loopback-component-explorer');
} catch (err) {
// Print the message only when the app was started via `app.listen()`.
// Do not print any message when the project is used as a component.
app.once('started', function() {
console.log(
log(
'Run `npm install loopback-component-explorer` to enable ' +
'the LoopBack explorer'
);
@@ -17,13 +21,13 @@ module.exports = function mountLoopBackExplorer(app) {
return;
}
var restApiRoot = app.get('restApiRoot');
var mountPath = '/explorer';
const restApiRoot = app.get('restApiRoot');
const mountPath = '/explorer';
explorer(app, { basePath: restApiRoot, mountPath });
app.once('started', function() {
var baseUrl = app.get('url').replace(/\/$/, '');
const baseUrl = app.get('url').replace(/\/$/, '');
console.log('Browse your REST API at %s%s', baseUrl, mountPath);
log('Browse your REST API at %s%s', baseUrl, mountPath);
});
};

View File

@@ -1,35 +0,0 @@
var path = require('path');
var loopback = require('loopback');
var express = require('express');
var port = 1337;
// this will listen to traffic on port 1337
// The purpose is to redirect any user who is direct to https
// instead of http by mistake. Our nginx proxy server will listen
// for https traffic and serve from this port on this server.
// the view being send will have a short timeout and a redirect
module.exports = function(loopbackApp) {
var app = express();
app.set('view engine', 'jade');
// views in ../views'
app.set('views', path.join(__dirname, '..'));
// server static files
app.use(loopback.static(path.join(
__dirname,
'../',
'../public'
)));
// all traffic will be redirected on page load;
app.use(function(req, res) {
return res.render('views/redirect-https');
});
loopbackApp.once('started', function() {
app.listen(port, function() {
console.log('https redirect listening on port %s', port);
});
});
};

View File

@@ -6,8 +6,13 @@ import emoji from 'node-emoji';
import {
frontEndChallengeId,
dataVisChallengeId,
backEndChallengeId
backEndChallengeId,
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
dataVisId,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import {
@@ -24,27 +29,44 @@ 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 = {
[certTypes.frontEnd]: frontEndChallengeId,
[certTypes.dataVis]: dataVisChallengeId,
[certTypes.backEnd]: backEndChallengeId
[certTypes.backEnd]: backEndChallengeId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis]: dataVisId,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
const certViews = {
[certTypes.frontEnd]: 'certificate/front-end.jade',
[certTypes.dataVis]: 'certificate/data-vis.jade',
[certTypes.backEnd]: 'certificate/back-end.jade',
[certTypes.fullStack]: 'certificate/full-stack.jade'
[certTypes.fullStack]: 'certificate/full-stack.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis]: 'certificate/data-visualization.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
};
const certText = {
[certTypes.frontEnd]: 'Front End certified',
[certTypes.dataVis]: 'Data Vis Certified',
[certTypes.backEnd]: 'Back End Certified',
[certTypes.fullStack]: 'Full Stack Certified'
[certTypes.fullStack]: 'Full Stack Certified',
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
};
const dateFormat = 'MMM DD, YYYY';
@@ -139,7 +161,7 @@ function buildDisplayChallenges(
module.exports = function(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
const { AccessToken, Email, User } = app.models;
const { Email, User } = app.models;
const map$ = cachedMap(app.models);
function findUserByUsername$(username, fields) {
@@ -153,23 +175,6 @@ 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', getEmailSignin);
router.get('/signin', getEmailSignin);
router.get('/signout', signout);
router.get('/email-signin', getEmailSignin);
router.get('/deprecated-signin', getDepSignin);
router.get('/passwordless-auth', invalidateAuthToken, getPasswordlessAuth);
api.post('/passwordless-auth', postPasswordlessAuth);
router.get(
'/delete-my-account',
sendNonUserToMap,
@@ -208,11 +213,6 @@ module.exports = function(app) {
showCert.bind(null, certTypes.frontEnd)
);
api.get(
'/:username/data-visualization-certification',
showCert.bind(null, certTypes.dataVis)
);
api.get(
'/:username/back-end-certification',
showCert.bind(null, certTypes.backEnd)
@@ -223,6 +223,36 @@ module.exports = function(app) {
(req, res) => res.redirect(req.url.replace('full-stack', 'back-end'))
);
api.get(
'/:username/responsive-web-design-certification',
showCert.bind(null, certTypes.respWebDesign)
);
api.get(
'/:username/front-end-libraries-certification',
showCert.bind(null, certTypes.frontEndLibs)
);
api.get(
'/:username/javascript-algorithms-data-structures-certification',
showCert.bind(null, certTypes.jsAlgoDataStruct)
);
api.get(
'/:username/data-visualization-certification',
showCert.bind(null, certTypes.dataVis)
);
api.get(
'/:username/apis-microservices-certification',
showCert.bind(null, certTypes.apisMicroservices)
);
api.get(
'/:username/information-security-quality-assurance-certification',
showCert.bind(null, certTypes.infosecQa)
);
router.get('/:username', showUserProfile);
router.get(
'/:username/report-user/',
@@ -240,179 +270,6 @@ module.exports = function(app) {
app.use('/:lang', router);
app.use(api);
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.requestAuthEmail(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 = new Buffer(req.query.email, 'base64').toString();
return AccessToken.findOne$({ where: {id: authTokenId} })
.map(authToken => {
if (!authToken) {
req.flash('info', { msg: defaultErrorMsg });
return res.redirect('/email-signin');
}
const userId = authToken.userId;
return User.findById(userId, (err, user) => {
if (err || !user || user.email !== authEmailId) {
debug(err);
req.flash('info', { msg: defaultErrorMsg });
return res.redirect('/email-signin');
}
return authToken.validate((err, isValid) => {
if (err) { throw err; }
if (!isValid) {
req.flash('info', { msg: [ 'Looks like the link you clicked has',
'expired, please request a fresh link, to sign in.'].join('')
});
return res.redirect('/email-signin');
}
return authToken.destroy((err) => {
if (err) { debug(err); }
next();
});
});
});
})
.subscribe(
() => {},
next
);
}
function getPasswordlessAuth(req, res, next) {
if (req.user) {
req.flash('info', {
msg: 'Hey, looks like youre already signed in.'
});
return res.redirect('/');
}
if (!req.query || !req.query.email || !req.query.token) {
req.flash('info', { msg: defaultErrorMsg });
return res.redirect('/email-signin');
}
const email = new Buffer(req.query.email, 'base64').toString();
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('/');
}
function getDepSignin(req, res) {
if (req.user) {
return res.redirect('/');
}
return res.render('account/deprecated-signin', {
title: 'Sign in to freeCodeCamp using a Deprecated Login'
});
}
function getEmailSignin(req, res) {
if (req.user) {
return res.redirect('/');
}
if (isSignUpDisabled) {
return res.render('account/beta', {
title: 'New sign ups are disabled'
});
}
return res.render('account/email-signin', {
title: 'Sign in to freeCodeCamp using your Email Address'
});
}
function getAccount(req, res) {
const { username } = req.user;
return res.redirect('/' + username);
@@ -586,9 +443,14 @@ module.exports = function(app) {
isLocked: true,
isAvailableForHire: true,
isFrontEndCert: true,
isDataVisCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
username: true,
name: true,