"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 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")); }); 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", 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) { 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 } // 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)); 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;