UNPKG

taboo-cms

Version:
458 lines (425 loc) 14.3 kB
const Koa = require('koa'); const Router = require('koa-better-router'); const koaBody = require('koa-body'); const http = require('http'); const KeyGrip = require('keygrip'); const koaSession = require('koa-session'); const koaPassport = require('koa-passport'); const cors = require('koa-cors'); const _ = require('lodash'); const serve = require('koa-static'); const config = require('./config'); const Logger = require('./utils/Logger'); const CmsHelper = require('./utils/CmsHelper'); const FilesHelper = require('./utils/FilesHelper'); const EjsHelper = require('./utils/EjsHelper'); const ApiHelper = require('./utils/ApiHelper'); const ArrayHelper = require('./utils/ArrayHelper'); const EventsEmitter = require('./utils/EventsEmitter'); const Mailer = require('./utils/Mailer'); const SocketsServer = require('./utils/SocketsServer'); class TabooCms { constructor() { this.app = { running: false, logger: Logger, cwd: process.cwd(), config: config, koaApp: new Koa(), server: null, router: Router().loadMethods(), routes: [], modules: {}, policies: {}, dbConnections: {}, events: EventsEmitter, mailer: Mailer, locales: {}, adminLocales: {}, passport: null, afterModulesSetup: [], // Registered functions from each module config to be called after all modules setup afterModelsSetup: [], // Registered functions from each module config to be called after all models setup aclResources: [], sockets: SocketsServer, //sockets server }; this.start = this.start.bind(this); } async start(customMiddlewareSetup) { // TODO investigate jwt if (this.app.running) { Logger.error('TabooCms is already running'); } else { // The order below is important this.app.running = true; this.setupUtils(); this.setupOnServerError(); this.setupServerSecretKeys(); this.setupStaticFiles(); this.loadLocales(); await this.setupDb(); this.setupMiddleware(customMiddlewareSetup); this.setupSession(); // Setup passport after session this.setupPassport(); this.setupPolicies(); this.setupAppModules(); await this.setupModels(); this.setupServerResponse(); await this.startServer(); this.startSocketsServer(); } return this.app; } setupUtils() { Logger.setup(this.app.config); EventsEmitter.setup(this.app); CmsHelper.setup(this.app); EjsHelper.setup(this.app); FilesHelper.setup(this.app.config); ApiHelper.setup(this.app.config); Mailer.setup(this.app.config); SocketsServer.setup(this.app.config); } setupServerSecretKeys() { this.app.koaApp.keys = new KeyGrip(this.app.config.server.secretKeys, 'sha256'); } setupOnServerError() { const { silentErrors } = this.app.config.server; this.app.koaApp.on('error', (err, ctx) => { if (silentErrors.indexOf(err.name) === -1) { Logger.error('Server error:'); // Keep this whole ctx debug only for production to have more details to collect if (this.app.config.environment === 'production') { Logger.error(ctx); } Logger.error(err); } }); this.app.koaApp.use(async (ctx, next) => { let errorResponse; try { await next(); } catch (err) { ctx.status = err.status || 500; if (ctx.taboo && ctx.taboo.errorResponseAsJson) { errorResponse = { error: err, message: err.message, }; } else { errorResponse = await CmsHelper.getServerErrorResponse(err, ctx); } ctx.body = errorResponse; ctx.app.emit('error', err, ctx); } }); } setupStaticFiles() { const { publicDir, uploads: { serveStaticDir = null }, } = this.app.config.server; this.app.koaApp.use(serve(publicDir)); if (serveStaticDir && publicDir !== serveStaticDir) { this.app.koaApp.use(serve(serveStaticDir)); } } loadLocales() { const { localesDir, adminLocalesDir } = this.app.config.server; CmsHelper.loadLocales(localesDir); CmsHelper.loadLocales(adminLocalesDir, 'adminLocales'); } setupMiddleware(customMiddlewareSetup) { const { cors: { enabled: corsEnabled = false, options: corsOptions }, uploads: { maxFileSize }, } = this.app.config.server; // Setup taboo and view objects on ctx object this.app.koaApp.use(async (ctx, next) => { ctx.view = {}; // template view, layout and view variables ctx.flashMessages = []; // flash messages for views ctx.taboo = {}; // taboo cms related configuration ctx.taboo.clientConfig = {}; // any dynamic overrides for template default config.client CmsHelper.setDefaultLanguageParams(ctx); CmsHelper.setDefaultAdminLanguageParams(ctx); await next(); }); if (corsEnabled) { this.app.koaApp.use(cors(corsOptions)); } // Body parser this.app.koaApp.use( koaBody({ multipart: true, formidable: { maxFileSize: maxFileSize, }, }) ); if (customMiddlewareSetup && _.isFunction(customMiddlewareSetup)) { customMiddlewareSetup(this.app.koaApp, this.app.config); } // Log incoming requests and times only for debug and development envs if (this.app.config.debug) { this.app.koaApp.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; Logger.info(`${ctx.method} ${ctx.url} - ${ms} ms`); }); } } setupAppModules() { CmsHelper.setupAllModules(); const { config, modules, logger, afterModulesSetup } = this.app; let allRouteMethods = []; let allRoutes = []; let moduleName, module, methodAndPath; for (moduleName in modules) { if (modules.hasOwnProperty(moduleName)) { module = modules[moduleName]; const { routes = [] } = module.config; routes.map(route => { if (!route.order) { route.order = 0; } route.modulePath = module.path; methodAndPath = `${route.method}:${route.path}`; if (config.environment !== 'production' && allRouteMethods.indexOf(methodAndPath) !== -1) { logger.error( `Route with the following method '${route.method}' and path '${ route.path }' already exists, please check the following route: \n`, route ); } allRoutes.push(route); allRouteMethods.push(methodAndPath); }); } } allRoutes = ArrayHelper.sortByProperty(allRoutes, 'order'); this.app.routes = allRoutes; this.setupRoutes(allRoutes); this.app.koaApp.use(this.app.router.middleware()); afterModulesSetup.map(fn => { fn(modules); }); } async setupDb() { const adapterMethods = { connect: 'connect() method which is either async or returns a Promise', setupModel: 'setupModel() method which is either async or returns a Promise', }; const adapterProperties = { connection: 'connection', connectedTo: 'connectedTo', }; const { connections } = this.app.config.db; let name, config; for (name in connections) { if (connections.hasOwnProperty(name)) { config = connections[name]; if (config.adapter) { this.app.dbConnections[name] = new config.adapter(config); _.each(adapterMethods, (sample, method) => { if (!this.app.dbConnections[name][method] || !_.isFunction(this.app.dbConnections[name][method])) { throw Error(`Connection '${name}' adapter must implement ${sample}`); } }); await this.app.dbConnections[name].connect(config); _.each(adapterProperties, (sample, property) => { if (!this.app.dbConnections[name][property]) { throw Error(`Connection '${name}' adapter must have '${sample}' property`); } }); Logger.info(`Successfully established '${name}' connection: ${this.app.dbConnections[name].connectedTo}`); } } } } async setupModels() { const { modules, dbConnections, afterModelsSetup } = this.app; let moduleName, module, modelName, modelConfig; for (moduleName in modules) { module = modules[moduleName]; if (!module.models) { module.models = {}; } if (module.modelConfigs && _.size(module.modelConfigs) > 0) { for (modelName in module.modelConfigs) { modelConfig = module.modelConfigs[modelName]; if (dbConnections[modelConfig.connection]) { module.models[modelName] = await dbConnections[modelConfig.connection].setupModel(modelName, modelConfig); } } } } afterModelsSetup.map(fn => { fn(modules); }); } setupSession() { const customStoreMethods = { get: 'get(key)', set: 'set(key, value, maxAge, options)', destroy: 'destroy(key)', }; const { session } = this.app.config.server; // TODO implement custom session.options.encode and session.options.decode methods if (session.store && session.store !== 'cookie') { session.options.store = new session.store(session.options); _.each(customStoreMethods, (sample, method) => { if (!session.options.store[method] || !_.isFunction(session.options.store[method])) { throw Error(`Session must implement ${sample} method`); } }); } this.app.koaApp.use(koaSession(session.options, this.app.koaApp)); } setupPassport() { const { passport } = this.app.config; if (passport.setupStrategiesMethod && _.size(passport.strategies) > 0) { this.app.passport = koaPassport; passport.setupStrategiesMethod(this.app.passport, this.app.config); this.app.koaApp.use(this.app.passport.initialize()); this.app.koaApp.use(this.app.passport.session()); } } setupPolicies() { CmsHelper.setupPolicies(); } setupRoutes(routes) { routes.map(route => { this.app.router[route.method.toLowerCase()](route.path, CmsHelper.getRouterRouteArgs(route)); }); } startServer() { return new Promise(resolve => { this.app.server = http.createServer(this.app.koaApp.callback()).listen(this.app.config.server.port, () => { Logger.info(`Server is listening on port ${this.app.config.server.port}`); this.app.events.emit('server-started', this.app.koaApp); resolve(this.app.koaApp); }); }); } setupServerResponse() { this.app.koaApp.use(async (ctx, next) => { if (ctx.err) { ctx.throw(500, ctx.err); } else if (ctx.route && !ctx.body) { return (ctx.body = await CmsHelper.getServerResponse(ctx)); } else { await next(); } }); } getDbConnection(connectionName) { if (taboo.app.dbConnections[connectionName]) { return taboo.app.dbConnections[connectionName]; } return null; } startSocketsServer() { if (this.app.config.sockets.enabled) { this.app.sockets.start(this.app.server); } } } const taboo = new TabooCms(); module.exports = { start: taboo.start, cwd: taboo.app.cwd, _: _, logger: taboo.app.logger, config: taboo.app.config, events: taboo.app.events, mailer: taboo.app.mailer, locales: taboo.app.locales, adminLocales: taboo.app.adminLocales, sockets: taboo.app.sockets, filesHelper: FilesHelper, getPassport: () => { return taboo.app.passport; }, getDbConnection: taboo.getDbConnection, getPage: (moduleRoute, viewName) => { return CmsHelper.getPage(moduleRoute, viewName); }, getLayout: layoutName => { return CmsHelper.getLayout(layoutName); }, getLayoutPath: layoutName => { return CmsHelper.getLayoutPath(layoutName); }, composeResponse: (ctx, layoutTpl, pageTpl, params) => { return CmsHelper.composeResponse(ctx, layoutTpl, pageTpl, params); }, composeTemplate: (ctx, tpl, params) => { return CmsHelper.composeTemplate(ctx, tpl, params); }, getModel: (module, model) => { return CmsHelper.getModuleModel(module, model); }, Model: moduleModel => { const params = moduleModel.split('.'); if (params.length !== 2) { throw new Error('Please specify module and model: "module.Model"'); } return CmsHelper.getModuleModel(...params); }, getController: (module, controller) => { return CmsHelper.getModuleController(module, controller); }, Controller: moduleController => { const params = moduleController.split('.'); if (params.length !== 2) { throw new Error('Please specify module and controller: "module.Controller"'); } return CmsHelper.getModuleController(...params); }, getService: (module, service) => { return CmsHelper.getModuleService(module, service); }, Service: moduleService => { const params = moduleService.split('.'); if (params.length !== 2) { throw new Error('Please specify module and service: "module.Service"'); } return CmsHelper.getModuleService(...params); }, getHelper: (module, helper) => { return CmsHelper.getModuleHelper(module, helper); }, Helper: moduleHelper => { const params = moduleHelper.split('.'); if (params.length !== 2) { throw new Error('Please specify module and helper: "module.Helper"'); } return CmsHelper.getModuleHelper(...params); }, parseApiRequestParams: (requestParams, paramsList) => { return ApiHelper.parseRequestParams(requestParams, paramsList); }, cleanTimestamps: data => { ApiHelper.cleanTimestamps(data); }, getMissingTranslations: () => { return EjsHelper.getMissingTranslations(); }, getAllRoutes() { return taboo.app.routes; }, getAllModules() { return taboo.app.modules; }, getLocalesArray(admin = false) { return CmsHelper.getLocalesArray(admin); }, getAclResources() { return taboo.app.aclResources; }, isAllowed(subject, resource) { return CmsHelper.isAllowed(subject, resource); }, };