chore(server): Move api-server in to it's own DIR

This commit is contained in:
Bouncey
2018-08-31 16:04:04 +01:00
committed by mrugesh mohapatra
parent 9fba6bce4c
commit 46a217d0a5
369 changed files with 328 additions and 7431 deletions

View File

@ -0,0 +1,43 @@
const pathsOfNoReturn = [
'link',
'auth',
'login',
'logout',
'signin',
'signup',
'fonts',
'favicon',
'js',
'css'
];
const pathsWhiteList = [
'news',
'challenges',
'map',
'news',
'commit'
];
const pathsOfNoReturnRegex = new RegExp(pathsOfNoReturn.join('|'), 'i');
const whiteListRegex = new RegExp(pathsWhiteList.join('|'), 'i');
export default function addReturnToUrl() {
return function(req, res, next) {
// Remember original destination before login.
var path = req.path.split('/')[1];
if (
req.method !== 'GET' ||
pathsOfNoReturnRegex.test(path) ||
!whiteListRegex.test(path) ||
(/hot/i).test(req.path)
) {
return next();
}
req.session.returnTo = req.originalUrl.includes('/map') ?
'/' :
req.originalUrl;
return 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';
const cookieSecret = process.env.COOKIE_SECRET;
export default cookieParser.bind(cookieParser, cookieSecret);

View File

@ -0,0 +1,98 @@
import helmet from 'helmet';
let trusted = [
"'self'",
'https://search.freecodecamp.org',
'https://www.freecodecamp.rocks',
'https://api.freecodecamp.rocks',
'https://' + process.env.AUTH0_DOMAIN
];
const host = process.env.HOST || 'localhost';
const port = process.env.SYNC_PORT || '3000';
if (process.env.NODE_ENV !== 'production') {
trusted = trusted.concat([
`ws://${host}:${port}`
]);
}
export default function csp() {
return helmet.contentSecurityPolicy({
directives: {
defaultSrc: trusted.concat([
'https://*.cloudflare.com',
'*.cloudflare.com'
]),
connectSrc: trusted.concat([
'https://glitch.com',
'https://*.glitch.com',
'https://*.glitch.me',
'https://*.cloudflare.com',
'https://*.algolia.net'
]),
scriptSrc: [
"'unsafe-eval'",
"'unsafe-inline'",
'*.google-analytics.com',
'*.gstatic.com',
'https://*.cloudflare.com',
'*.cloudflare.com',
'https://*.gitter.im',
'https://*.cdnjs.com',
'*.cdnjs.com',
'https://*.jsdelivr.com',
'*.jsdelivr.com',
'*.twimg.com',
'https://*.twimg.com',
'*.youtube.com',
'*.ytimg.com'
].concat(trusted),
styleSrc: [
"'unsafe-inline'",
'*.gstatic.com',
'*.googleapis.com',
'*.bootstrapcdn.com',
'https://*.bootstrapcdn.com',
'*.cloudflare.com',
'https://*.cloudflare.com',
'https://use.fontawesome.com'
].concat(trusted),
fontSrc: [
'*.cloudflare.com',
'https://*.cloudflare.com',
'*.bootstrapcdn.com',
'*.googleapis.com',
'*.gstatic.com',
'https://*.bootstrapcdn.com',
'https://use.fontawesome.com'
].concat(trusted),
imgSrc: [
// allow all input since we have user submitted images for
// public profile
'*',
'data:'
],
mediaSrc: [
'*.bitly.com',
'*.amazonaws.com',
'*.twitter.com'
].concat(trusted),
frameSrc: [
'*.gitter.im',
'*.gitter.im https:',
'*.youtube.com',
'*.twitter.com',
'*.ghbtns.com',
'*.freecatphotoapp.com',
'freecodecamp.github.io'
].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,19 @@
import csurf from 'csurf';
export default function() {
const protection = csurf(
{
cookie: {
domain: process.env.COOKIE_DOMAIN || 'localhost'
}
}
);
return function csrf(req, res, next) {
const path = req.path.split('/')[1];
if (/(^api$|^external$|^internal$|^p$)/.test(path)) {
return next();
}
return protection(req, res, next);
};
}

View File

@ -0,0 +1,34 @@
import dedent from 'dedent';
const ALLOWED_METHODS = ['GET'];
const EXCLUDED_PATHS = [
'/api/flyers/findOne',
'/signout',
'/accept-privacy-terms',
'/update-email',
'/confirm-email',
'/passwordless-change',
'/external/services/user'
];
export default function emailNotVerifiedNotice() {
return function(req, res, next) {
if (
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
EXCLUDED_PATHS.indexOf(req.path) === -1
) {
const { user } = req;
if (user && (!user.email || user.email === '' || !user.emailVerified)) {
req.flash(
'info',
dedent`
New privacy laws now require that we have an email address where we can reach
you. Please update your email address in the <a href='/settings'>settings</a>
and click the link we send you to confirm.
`
);
}
}
return next();
};
}

View File

@ -0,0 +1,90 @@
// import { inspect } from 'util';
// import _ from 'lodash/fp';
import accepts from 'accepts';
import { homeLocation } from '../../../config/env';
import { unwrapHandledError } from '../utils/create-handled-error.js';
const isDev = process.env.NODE_ENV !== 'production';
// const toString = Object.prototype.toString;
// is full error or just trace
// _.toString(new Error('foo')) => "Error: foo
// Object.prototype.toString.call(new Error('foo')) => "[object Error]"
// const isInspect = val => !val.stack && _.toString(val) === toString.call(val);
// const stringifyErr = val => {
// if (val.stack) {
// return String(val.stack);
// }
// const str = String(val);
// return isInspect(val) ?
// inspect(val) :
// str;
// };
// const createStackHtml = _.flow(
// _.cond([
// [isInspect, err => [err]],
// // may be stack or just err.msg
// [_.stubTrue, _.flow(stringifyErr, _.split('\n'), _.tail) ]
// ]),
// _.map(_.escape),
// _.map(line => `<li>${line}</lin>`),
// _.join('')
// );
// const createErrorTitle = _.cond([
// [
// _.negate(isInspect),
// _.flow(stringifyErr, _.split('\n'), _.head, _.defaultTo('Error'))
// ],
// [_.stubTrue, _.constant('Error')]
// ]);
export default function prodErrorHandler() {
// error handling in production.
// disabling eslint due to express parity rules for error handlers
return function(err, req, res, next) {
// eslint-disable-line
const handled = unwrapHandledError(err);
// respect handled error status
let status = handled.status || err.status || res.statusCode;
if (!handled.status && status < 400) {
status = 500;
}
res.status(status);
// parse res type
const accept = accepts(req);
const type = accept.type('html', 'json', 'text');
const redirectTo = handled.redirectTo || `${homeLocation}/`;
const message =
handled.message || 'Oops! Something went wrong. Please try again later';
if (isDev) {
console.error(err);
}
if (type === 'html') {
if (typeof req.flash === 'function') {
req.flash(handled.type || 'danger', message);
}
return res.redirectWithFlash(redirectTo);
// json
} else if (type === 'json') {
res.setHeader('Content-Type', 'application/json');
return res.send({
type: handled.type || 'errors',
message
});
// plain text
} else {
res.setHeader('Content-Type', 'text/plain');
return res.send(message);
}
};
}

View File

@ -0,0 +1,49 @@
import debug from 'debug';
import Rollbar from 'rollbar';
import {
isHandledError,
unwrapHandledError
} from '../utils/create-handled-error.js';
const { ROLLBAR_APP_ID } = process.env;
const rollbar = new Rollbar(ROLLBAR_APP_ID);
const log = debug('fcc:middlewares:error-reporter');
const errTemplate = ({message, ...restError}, req) => `
Time: ${new Date(Date.now()).toISOString()}
Error: ${message}
Is authenticated user: ${!!req.user}
Route: ${JSON.stringify(req.route, null, 2)}
${JSON.stringify(restError, null, 2)}
`;
export default function errrorReporter() {
if (process.env.NODE_ENV !== 'production' && process.env.ERROR_REPORTER) {
return (err, req, res, next) => {
console.error(errTemplate(err, req));
if (isHandledError(err)) {
// log out user messages in development
const handled = unwrapHandledError(err);
log(handled.message);
}
next(err);
};
}
return (err, req, res, next) => {
// handled errors do not need to be reported,
// they report a message and maybe redirect the user
// errors with status codes shouldn't be reported
// as they are usually user messages
if (isHandledError(err) || err.statusCode || err.status) {
return next(err);
}
// logging the error provides us with more information,
// i.e isAuthenticatedUser, req.route
console.error(errTemplate(err, req));
return rollbar.error(err.message, err);
};
}

View File

@ -0,0 +1,23 @@
import qs from 'query-string';
// add rx methods to express
export default function() {
return function expressExtensions(req, res, next) {
res.redirectWithFlash = uri => {
const flash = req.flash();
res.redirect(
`${uri}?${qs.stringify(
{ messages: qs.stringify(flash, { arrayFormat: 'index' }) },
{ arrayFormat: 'index' }
)}`
);
};
res.sendFlash = (type, message) => {
if (type && message) {
req.flash(type, message);
}
return res.json(req.flash());
};
next();
};
}

View File

@ -0,0 +1,30 @@
import dedent from 'dedent';
const ALLOWED_METHODS = ['GET'];
const EXCLUDED_PATHS = [
'/api/flyers/findOne',
'/challenges/current-challenge',
'/challenges/next-challenge',
'/map-aside',
'/signout'
];
export default function flashCheaters() {
return function(req, res, next) {
if (
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
EXCLUDED_PATHS.indexOf(req.path) === -1 &&
req.user && req.url !== '/' && req.user.isCheater
) {
req.flash(
'danger',
dedent`
Upon review, this account has been flagged for academic
dishonesty. If youre the owner of this account contact
team@freecodecamp.org for details.
`
);
}
return next();
};
}

View File

@ -0,0 +1,59 @@
import _ from 'lodash';
import manifest from '../rev-manifest';
let chunkManifest;
try {
chunkManifest = require('../manifests/chunk-manifest.json');
} catch (err) {
chunkManifest = {};
}
chunkManifest = Object.keys(chunkManifest).reduce((manifest, key) => {
manifest[key] = '/' + chunkManifest[key];
return manifest;
}, {});
const isDev = process.env.NODE_ENV !== 'production';
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint):\s/i;
function rev(scopedPrepend, asset) {
if (isDev) {
// do not use revision in dev mode
return `${scopedPrepend}/${asset}`;
}
return `${scopedPrepend}/${ manifest[asset] || asset }`;
}
function removeOldTerms(str = '') {
return str.replace(challengesRegex, '');
}
const cacheBreaker = isDev ?
// add cacheBreaker in dev instead of rev manifest
asset => `${asset}?cacheBreaker=${Math.random()}` :
_.identity;
export default function jadeHelpers() {
return function jadeHelpersMiddleware(req, res, next) {
Object.assign(
res.locals,
{
removeOldTerms,
rev,
cacheBreaker,
// static data
user: req.user,
chunkManifest,
_csrf: req.csrfToken ? req.csrfToken() : null,
theme: req.user &&
req.user.theme ||
req.cookies.theme ||
'default'
}
);
if (req.csrfToken) {
res.expose({ token: res.locals._csrf }, 'csrf');
}
next();
};
}

View File

@ -0,0 +1,69 @@
import loopback from 'loopback';
import jwt from 'jsonwebtoken';
import { isBefore } from 'date-fns';
import { homeLocation } from '../../../config/env';
import { wrapHandledError } from '../utils/create-handled-error';
export default () => function authorizeByJWT(req, res, next) {
const path = req.path.split('/')[1];
if (/^external$|^internal$/.test(path)) {
const cookie = req.signedCookies && req.signedCookies['jwt_access_token'] ||
req.cookie && req.cookie['jwt_access_token'];
if (!cookie) {
throw wrapHandledError(
new Error('Access token is required for this request'),
{
type: 'info',
redirect: `${homeLocation}/signin`,
message: 'Access token is required for this request',
status: 403
}
);
}
let token;
try {
token = jwt.verify(cookie, process.env.JWT_SECRET);
} catch (err) {
throw wrapHandledError(
new Error(err.message),
{
type: 'info',
redirect: `${homeLocation}/signin`,
message: 'Your access token is invalid',
status: 403
}
);
}
const { accessToken: {created, ttl, userId }} = token;
const valid = isBefore(Date.now(), Date.parse(created) + ttl);
if (!valid) {
throw wrapHandledError(
new Error('Access token is no longer vaild'),
{
type: 'info',
redirect: `${homeLocation}/signin`,
message: 'Access token is no longer vaild',
status: 403
}
);
}
if (!req.user) {
const User = loopback.getModelByType('User');
return User.findById(userId)
.then(user => {
if (user) {
user.points = user.progressTimestamps.length;
req.user = user;
}
return;
})
.then(next)
.catch(next);
} else {
return next();
}
}
return next();
};

View File

@ -0,0 +1,21 @@
import _ from 'lodash';
import { Observable } from 'rx';
import { login } from 'passport/lib/http/request';
// make login polymorphic
// if supplied callback it works as normal
// if called without callback it returns an observable
// login(user, options?, cb?) => Void|Observable
function login$(...args) {
console.log('args');
if (_.isFunction(_.last(args))) {
return login.apply(this, args);
}
return Observable.fromNodeCallback(login).apply(this, args);
}
export default function passportLogin() {
return (req, res, next) => {
req.login = req.logIn = login$;
next();
};
}

View File

@ -0,0 +1,26 @@
const ALLOWED_METHODS = ['GET'];
const EXCLUDED_PATHS = [
'/api/flyers/findOne',
'/signout',
'/accept-privacy-terms',
'/update-email',
'/confirm-email',
'/passwordless-change',
'/external/services/user'
];
export default function privacyTermsNotAcceptedNotice() {
return function(req, res, next) {
if (
ALLOWED_METHODS.indexOf(req.method) !== -1 &&
EXCLUDED_PATHS.indexOf(req.path) === -1
) {
const { user } = req;
if (user && user.acceptedPrivacyTerms !== true) {
res.redirect('/accept-privacy-terms');
return next;
}
}
return next();
};
}

View File

@ -0,0 +1,20 @@
import session from 'express-session';
import MongoStoreFactory from 'connect-mongo';
const MongoStore = MongoStoreFactory(session);
const sessionSecret = process.env.SESSION_SECRET;
const url = process.env.MONGODB || process.env.MONGOHQ_URL;
export default function sessionsMiddleware() {
return session({
// 900 day session cookie
cookie: { maxAge: 900 * 24 * 60 * 60 * 1000 },
// resave forces session to be resaved
// regardless of whether it was modified
// this causes race conditions during parallel req
resave: false,
saveUninitialized: true,
secret: sessionSecret,
store: new MongoStore({ url })
});
}

View File

@ -0,0 +1,57 @@
import validator from 'express-validator';
import { isPoly } from '../../common/utils/polyvinyl';
const isObject = val => !!val && typeof val === 'object';
export default function() {
return validator({
customValidators: {
matchRegex(param, regex) {
return regex.test(param);
},
isString(value) {
return typeof value === 'string';
},
isNumber(value) {
return typeof value === 'number';
},
isFiles(value) {
if (!isObject(value)) {
return false;
}
const keys = Object.keys(value);
return !!keys.length &&
// every key is a file
keys.every(key => isObject(value[key])) &&
// every file has contents
keys.map(key => value[key]).every(file => isPoly(file));
}
},
customSanitizers: {
// Refer : http://stackoverflow.com/a/430240/1932901
trimTags(value) {
const tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*';
const tagOrComment = new RegExp(
'<(?:'
// Comment body.
+ '!--(?:(?:-*[^->])*--+|-?)'
// Special "raw text" elements whose content should be elided.
+ '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
+ '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
// Regular name
+ '|/?[a-z]'
+ tagBody
+ ')>',
'gi'
);
let rawValue;
do {
rawValue = value;
value = value.replace(tagOrComment, '');
} while (value !== rawValue);
return value.replace(/</g, '&lt;');
}
}
});
}