Merge branch 'staging' of github.com:FreeCodeCamp/freecodecamp into staging

This commit is contained in:
Quincy Larson
2015-08-04 14:40:56 -07:00
22 changed files with 283 additions and 300 deletions

View File

@ -161,9 +161,9 @@ module.exports = function(User) {
if (!username) {
// Zalgo!!
return nextTick(() => {
cb(
new TypeError('FCC: username should be a string but got %s', username)
);
cb(new TypeError(
`username should be a string but got ${ username }`
));
});
}
User.findOne({ where: { username } }, (err, user) => {
@ -171,7 +171,7 @@ module.exports = function(User) {
return cb(err);
}
if (!user || user.username !== username) {
return cb(new Error('FCC: no user found for %s', username));
return cb(new Error(`no user found for ${ username }`));
}
const aboutUser = getAboutProfile(user);
return cb(null, aboutUser);

View File

@ -19,28 +19,14 @@
"password": {
"type": "string"
},
"facebook": {
"type": "string"
},
"twitter": {
"type": "string"
},
"google": {
"type": "string"
},
"github": {
"type": "string"
},
"linkedin": {
"type": "string"
},
"tokens": {
"type": "array"
},
"progressTimestamps": {
"type": "array",
"default": []
},
"isGithubCool": {
"type": "boolean",
"default": false
},
"username": {
"type": "string",
"lowercase": true,
@ -123,12 +109,6 @@
"type": "string",
"default": ""
},
"resetPasswordToken": {
"type": "string"
},
"resetPasswordExpires": {
"type": "string"
},
"completedBonfires": {
"type": [
{
@ -170,21 +150,10 @@
"type": "number",
"default": 0
},
"needsSomeDataModeled": {
"type": "boolean",
"default": false
},
"needsMigration": {
"type": "boolean",
"default": true
},
"sendMonthlyEmail": {
"type": "boolean",
"default": true
},
"challengesHash": {
"type": {}
},
"currentChallenge": {
"type": {}
},
@ -205,10 +174,6 @@
}
],
"default": []
},
"uncompletedChallenges": {
"type": "array",
"default": []
}
},
"validations": [],

View File

@ -66,7 +66,7 @@
"lodash": "^3.9.3",
"loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password",
"loopback-boot": "^2.8.0",
"loopback-component-passport": "1.4.0",
"loopback-component-passport": "^1.5.0",
"loopback-connector-mongodb": "^1.10.0",
"lusca": "~1.0.2",
"method-override": "~2.3.0",

View File

@ -577,6 +577,7 @@ thead {
margin: 0 auto;
position: relative;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}

View File

@ -277,7 +277,7 @@
"difficulty": "9.98141",
"description": [
"",
"In JavaScript whole numbers (called integers) can be really easily to preform mathematical functions",
"In JavaScript whole numbers (called integers) can be easily used to preform mathematical functions",
"Let's try a few of the most commonly used ones now",
"We use <code> + </code> for addition",
"Replace the <code> 0 </code> with correct number to achieve the result in the comment."
@ -301,7 +301,7 @@
"difficulty": "9.98142",
"description": [
"",
"In JavaScript whole numbers (called integers) can be really easily to preform mathematical functions",
"In JavaScript whole numbers (called integers) can be easily used to preform mathematical functions",
"Let's try a few of the most commonly used ones now",
"We use <code> - </code> for subtraction",
"Replace the <code> 0 </code> with correct number to achieve the result in the comment."
@ -325,7 +325,7 @@
"difficulty": "9.98143",
"description": [
"",
"In JavaScript whole numbers (called integers) can be really easily to preform mathematical functions",
"In JavaScript whole numbers (called integers) can be easily used to preform mathematical functions",
"Let's try a few of the most commonly used ones now",
"We use <code> * </code> for multiplication",
"Replace the <code> 0 </code> with correct number to achieve the result in the comment."
@ -349,7 +349,7 @@
"difficulty": "9.9814",
"description": [
"",
"In JavaScript whole numbers (called integers) can be really easily to preform mathematical functions",
"In JavaScript whole numbers (called integers) can be easily used to preform mathematical functions",
"Let's try a few of the most commonly used ones now",
"We use <code> / </code> for division",
"Replace the <code> 0 </code> with correct number to achieve the result in the comment."

View File

@ -12,7 +12,7 @@
"Bootstrap will figure out how wide your screen is and respond by resizing your HTML elements - hence the name <code>Responsive Design</code>.",
"With responsive design, there is no need to design a mobile version of your website. It will look good on devices with screens of any width.",
"You can add Bootstrap to any app just by including it with <code>&#60;link rel='stylesheet' href='//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css'/&#62;</code> at the top of your HTML. But we've gone ahead and automatically added it to your Cat Photo App for you.",
"To get started, we should nest all of our HTML in a <code>div</code> element with the class \"fluid-container\"."
"To get started, we should nest all of our HTML in a <code>div</code> element with the class \"container-fluid\"."
],
"tests": [
"assert($('div').hasClass('container-fluid'), 'Your <code>div</code> element should have the class \"row\"')",
@ -1566,7 +1566,7 @@
"difficulty": 2.18,
"description": [
"Now let's make sure all the content on your page is mobile-responsive.",
"Let's nest your <code>h3</code> element within a <code>div<code> element with the class \"containter-fluid\"."
"Let's nest your <code>h3</code> element within a <code>div<code> element with the class \"container-fluid\"."
],
"tests": [
"assert($('div').hasClass('container-fluid'), 'Your <code>div</code> element should have the class \"container-fluid\"')",
@ -1743,7 +1743,7 @@
],
"tests": [
"assert($('.btn').length > 5, 'Apply the \"btn\" class to each of your <code>button</code> elements.')",
"assert($('.btn').length > 5, 'Apply the \"btn-default\" class to each of your <code>button</code> elements.')"
"assert($('.btn-default').length > 5, 'Apply the \"btn-default\" class to each of your <code>button</code> elements.')"
],
"challengeSeed": [
"<div class='container-fluid'>",

View File

@ -1,27 +1,60 @@
{
"initial:before": {
"loopback#favicon": {}
"loopback#favicon": {
"params": "$!../public/favicon.ico"
}
},
"initial": {
"compression": {}
"compression": {},
"morgan": {
"params": "dev"
}
},
"session": {
"./middlewares/sessions.js": {}
},
"auth:before": {
"./middlewares/add-return-to": {}
},
"auth": {
},
"parse": {
"body-parser#json": {},
"body-parser#urlencoded": {
"params": { "extended": true }
},
"method-override": {},
"./middlewares/cookie-parser": {}
},
"parse:after": {
"./middlewares/validator": {}
},
"routes:before": {
"express-flash": {},
"helmet#xssFilter": {},
"helmet#noSniff": {},
"helmet#frameguard": {},
"./middlewares/constant-headers": {},
"./middlewares/csp": {},
"./middlewares/express-rx": {},
"./middlewares/global-locals": {}
},
"routes": {
},
"files": {
"loopback#static": {
"params": "$!../public"
"params": [
"$!../public",
{
"maxAge": "86400000"
}
]
}
},
"final": {
"loopback#urlNotFound": {}
},
"final:after": {
"errorhandler": {}
"./middlewares/keymetrics": {},
"./middlewares/error-handlers": {}
}
}

View File

@ -0,0 +1,13 @@
export default function addReturnToUrl() {
return function(req, res, next) {
// Remember original destination before login.
var path = req.path.split('/')[1];
if (/auth|login|logout|signin|signup|fonts|favicon/i.test(path)) {
return next();
} else if (/\/stories\/\w+/i.test(req.path)) {
return next();
}
req.session.returnTo = req.path;
next();
};
}

View File

@ -0,0 +1,9 @@
export default function constantHeaders() {
return function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
};
}

View File

@ -0,0 +1,4 @@
import cookieParser from 'cookie-parser';
import secrets from '../../config/secrets';
export default cookieParser.bind(cookieParser, secrets.cookieSecret);

98
server/middlewares/csp.js Normal file
View File

@ -0,0 +1,98 @@
import helmet from 'helmet';
const trusted = [
"'self'",
'blob:',
'104.236.218.15',
'*.freecodecamp.com',
'http://www.freecodecamp.com',
'https://www.freecodecamp.com',
'https://freecodecamp.com',
'https://freecodecamp.org',
'*.freecodecamp.org',
// NOTE(berks): add the following as the blob above was not covering www
'http://www.freecodecamp.org',
'ws://freecodecamp.com/',
'ws://www.freecodecamp.com/',
'*.gstatic.com',
'*.google-analytics.com',
'*.googleapis.com',
'*.google.com',
'*.gstatic.com',
'*.doubleclick.net',
'*.twitter.com',
'*.twitch.tv',
'*.twimg.com',
"'unsafe-eval'",
"'unsafe-inline'",
'*.bootstrapcdn.com',
'*.cloudflare.com',
'https://*.cloudflare.com',
'localhost:3001',
'ws://localhost:3001/',
'http://localhost:3001',
'localhost:3000',
'ws://localhost:3000/',
'http://localhost:3000',
'*.ionicframework.com',
'https://syndication.twitter.com',
'*.youtube.com',
'*.jsdelivr.net',
'https://*.jsdelivr.net',
'*.ytimg.com',
'*.bitly.com',
'http://cdn.inspectlet.com/',
'https://cdn.inspeclet.com/',
'wss://inspectletws.herokuapp.com/',
'http://hn.inspectlet.com/',
'*.googleapis.com',
'*.gstatic.com',
'https://hn.inspectlet.com/'
];
export default function csp() {
return helmet.csp({
defaultSrc: trusted,
scriptSrc: [
'*.optimizely.com',
'*.aspnetcdn.com',
'*.d3js.org',
'https://cdn.inspectlet.com/inspectlet.js',
'http://cdn.inspectlet.com/inspectlet.js',
'http://beta.freecodecamp.com'
].concat(trusted),
'connect-src': [
'vimeo.com'
].concat(trusted),
styleSrc: [
'*.googleapis.com',
'*.gstatic.com'
].concat(trusted),
imgSrc: [
// allow all input since we have user submitted images for
// public profile
'*'
].concat(trusted),
fontSrc: [
'*.googleapis.com',
'*.gstatic.com'
].concat(trusted),
mediaSrc: [
'*.amazonaws.com',
'*.twitter.com'
].concat(trusted),
frameSrc: [
'*.gitter.im',
'*.gitter.im https:',
'*.vimeo.com',
'*.twitter.com',
'*.ghbtns.com'
].concat(trusted),
// set to true if you only want to report errors
reportOnly: false,
// set to true if you want to set all headers
setAllHeaders: false,
// set to true if you want to force buggy CSP in Safari 5
safari5: false
});
}

View File

@ -0,0 +1,43 @@
import errorHanlder from 'errorhandler';
import accepts from 'accepts';
export default function prodErrorHandler() {
if (process.env.NODE_ENV === 'development') {
return errorHanlder({ log: true });
}
// error handling in production.
// disabling eslint due to express parity rules for error handlers
return function(err, req, res, next) { // eslint-disable-line
// respect err.status
if (err.status) {
res.statusCode = err.status;
}
// default status code to 500
if (res.statusCode < 400) {
res.statusCode = 500;
}
// parse res type
var accept = accepts(req);
var type = accept.type('html', 'json', 'text');
var message = 'opps! Something went wrong. Please try again later';
if (type === 'html') {
req.flash('errors', {
msg: message
});
return res.redirect('/');
// json
} else if (type === 'json') {
res.setHeader('Content-Type', 'application/json');
return res.send({
message: message
});
// plain text
} else {
res.setHeader('Content-Type', 'text/plain');
return res.send(message);
}
};
}

View File

@ -0,0 +1,10 @@
import { Observable } from 'rx';
// add rx methods to express
export default function expressRx() {
return function expressRx(req, res, next) {
// render to observable stream
res.render$ = Observable.fromNodeCallback(res.render, res);
next();
};
}

View File

@ -0,0 +1,7 @@
export default function globalLocals() {
return function(req, res, next) {
// Make user object available in templates.
res.locals.user = req.user;
next();
};
}

View File

@ -0,0 +1,8 @@
import pmx from 'pmx';
export default function keymetrics() {
if (process.env.NODE_ENV !== 'production') {
return (err, req, res, next) => next(err);
}
return pmx.expressErrorHandler();
}

View File

@ -0,0 +1,17 @@
import session from 'express-session';
import MongoStoreFactory from 'connect-mongo';
import secrets from '../../config/secrets';
const MongoStore = MongoStoreFactory(session);
export default function sessionsMiddleware() {
return session({
resave: true,
saveUninitialized: true,
secret: secrets.sessionSecret,
store: new MongoStore({
url: secrets.db,
'autoReconnect': true
})
});
}

View File

@ -0,0 +1,9 @@
import validator from 'express-validator';
export default validator.bind(validator, {
customValidators: {
matchRegex: function matchRegex(param, regex) {
return regex.test(param);
}
}
});

View File

@ -5,28 +5,9 @@ pmx.init();
var assign = require('lodash').assign,
loopback = require('loopback'),
boot = require('loopback-boot'),
accepts = require('accepts'),
cookieParser = require('cookie-parser'),
compress = require('compression'),
session = require('express-session'),
expressState = require('express-state'),
logger = require('morgan'),
errorHandler = require('errorhandler'),
methodOverride = require('method-override'),
bodyParser = require('body-parser'),
helmet = require('helmet'),
MongoStore = require('connect-mongo')(session),
flash = require('express-flash'),
path = require('path'),
expressValidator = require('express-validator'),
lessMiddleware = require('less-middleware'),
passportProviders = require('./passport-providers'),
rxMiddleware = require('./utils/rx').rxMiddleware,
/**
* API keys and Passport configuration.
*/
secrets = require('./../config/secrets');
passportProviders = require('./passport-providers');
var generateKey =
require('loopback-component-passport/lib/models/utils').generateKey;
@ -45,176 +26,16 @@ var passportConfigurator = new PassportConfigurator(app);
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(compress());
app.use(lessMiddleware(path.join(__dirname, '/public')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(expressValidator({
customValidators: {
matchRegex: function(param, regex) {
return regex.test(param);
}
}
}));
app.use(methodOverride());
app.use(cookieParser(secrets.cookieSecret));
app.use(session({
resave: true,
saveUninitialized: true,
secret: secrets.sessionSecret,
store: new MongoStore({
url: secrets.db,
'autoReconnect': true
})
}));
app.use(flash());
app.disable('x-powered-by');
app.use(helmet.xssFilter());
app.use(helmet.noSniff());
app.use(helmet.frameguard());
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
var trusted = [
"'self'",
'blob:',
'104.236.218.15',
'*.freecodecamp.com',
'http://www.freecodecamp.com',
'https://www.freecodecamp.com',
'https://freecodecamp.com',
'https://freecodecamp.org',
'*.freecodecamp.org',
// NOTE(berks): add the following as the blob above was not covering www
'http://www.freecodecamp.org',
'ws://freecodecamp.com/',
'ws://www.freecodecamp.com/',
'*.gstatic.com',
'*.google-analytics.com',
'*.googleapis.com',
'*.google.com',
'*.gstatic.com',
'*.doubleclick.net',
'*.twitter.com',
'*.twitch.tv',
'*.twimg.com',
"'unsafe-eval'",
"'unsafe-inline'",
'*.bootstrapcdn.com',
'*.cloudflare.com',
'https://*.cloudflare.com',
'localhost:3001',
'ws://localhost:3001/',
'http://localhost:3001',
'localhost:3000',
'ws://localhost:3000/',
'http://localhost:3000',
'*.ionicframework.com',
'https://syndication.twitter.com',
'*.youtube.com',
'*.jsdelivr.net',
'https://*.jsdelivr.net',
'*.ytimg.com',
'*.bitly.com',
'http://cdn.inspectlet.com/',
'https://cdn.inspeclet.com/',
'wss://inspectletws.herokuapp.com/',
'http://hn.inspectlet.com/',
'*.googleapis.com',
'*.gstatic.com',
'https://hn.inspectlet.com/'
];
app.use(helmet.csp({
defaultSrc: trusted,
scriptSrc: [
'*.optimizely.com',
'*.aspnetcdn.com',
'*.d3js.org',
'https://cdn.inspectlet.com/inspectlet.js',
'http://cdn.inspectlet.com/inspectlet.js',
'http://beta.freecodecamp.com'
].concat(trusted),
'connect-src': [
'vimeo.com'
].concat(trusted),
styleSrc: [
'*.googleapis.com',
'*.gstatic.com'
].concat(trusted),
imgSrc: [
/* allow all input since we have user submitted images for public profile*/
'*'
].concat(trusted),
fontSrc: [
'*.googleapis.com',
'*.gstatic.com'
].concat(trusted),
mediaSrc: [
'*.amazonaws.com',
'*.twitter.com'
].concat(trusted),
frameSrc: [
'*.gitter.im',
'*.gitter.im https:',
'*.vimeo.com',
'*.twitter.com',
'*.ghbtns.com'
].concat(trusted),
// set to true if you only want to report errors
reportOnly: false,
// set to true if you want to set all headers
setAllHeaders: false,
// set to true if you want to force buggy CSP in Safari 5
safari5: false
}));
// adds passport initialization after session middleware phase is complete
passportConfigurator.init();
// add rx methods to express
app.use(rxMiddleware());
app.use(function(req, res, next) {
// Make user object available in templates.
res.locals.user = req.user;
next();
});
app.use(
loopback.static(path.join(__dirname, '../public'), {
maxAge: 86400000
})
);
boot(app, {
appRootDir: __dirname,
dev: process.env.NODE_ENV
});
app.use(function(req, res, next) {
// Remember original destination before login.
var path = req.path.split('/')[1];
if (/auth|login|logout|signin|signup|fonts|favicon/i.test(path)) {
return next();
} else if (/\/stories\/\w+/i.test(req.path)) {
return next();
}
req.session.returnTo = req.path;
next();
});
passportConfigurator.setupModels({
userModel: app.models.user,
@ -244,6 +65,9 @@ var passportOptions = {
if (email) {
userObj.email = email;
}
if (provider === 'github-login') {
userObj.isGithubCool = true;
}
return userObj;
}
};
@ -257,58 +81,6 @@ Object.keys(passportProviders).map(function(strategy) {
);
});
/**
* OAuth sign-in routes.
*/
/**
* 500 Error Handler.
*/
if (process.env.NODE_ENV === 'development') {
app.use(errorHandler({
log: true
}));
} else {
app.use(pmx.expressErrorHandler());
// error handling in production disabling eslint due to express parity rules
// for error handlers
app.use(function(err, req, res, next) { // eslint-disable-line
// respect err.status
if (err.status) {
res.statusCode = err.status;
}
// default status code to 500
if (res.statusCode < 400) {
res.statusCode = 500;
}
// parse res type
var accept = accepts(req);
var type = accept.type('html', 'json', 'text');
var message = 'opps! Something went wrong. Please try again later';
if (type === 'html') {
req.flash('errors', {
msg: message
});
return res.redirect('/');
// json
} else if (type === 'json') {
res.setHeader('Content-Type', 'application/json');
return res.send({
message: message
});
// plain text
} else {
res.setHeader('Content-Type', 'text/plain');
return res.send(message);
}
});
}
app.start = function() {
app.listen(app.get('port'), function() {
app.emit('started');

View File

@ -30,12 +30,3 @@ exports.observableQueryFromModel =
exports.observeMethod = function observeMethod(context, methodName) {
return Rx.Observable.fromNodeCallback(context[methodName], context);
};
// add rx methods to express
exports.rxMiddleware = function rxMiddleware() {
return function rxMiddleware(req, res, next) {
// render to observable
res.render$ = Rx.Observable.fromNodeCallback(res.render, res);
next();
};
};

View File

@ -268,7 +268,7 @@ block content
| Link GitHub with my account
if (!user.twitter)
.col-xs-12
a.btn.btn-lg.btn-block.btn-twitter.btn-link-social(href='/auth/twitter')
a.btn.btn-lg.btn-block.btn-twitter.btn-link-social.disabled(href='#')
i.fa.fa-twitter
| Link Twitter with my account
if (!user.google)

View File

@ -2,7 +2,7 @@ extends ../layout
block content
.jumbotron.text-center
h2 Sign in with one of these options:
a.btn.btn-lg.btn-block.btn-twitter.btn-social(href='/auth/github')
a.btn.btn-lg.btn-block.btn-github.btn-social(href='/auth/github')
i.fa.fa-github
| Sign in with Github
a.btn.btn-lg.btn-block.btn-twitter.btn-social(href='/auth/twitter')

View File

@ -1,5 +1,8 @@
extends layout
block content
.bg-danger
a(href='https://github.com/FreeCodeCamp/freecodecamp/wiki/beta before continuing')
h3 Warning! You are on beta! Please read this link before continuing! https://github.com/freecodecamp/freecodecamp/wiki/beta
.jumbotron
.text-center
h1.hug-top Code with Us