element with the class \"container-fluid\"."
],
"tests": [
"assert($('div').hasClass('container-fluid'), 'Your div
element should have the class \"container-fluid\"')",
@@ -1743,7 +1743,7 @@
],
"tests": [
"assert($('.btn').length > 5, 'Apply the \"btn\" class to each of your button
elements.')",
- "assert($('.btn').length > 5, 'Apply the \"btn-default\" class to each of your button
elements.')"
+ "assert($('.btn-default').length > 5, 'Apply the \"btn-default\" class to each of your button
elements.')"
],
"challengeSeed": [
"",
diff --git a/server/middleware.json b/server/middleware.json
index 664cacda58..10d78033a0 100644
--- a/server/middleware.json
+++ b/server/middleware.json
@@ -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": {}
}
}
diff --git a/server/middlewares/add-return-to.js b/server/middlewares/add-return-to.js
new file mode 100644
index 0000000000..9e9522b1ad
--- /dev/null
+++ b/server/middlewares/add-return-to.js
@@ -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();
+ };
+}
diff --git a/server/middlewares/constant-headers.js b/server/middlewares/constant-headers.js
new file mode 100644
index 0000000000..28a1ad32bd
--- /dev/null
+++ b/server/middlewares/constant-headers.js
@@ -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();
+ };
+}
diff --git a/server/middlewares/cookie-parser.js b/server/middlewares/cookie-parser.js
new file mode 100644
index 0000000000..a991b5dc7e
--- /dev/null
+++ b/server/middlewares/cookie-parser.js
@@ -0,0 +1,4 @@
+import cookieParser from 'cookie-parser';
+import secrets from '../../config/secrets';
+
+export default cookieParser.bind(cookieParser, secrets.cookieSecret);
diff --git a/server/middlewares/csp.js b/server/middlewares/csp.js
new file mode 100644
index 0000000000..8354ce1df9
--- /dev/null
+++ b/server/middlewares/csp.js
@@ -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
+ });
+}
diff --git a/server/middlewares/error-handlers.js b/server/middlewares/error-handlers.js
new file mode 100644
index 0000000000..c8eefaef08
--- /dev/null
+++ b/server/middlewares/error-handlers.js
@@ -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);
+ }
+ };
+}
diff --git a/server/middlewares/express-rx.js b/server/middlewares/express-rx.js
new file mode 100644
index 0000000000..d687e38363
--- /dev/null
+++ b/server/middlewares/express-rx.js
@@ -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();
+ };
+}
diff --git a/server/middlewares/global-locals.js b/server/middlewares/global-locals.js
new file mode 100644
index 0000000000..32a6148562
--- /dev/null
+++ b/server/middlewares/global-locals.js
@@ -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();
+ };
+}
diff --git a/server/middlewares/keymetrics.js b/server/middlewares/keymetrics.js
new file mode 100644
index 0000000000..0902d6caab
--- /dev/null
+++ b/server/middlewares/keymetrics.js
@@ -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();
+}
diff --git a/server/middlewares/sessions.js b/server/middlewares/sessions.js
new file mode 100644
index 0000000000..fc0e4ccc1b
--- /dev/null
+++ b/server/middlewares/sessions.js
@@ -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
+ })
+ });
+}
diff --git a/server/middlewares/validator.js b/server/middlewares/validator.js
new file mode 100644
index 0000000000..b40765bc89
--- /dev/null
+++ b/server/middlewares/validator.js
@@ -0,0 +1,9 @@
+import validator from 'express-validator';
+
+export default validator.bind(validator, {
+ customValidators: {
+ matchRegex: function matchRegex(param, regex) {
+ return regex.test(param);
+ }
+ }
+});
diff --git a/server/server.js b/server/server.js
index fe0e924a55..93b0a1a322 100755
--- a/server/server.js
+++ b/server/server.js
@@ -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');
diff --git a/server/utils/rx.js b/server/utils/rx.js
index ed7e0ebf73..7f0bac0704 100644
--- a/server/utils/rx.js
+++ b/server/utils/rx.js
@@ -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();
- };
-};
diff --git a/server/views/account/account.jade b/server/views/account/account.jade
index 0dc4e5cb2c..436651c1e0 100644
--- a/server/views/account/account.jade
+++ b/server/views/account/account.jade
@@ -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)
diff --git a/server/views/account/signin.jade b/server/views/account/signin.jade
index 0e2e22a9ab..67047098a5 100644
--- a/server/views/account/signin.jade
+++ b/server/views/account/signin.jade
@@ -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')
diff --git a/server/views/home.jade b/server/views/home.jade
index 7b25a30777..4e7a8f9c73 100644
--- a/server/views/home.jade
+++ b/server/views/home.jade
@@ -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