app.js
var newrelic;
if (process.env.NEW_RELIC_HOME) {
newrelic = require('newrelic');
} else {
newrelic = {
getBrowserTimingHeader: function () {
return '<!-- New Relic RUM disabled -->';
}
};
}
var express = require('express'),
domain = require('domain'),
cluster = require('cluster'),
Habitat = require('habitat'),
helmet = require('helmet'),
http = require('http'),
middleware = require('./lib/middleware'),
nunjucks = require('nunjucks'),
path = require('path'),
lessMiddleWare = require('less-middleware'),
i18n = require('webmaker-i18n'),
WebmakerAuth = require('webmaker-auth'),
navigation = require('./navigation'),
markdown = require('markdown').markdown,
proxy = require('proxy-middleware'),
url = require('url');
Habitat.load();
var app = express(),
env = new Habitat(),
nunjucksEnv = nunjucks.configure([path.join(__dirname, '/views'), path.join(__dirname, '/bower_components')], {
autoescape: true,
watch: false
}),
NODE_ENV = env.get('NODE_ENV'),
WWW_ROOT = path.resolve(__dirname, 'public'),
server,
messina,
logger;
var flags = env.get('FLAGS') || {};
nunjucksEnv.addFilter('instantiate', function (input) {
return nunjucks.renderString(input, this.getVariables());
});
// `localVar` filter accepting two parameters
// one is the input from 'gettext()' and another is the function itself
// the use case is {{ gettext('some input') | localVar(object) }}
// if the key name is 'some input': 'My name is {{name}}'
// tmpl.render(localVar) will try to render it with the available variable from the
// `localVar` object and return something like `My name is Ali`
nunjucksEnv.addFilter('localVar', function (input, localVar) {
return nunjucks.renderString(input, localVar);
});
// Make the client-side gettext possible!!
nunjucksEnv.addFilter('gettext', function (string) {
return this.lookup('gettext')(string);
});
// For navigation
nunjucksEnv.addFilter('getSection', function (pageId) {
var id = '';
navigation.forEach(function (section) {
if (section.exclude && flags[section.exclude]) {
return;
} else if (section.flag && !flags[section.flag]) {
return;
}
section.pages.forEach(function (page) {
if (page.id === pageId) {
id = section.id;
}
});
});
return id;
});
// Markdown
nunjucksEnv.addFilter('markdown', function (string) {
return markdown.toHTML(string);
});
if (!(env.get('MAKE_ENDPOINT') && env.get('MAKE_PRIVATEKEY') && env.get('MAKE_PUBLICKEY'))) {
throw new Error('MakeAPI Config setting invalid or missing!');
}
// Initialize make client so it is available to other modules
require('./lib/makeapi')({
readOnlyURL: env.get('MAKE_ENDPOINT_READONLY') || env.get('MAKE_ENDPOINT'),
authenticatedURL: env.get('MAKE_ENDPOINT'),
hawk: {
key: env.get('MAKE_PRIVATEKEY'),
id: env.get('MAKE_PUBLICKEY'),
algorithm: 'sha256'
}
});
var webmakerAuth = new WebmakerAuth({
loginURL: env.get('LOGIN'),
authLoginURL: env.get('LOGINAPI'),
loginHost: env.get('LOGIN_EMAIL_URL'),
secretKey: env.get('SESSION_SECRET'),
forceSSL: env.get('FORCE_SSL'),
domain: env.get('COOKIE_DOMAIN')
});
var routes = require('./routes');
nunjucksEnv.express(app);
app.disable('x-powered-by');
if (env.get('ENABLE_GELF_LOGS')) {
messina = require('messina');
logger = messina('webmaker.org-' + env.get('NODE_ENV') || 'development');
logger.init();
app.use(logger.middleware());
} else {
app.use(express.logger('dev'));
}
// Setup locales with i18n
app.use(i18n.middleware({
supported_languages: env.get('SUPPORTED_LANGS'),
default_lang: 'en-US',
mappings: require('webmaker-locale-mapping'),
translation_directory: path.resolve(__dirname, 'locale')
}));
// Proxy to profile-2
if (env.get('PROFILE_URL')) {
app.use('/user', proxy(url.parse(env.get('PROFILE_URL'))));
}
app.use(require('prerender-node'));
app.use(helmet.iexss());
app.use(helmet.contentTypeOptions());
app.use(helmet.xframe('allow-from', 'http://optimizely.com'));
if ( !! env.get('FORCE_SSL')) {
app.use(helmet.hsts());
app.enable('trust proxy');
}
app.use(function (req, res, next) {
var guard = domain.create();
guard.add(req);
guard.add(res);
// Safely run a function in error isolation
function isolate(fn) {
try {
fn();
} catch (e) {
console.error('Internal error isolating shutdown sequence: ' + e);
}
}
guard.on('error', function (err) {
try {
// Make sure we close down within 15 seconds
var killtimer = setTimeout(function () {
process.exit(1);
}, 15000);
// But don't keep the process open just for that!
killtimer.unref();
console.error(err);
// Try and shutdown the server, cluster worker
isolate(function () {
server.close();
if (cluster.worker) {
cluster.worker.disconnect();
}
});
// Try sending a pretty 500 to the user
isolate(function () {
if (res._headerSent || res.finished) {
return;
}
res.statusCode = 500;
res.render('error.html', {
message: err.message,
code: err.status
});
});
guard.dispose();
} catch (err2) {
logger.error('Internal error shutting down domain: ', err2.stack);
}
process.exit(1);
});
guard.run(next);
});
var optimize = NODE_ENV !== 'development';
// goggles pages are straight up redirect to goggles.mozilla.org now
app.use(function gogglesRedirect(req, res, next) {
var path = req.path;
if (path.match(/\/goggles(\/.*)*$/)) {
res.redirect('https://goggles.mozilla.org');
} else {
next();
}
});
// convert requests for ltr- or rtl-specific CSS back to the real filename,
// as the rtltr-for-less package was a hack that was never meant to hit production.
app.use(function rtltrRedirect(req, res, next) {
var path = req.path;
if (path.match(/\w+\.(ltr|rtl)\.css/)) {
res.redirect(path.replace(/\.(ltr|rtl)/, ""));
} else {
next();
}
});
app.use(lessMiddleWare({
once: optimize,
debug: !optimize,
dest: '/css',
src: '../less',
root: WWW_ROOT,
compress: optimize,
yuicompress: optimize,
optimization: optimize ? 0 : 2,
sourceMap: !optimize
}));
app.use(express.compress());
app.use(express.static(WWW_ROOT));
app.use('/bower_components', express.static(path.join(__dirname, 'bower_components')));
app.use(express.json());
app.use(express.urlencoded());
app.use(webmakerAuth.cookieParser());
app.use(webmakerAuth.cookieSession());
// Adding an external JSON file to our existing one for the specified locale
var webmakerLoginJSON = require('./bower_components/webmaker-login-ux/locale/en_US/webmaker-login.json');
var weblitLocaleJSON = require('./node_modules/web-literacy-client/dist/weblitmap.json');
i18n.addLocaleObject({
'en-US': webmakerLoginJSON
}, function (err, res) {
if (err) {
console.error(err);
}
});
i18n.addLocaleObject({
'en-US': weblitLocaleJSON
}, function (err, res) {
if (err) {
console.error(err);
}
});
app.use(express.csrf());
app.locals({
makeEndpoint: env.get('MAKE_ENDPOINT_READONLY') || env.get('MAKE_ENDPOINT'),
newrelic: newrelic,
personaSSO: env.get('AUDIENCE'),
loginAPI: env.get('LOGIN'),
ga_account: env.get('GA_ACCOUNT'),
ga_domain: env.get('GA_DOMAIN'),
languages: i18n.getSupportLanguages(),
EVENTS_URL: env.get('EVENTS_URL'),
TEACH_URL: env.get('TEACH_URL'),
flags: flags,
personaHostname: env.get('PERSONA_HOSTNAME', 'https://login.persona.org'),
bower_path: '../bower_components'
});
app.use(function (req, res, next) {
var user = req.session.user;
res.locals({
wlcPoints: require('./lib/web-literacy-points.json'),
currentPath: req.path,
returnPath: req.param('page'),
email: user ? user.email : '',
username: user ? user.username : '',
isAdmin: user ? user.isAdmin : false,
isSuperMentor: user ? user.isSuperMentor : false,
isMentor: user ? user.isMentor : false,
makerID: user ? user.id : '',
csrf: req.csrfToken(),
navigation: navigation,
gettext: req.gettext,
campaignHeader: app.locals.flags.campaign ? '' + (Math.floor(Math.random() * +app.locals.flags.campaign)) : 0
});
next();
});
// Nunjucks
// This just uses nunjucks-dev for now -- middleware to handle compiling templates in progress
app.use('/templates', express.static(path.join(__dirname, 'views')));
// adding Content Security Policy (CSP) to webmaker.org
app.use(middleware.addCSP());
app.use(app.router);
// We've run out of known routes, 404
app.use(function (req, res, next) {
res.status(404);
res.render('error.html', {
code: 404
});
});
// Final error-handling middleware
app.use(function (err, req, res, next) {
if (typeof err === 'string') {
console.error('You\'re passing a string into next(). Go fix this: %s', err);
}
var error = {
message: err.toString(),
code: http.STATUS_CODES[err.status] ? err.status : 500
};
console.error(err, err.stack);
res.status(error.code);
res.format({
'text/html': function () {
res.render('error.html', error);
},
'application/json': function () {
res.send(err.message);
}
});
});
var middleware = require('./lib/middleware');
// ROUTES
app.post('/verify', webmakerAuth.handlers.verify);
app.post('/authenticate', webmakerAuth.handlers.authenticate);
app.post('/logout', webmakerAuth.handlers.logout);
app.post('/auth/v2/create', webmakerAuth.handlers.createUser);
app.post('/auth/v2/uid-exists', webmakerAuth.handlers.uidExists);
app.post('/auth/v2/request', webmakerAuth.handlers.request);
app.post('/auth/v2/authenticateToken', webmakerAuth.handlers.authenticateToken);
app.post('/auth/v2/verify-password', webmakerAuth.handlers.verifyPassword);
app.post('/auth/v2/request-reset-code', webmakerAuth.handlers.requestResetCode);
app.post('/auth/v2/reset-password', webmakerAuth.handlers.resetPassword);
// These webmaker-auth route handlers require a csrf token and a valid user session.
app.post('/auth/v2/remove-password', webmakerAuth.handlers.removePassword);
app.post('/auth/v2/enable-passwords', webmakerAuth.handlers.enablePasswords);
app.get('/healthcheck', routes.api.healthcheck);
app.get('/signup/:auth?', routes.angular);
// Angular
app.get('/tools', routes.angular);
app.get('/remix-your-school', routes.angular);
app.get('/music-video', routes.angular);
app.get('/private-eye', routes.angular);
app.get('/appmaker', routes.angular);
app.get('/feedback', routes.angular);
app.get('/getinvolved', routes.angular);
app.get('/about', routes.angular);
app.get('/make-your-own', routes.angular);
app.get('/madewithcode-*', routes.angular);
app.get('/home-:variant', middleware.homePageRedirect, routes.angular);
app.get('/explore', routes.gallery({
layout: 'index',
noPrefix: true
}));
app.get('/gallery/list/:list', routes.gallery_old({
layout: 'index',
prefix: 'p'
}));
app.get('/editor', middleware.checkAdmin, routes.gallery_old({
page: 'editor'
}));
app.get('/privacy-makes', routes.gallery_old({
layout: 'privacy-makes',
prefix: 'privacy',
limit: 20
}));
// Initialize badges routes
var badgesRoutes = routes.badges(env);
// Badge pages
app.get('/badges/:badge?', badgesRoutes.details);
// Badges admin
app.get('/admin/badges', badgesRoutes.middleware.atleast('isMentor'), routes.angular);
app.get('/admin/create-badge', badgesRoutes.middleware.atleast('isAdmin'), routes.angular);
app.get('/admin/badges/:badge', badgesRoutes.middleware.hasPermissions('viewInstances'), routes.angular);
app.get('/admin/badges/:badge/update', badgesRoutes.middleware.atleast('isAdmin'), routes.angular);
// Badges API
app.get('/api/badges', badgesRoutes.getAll);
app.post('/api/badges/create', badgesRoutes.create);
app.get('/api/badges/:badge', badgesRoutes.getBadge);
app.post('/api/badges/:badge/update', badgesRoutes.middleware.atleast('isAdmin'), badgesRoutes.update);
app.post('/api/badges/:badge/apply', badgesRoutes.apply);
app.post('/api/badges/:badge/claim', badgesRoutes.claim);
app.post('/api/badges/:badge/issue', badgesRoutes.middleware.hasPermissions('issue'), badgesRoutes.issue);
app.get('/api/badges/:badge/instances',
badgesRoutes.middleware.hasPermissions('viewInstances'),
badgesRoutes.getInstances);
app.get('/api/badges/:badge/applications',
badgesRoutes.middleware.hasPermissions('applications'),
badgesRoutes.getApplications);
app.post('/api/badges/:badge/applications/:application/review',
badgesRoutes.middleware.hasPermissions('applications'),
badgesRoutes.submitReview);
app.delete('/api/badges/:badge/instance/email/:email',
badgesRoutes.middleware.hasPermissions('delete'),
badgesRoutes.deleteInstance);
app.post('/api/submit-resource', routes.api.submitResource);
app.get('/search', routes.search);
// MOI splash page
app.get('/localweb', routes.page('localweb'));
// Old route - turned into a 301 (perm. redirect) on 2014-02-11.
// This route should not be removed until sufficient time
// has passed for search engines to index the new URL.
var literacyRedirect = function (req, res) {
res.redirect(301, req.path.replace('standard', 'literacy'));
};
app.get('/standard', literacyRedirect);
app.get('/standard/*', literacyRedirect);
app.get('/style-guide', routes.page('style-guide'));
app.get('/details', middleware.removeXFrameOptions, routes.details);
// Old
app.get('/details/:id', middleware.removeXFrameOptions, function (req, res) {
res.redirect('/details?id=' + req.params.id);
});
app.get('/me', routes.me);
// Old
app.get('/myprojects', routes.me);
app.post('/remove', routes.remove);
app.post('/like', routes.like.like);
app.post('/unlike', routes.like.unlike);
app.post('/report', routes.report.report);
app.post('/cancelReport', routes.report.cancelReport);
app.get('/t/:tag', routes.tag);
app.get('/u/:user', routes.usersearch);
app.get('/terms', routes.angular);
app.get('/privacy', routes.angular);
app.get('/languages', routes.page('languages'));
app.get('/app', routes.app);
app.post('/app/send-download-link', routes.api.sendSMS);
app.get('/sitemap.xml', function (req, res) {
res.type('xml');
res.render('sitemap.xml');
});
// Localized Strings
app.get('/strings/:lang?', i18n.stringsRoute('en-US'));
var accountSettingsUrl = env.get('LOGIN') + '/account';
var makeApiUrl = env.get('MAKE_ENDPOINT');
var eventsUrl = env.get('EVENTS_URL');
var teachUrl = env.get('TEACH_URL');
var gogglesUrl = env.get('XRAY_GOGGLES_URL');
app.get('/angular-config.js', function (req, res) {
// Angular config
var angularConfig = {
accountSettingsUrl: accountSettingsUrl,
makeApiUrl: makeApiUrl,
eventsUrl: eventsUrl,
teachUrl: teachUrl,
lang: req.localeInfo.lang,
localeInfo: req.localeInfo,
direction: req.localeInfo.direction,
defaultLang: 'en-US',
supportLang: i18n.getLanguages(),
supported_languages: i18n.getSupportLanguages(),
langmap: i18n.getAllLocaleCodes(),
csrf: req.csrfToken(),
wlcPoints: res.locals.wlcPoints,
gogglesUrl: gogglesUrl
};
res.setHeader('Content-type', 'text/javascript');
res.send('window.angularConfig = ' + JSON.stringify(angularConfig));
});
app.get('/localeInfo', function (req, res) {
req.localeInfo.didYouKnowLocale = {};
req.localeInfo.otherLangPrefs.forEach(function (item, i) {
req.localeInfo.didYouKnowLocale[item] = i18n.gettext('Did you know Webmaker is also available in', item);
});
res.setHeader('Content-type', 'text/javascript');
res.send(JSON.stringify(req.localeInfo));
});
/**
* Legacy Webmaker Redirects
*/
require('./routes/redirect')(app);
server = app.listen(env.get('PORT'), function () {
console.log('Server listening ( http://localhost:%d )', env.get('PORT'));
});