'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 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 quickReports = require('./routes/quick-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 = ''; response.push(el); } return new hbs.handlebars.SafeString( response.join('\n') ); }); async 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()); if (config.redis.enabled) { const RedisStore = require('connect-redis')(session); app.use(session({ store: new RedisStore(config.redis), secret: config.www.secret, saveUninitialized: false, resave: false })); } else { app.use(session({ store: false, secret: config.www.secret, saveUninitialized: false, resave: false })); } app.use(expressLocale({ priority: ['query', 'cookie', 'accept-language', 'default'], query: { name: 'locale' }, cookie: { name: 'i18nextLng' }, 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); } if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) { // Endpoint under /api are authenticated by access token app.all('/api/*', passport.authByAccessToken); } useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static'))); useWith404Fallback('/client', express.static(path.join(__dirname, '..', 'client', 'dist'))); useWith404Fallback('/static-npm/fontawesome', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@fortawesome', 'fontawesome-free', 'webfonts'))); useWith404Fallback('/static-npm/jquery.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'jquery', 'dist', 'jquery.min.js'))); useWith404Fallback('/static-npm/popper.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'popper.js', 'dist', 'umd', 'popper.min.js'))); useWith404Fallback('/static-npm/bootstrap.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.min.js'))); useWith404Fallback('/static-npm/coreui.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@coreui', 'coreui', 'dist', 'js', 'coreui.min.js'))); // 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(); }); // 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', await sandboxedMosaico.getRouter(appType)); useWith404Fallback('/ckeditor', await sandboxedCKEditor.getRouter(appType)); useWith404Fallback('/grapesjs', await sandboxedGrapesJS.getRouter(appType)); useWith404Fallback('/codeeditor', await sandboxedCodeEditor.getRouter(appType)); if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) { useWith404Fallback('/subscriptions', subscriptions); useWith404Fallback('/webhooks', webhooks); if (config.reports && config.reports.enabled === true) { useWith404Fallback('/rpts', reports); // This needs to be different from "reports", which is already used by the UI } useWith404Fallback('/quick-rpts', quickReports); // 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('/', await index.getRouter(appType)); app.use((err, req, res, next) => { if (!err) { return next(); } if (req.needsRESTJSONResponse) { const resp = { message: err.message, error: config.sendStacktracesToClient ? err : {} }; if (err instanceof interoperableErrors.InteroperableError) { resp.type = err.type; resp.data = err.data; } log.verbose('HTTP', err); res.status(err.status || 500).json(resp); } else if (req.needsAPIJSONResponse) { const resp = { error: err.message || err, data: [] }; log.verbose('HTTP', err); 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('/login?next=' + encodeURIComponent(req.originalUrl))); } else { log.verbose('HTTP', err); res.status(err.status || 500); res.render('error', { message: err.message, error: config.sendStacktracesToClient ? err : {} }); } } }); return app; } module.exports.createApp = createApp; module.exports.setReady = setReady;