This prevents one to perform action on services that are still initializing (e.g. senders, where update of a send configuration causes config reload on the sender process, which may not be started yet and thus responds with error that send method is not defined)
394 lines
13 KiB
JavaScript
394 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const config = require('config');
|
|
const log = require('./lib/log');
|
|
|
|
const express = require('express');
|
|
const expressLocale = require('express-locale');
|
|
const bodyParser = require('body-parser');
|
|
const path = require('path');
|
|
const favicon = require('serve-favicon');
|
|
const logger = require('morgan');
|
|
const cookieParser = require('cookie-parser');
|
|
const session = require('express-session');
|
|
const RedisStore = require('connect-redis')(session);
|
|
const flash = require('connect-flash');
|
|
const hbs = require('hbs');
|
|
const compression = require('compression');
|
|
const passport = require('./lib/passport');
|
|
const contextHelpers = require('./lib/context-helpers');
|
|
|
|
const api = require('./routes/api');
|
|
|
|
// These are routes for the new React-based client
|
|
const reports = require('./routes/reports');
|
|
const subscriptions = require('./routes/subscriptions');
|
|
const subscription = require('./routes/subscription');
|
|
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
|
|
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
|
|
const sandboxedGrapesJS = require('./routes/sandboxed-grapesjs');
|
|
const sandboxedCodeEditor = require('./routes/sandboxed-codeeditor');
|
|
const files = require('./routes/files');
|
|
const links = require('./routes/links');
|
|
const archive = require('./routes/archive');
|
|
const webhooks = require('./routes/webhooks');
|
|
|
|
const namespacesRest = require('./routes/rest/namespaces');
|
|
const sendConfigurationsRest = require('./routes/rest/send-configurations');
|
|
const usersRest = require('./routes/rest/users');
|
|
const accountRest = require('./routes/rest/account');
|
|
const reportTemplatesRest = require('./routes/rest/report-templates');
|
|
const reportsRest = require('./routes/rest/reports');
|
|
const campaignsRest = require('./routes/rest/campaigns');
|
|
const triggersRest = require('./routes/rest/triggers');
|
|
const listsRest = require('./routes/rest/lists');
|
|
const formsRest = require('./routes/rest/forms');
|
|
const fieldsRest = require('./routes/rest/fields');
|
|
const importsRest = require('./routes/rest/imports');
|
|
const importRunsRest = require('./routes/rest/import-runs');
|
|
const sharesRest = require('./routes/rest/shares');
|
|
const segmentsRest = require('./routes/rest/segments');
|
|
const subscriptionsRest = require('./routes/rest/subscriptions');
|
|
const templatesRest = require('./routes/rest/templates');
|
|
const mosaicoTemplatesRest = require('./routes/rest/mosaico-templates');
|
|
const blacklistRest = require('./routes/rest/blacklist');
|
|
const editorsRest = require('./routes/rest/editors');
|
|
const filesRest = require('./routes/rest/files');
|
|
const settingsRest = require('./routes/rest/settings');
|
|
|
|
const index = require('./routes/index');
|
|
|
|
const interoperableErrors = require('../shared/interoperable-errors');
|
|
|
|
const { getTrustedUrl } = require('./lib/urls');
|
|
const { AppType } = require('../shared/app');
|
|
|
|
|
|
let isReady = false;
|
|
function setReady() {
|
|
isReady = true;
|
|
}
|
|
|
|
|
|
hbs.registerPartials(__dirname + '/views/partials');
|
|
hbs.registerPartials(__dirname + '/views/subscription/partials/');
|
|
|
|
/**
|
|
* We need this helper to make sure that we consume flash messages only
|
|
* when we are able to actually display these. Otherwise we might end up
|
|
* in a situation where we consume a flash messages but then comes a redirect
|
|
* and the message is never displayed
|
|
*/
|
|
hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer-arrow-callback
|
|
if (typeof this.flash !== 'function') { // eslint-disable-line no-invalid-this
|
|
return '';
|
|
}
|
|
|
|
const messages = this.flash(); // eslint-disable-line no-invalid-this
|
|
const response = [];
|
|
|
|
// group messages by type
|
|
for (const key in messages) {
|
|
let el = '<div class="alert alert-' + key + ' alert-dismissible" role="alert"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>';
|
|
|
|
if (key === 'danger') {
|
|
el += '<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ';
|
|
}
|
|
|
|
let rows = [];
|
|
|
|
for (const message of messages[key]) {
|
|
rows.push(hbs.handlebars.escapeExpression(message).replace(/(\r\n|\n|\r)/gm, '<br>'));
|
|
}
|
|
|
|
if (rows.length > 1) {
|
|
el += '<p>' + rows.join('</p>\n<p>') + '</p>';
|
|
} else {
|
|
el += rows.join('');
|
|
}
|
|
|
|
el += '</div>';
|
|
|
|
response.push(el);
|
|
}
|
|
|
|
return new hbs.handlebars.SafeString(
|
|
response.join('\n')
|
|
);
|
|
});
|
|
|
|
|
|
|
|
function createApp(appType) {
|
|
const app = express();
|
|
|
|
function install404Fallback(url) {
|
|
app.use(url, (req, res, next) => {
|
|
next(new interoperableErrors.NotFoundError());
|
|
});
|
|
|
|
app.use(url + '/*', (req, res, next) => {
|
|
next(new interoperableErrors.NotFoundError());
|
|
});
|
|
}
|
|
|
|
function useWith404Fallback(url, route) {
|
|
app.use(url, route);
|
|
install404Fallback(url);
|
|
}
|
|
|
|
// view engine setup
|
|
app.set('views', path.join(__dirname, 'views'));
|
|
app.set('view engine', 'hbs');
|
|
|
|
// Handle proxies. Needed to resolve client IP
|
|
if (config.www.proxy) {
|
|
app.set('trust proxy', config.www.proxy);
|
|
}
|
|
|
|
// Do not expose software used
|
|
app.disable('x-powered-by');
|
|
|
|
app.use(compression());
|
|
app.use(favicon(path.join(__dirname, '..', 'client', 'static', 'favicon.ico')));
|
|
|
|
app.use(logger(config.www.log, {
|
|
stream: {
|
|
write: message => {
|
|
message = (message || '').toString();
|
|
if (message) {
|
|
log.info('HTTP', message.replace('\n', '').trim());
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
app.use(cookieParser());
|
|
app.use(session({
|
|
store: config.redis.enabled ? new RedisStore(config.redis) : false,
|
|
secret: config.www.secret,
|
|
saveUninitialized: false,
|
|
resave: false
|
|
}));
|
|
|
|
app.use(expressLocale({
|
|
priority: ['query', 'accept-language', 'default'],
|
|
query: {
|
|
name: 'language'
|
|
},
|
|
default: config.defaultLanguage
|
|
}));
|
|
|
|
app.use(flash());
|
|
|
|
app.use(bodyParser.urlencoded({
|
|
extended: true,
|
|
limit: config.www.postSize
|
|
}));
|
|
|
|
app.use(bodyParser.text({
|
|
limit: config.www.postSize
|
|
}));
|
|
|
|
app.use(bodyParser.json({
|
|
limit: config.www.postSize
|
|
}));
|
|
|
|
|
|
app.use((req, res, next) => {
|
|
if (isReady) {
|
|
next();
|
|
} else {
|
|
res.status(500);
|
|
res.render('error', {
|
|
message: 'Mailtrain is starting. Try again after a few seconds.',
|
|
error: {}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (appType === AppType.TRUSTED) {
|
|
passport.setupRegularAuth(app);
|
|
} else if (appType === AppType.SANDBOXED) {
|
|
app.use(passport.tryAuthByRestrictedAccessToken);
|
|
}
|
|
|
|
useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static')));
|
|
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, '..', 'client', 'dist')));
|
|
useWith404Fallback('/locales', express.static(path.join(__dirname, '..', 'client', 'locales')));
|
|
|
|
|
|
// Make sure flash messages are available
|
|
// Currently, flash messages are used only from routes/subscription.js
|
|
app.use((req, res, next) => {
|
|
res.locals.flash = req.flash.bind(req);
|
|
next();
|
|
});
|
|
|
|
// Endpoint under /api are authenticated by access token
|
|
app.all('/api/*', passport.authByAccessToken);
|
|
|
|
// Marks the following endpoint to return JSON object when error occurs
|
|
app.all('/api/*', (req, res, next) => {
|
|
req.needsAPIJSONResponse = true;
|
|
next();
|
|
});
|
|
|
|
app.all('/rest/*', (req, res, next) => {
|
|
req.needsRESTJSONResponse = true;
|
|
next();
|
|
});
|
|
|
|
// Initializes the request context to be used for authorization
|
|
app.use((req, res, next) => {
|
|
req.context = contextHelpers.getRequestContext(req);
|
|
next();
|
|
});
|
|
|
|
if (appType === AppType.PUBLIC) {
|
|
useWith404Fallback('/subscription', subscription);
|
|
useWith404Fallback('/links', links);
|
|
useWith404Fallback('/archive', archive);
|
|
useWith404Fallback('/files', files);
|
|
}
|
|
|
|
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
|
|
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
|
|
useWith404Fallback('/grapesjs', sandboxedGrapesJS.getRouter(appType));
|
|
useWith404Fallback('/codeeditor', sandboxedCodeEditor.getRouter(appType));
|
|
|
|
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
|
|
if (config.reports && config.reports.enabled === true) {
|
|
useWith404Fallback('/reports', reports);
|
|
}
|
|
|
|
useWith404Fallback('/subscriptions', subscriptions);
|
|
useWith404Fallback('/webhooks', webhooks);
|
|
|
|
// API endpoints
|
|
useWith404Fallback('/api', api);
|
|
|
|
// REST endpoints
|
|
app.use('/rest', namespacesRest);
|
|
app.use('/rest', sendConfigurationsRest);
|
|
app.use('/rest', usersRest);
|
|
app.use('/rest', accountRest);
|
|
app.use('/rest', campaignsRest);
|
|
app.use('/rest', triggersRest);
|
|
app.use('/rest', listsRest);
|
|
app.use('/rest', formsRest);
|
|
app.use('/rest', fieldsRest);
|
|
app.use('/rest', importsRest);
|
|
app.use('/rest', importRunsRest);
|
|
app.use('/rest', sharesRest);
|
|
app.use('/rest', segmentsRest);
|
|
app.use('/rest', subscriptionsRest);
|
|
app.use('/rest', templatesRest);
|
|
app.use('/rest', mosaicoTemplatesRest);
|
|
app.use('/rest', blacklistRest);
|
|
app.use('/rest', editorsRest);
|
|
app.use('/rest', filesRest);
|
|
app.use('/rest', settingsRest);
|
|
|
|
if (config.reports && config.reports.enabled === true) {
|
|
app.use('/rest', reportTemplatesRest);
|
|
app.use('/rest', reportsRest);
|
|
}
|
|
install404Fallback('/rest');
|
|
}
|
|
|
|
app.use('/', index.getRouter(appType));
|
|
|
|
// Error handlers
|
|
if (app.get('env') === 'development' || app.get('env') === 'test') {
|
|
// development error handler
|
|
// will print stacktrace
|
|
app.use((err, req, res, next) => {
|
|
if (!err) {
|
|
return next();
|
|
}
|
|
|
|
if (req.needsRESTJSONResponse) {
|
|
const resp = {
|
|
message: err.message,
|
|
error: err
|
|
};
|
|
|
|
if (err instanceof interoperableErrors.InteroperableError) {
|
|
resp.type = err.type;
|
|
resp.data = err.data;
|
|
}
|
|
|
|
res.status(err.status || 500).json(resp);
|
|
|
|
} else if (req.needsAPIJSONResponse) {
|
|
const resp = {
|
|
error: err.message || err,
|
|
data: []
|
|
};
|
|
|
|
return status(err.status || 500).json(resp);
|
|
|
|
} else {
|
|
if (err instanceof interoperableErrors.NotLoggedInError) {
|
|
return res.redirect(getTrustedUrl('/account/login?next=' + encodeURIComponent(req.originalUrl)));
|
|
} else {
|
|
res.status(err.status || 500);
|
|
res.render('error', {
|
|
message: err.message,
|
|
error: err
|
|
});
|
|
}
|
|
}
|
|
|
|
});
|
|
} else {
|
|
// production error handler
|
|
// no stacktraces leaked to user
|
|
app.use((err, req, res, next) => {
|
|
if (!err) {
|
|
return next();
|
|
}
|
|
|
|
if (req.needsRESTJSONResponse) {
|
|
const resp = {
|
|
message: err.message,
|
|
error: {}
|
|
};
|
|
|
|
if (err instanceof interoperableErrors.InteroperableError) {
|
|
resp.type = err.type;
|
|
resp.data = err.data;
|
|
}
|
|
|
|
res.status(err.status || 500).json(resp);
|
|
|
|
} else if (req.needsAPIJSONResponse) {
|
|
const resp = {
|
|
error: err.message || err,
|
|
data: []
|
|
};
|
|
|
|
return res.status(err.status || 500).json(resp);
|
|
|
|
} else {
|
|
// TODO: Render interoperable errors using a special client that does internationalization of the error message
|
|
|
|
if (err instanceof interoperableErrors.NotLoggedInError) {
|
|
return res.redirect(getTrustedUrl('/account/login?next=' + encodeURIComponent(req.originalUrl)));
|
|
} else {
|
|
res.status(err.status || 500);
|
|
res.render('error', {
|
|
message: err.message,
|
|
error: {}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return app;
|
|
}
|
|
|
|
module.exports.createApp = createApp;
|
|
module.exports.setReady = setReady;
|