You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
498 lines
16 KiB
498 lines
16 KiB
// site-platform.js
|
|
// Copyright (C) 2022 DTP Technologies, LLC
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const glob = require('glob');
|
|
|
|
const express = require('express');
|
|
const cookieParser = require('cookie-parser');
|
|
const session = require('express-session');
|
|
const RedisSessionStore = require('connect-redis')(session);
|
|
const passport = require('passport');
|
|
|
|
const mongoose = require('mongoose');
|
|
const Redis = require('ioredis');
|
|
const ioEmitter = require('socket.io-emitter');
|
|
|
|
const morgan = require('morgan');
|
|
const rfs = require('rotating-file-stream');
|
|
const compress = require('compression');
|
|
const methodOverride = require('method-override');
|
|
const marked = require('marked');
|
|
|
|
const { SiteAsync } = require(path.join(__dirname, 'site-async'));
|
|
const { SiteLog } = require(path.join(__dirname, 'site-log'));
|
|
|
|
module.connectDatabase = async (/*dtp*/) => {
|
|
try {
|
|
module.log.info('connecting to MongoDB database', {
|
|
pid: process.pid,
|
|
host: process.env.MONGODB_HOST,
|
|
database: process.env.MONGODB_DATABASE,
|
|
});
|
|
const mongoConnectionInfo = {
|
|
host: process.env.MONGODB_HOST,
|
|
db: process.env.MONGODB_DATABASE,
|
|
username: encodeURIComponent(process.env.MONGODB_USERNAME),
|
|
password: encodeURIComponent(process.env.MONGODB_PASSWORD),
|
|
options: process.env.MONGODB_OPTIONS || '',
|
|
};
|
|
let mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`;
|
|
if (process.env.NODE_ENV === 'production'){
|
|
mongoConnectUri = `mongodb://${mongoConnectionInfo.username}:${mongoConnectionInfo.password}@${mongoConnectionInfo.host}/${mongoConnectionInfo.options}`;
|
|
}
|
|
module.db = await mongoose.connect(mongoConnectUri, {
|
|
socketTimeoutMS: 0,
|
|
keepAlive: true,
|
|
keepAliveInitialDelay: 300000,
|
|
dbName: mongoConnectionInfo.db,
|
|
});
|
|
module.log.info('connected to MongoDB');
|
|
} catch (error) {
|
|
module.log.error('failed to connect to database', { error });
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
module.loadModels = async (dtp) => {
|
|
dtp.models = module.models = [ ];
|
|
const modelScripts = glob.sync(path.join(dtp.config.root, 'app', 'models', '*.js'));
|
|
modelScripts.forEach((modelScript) => {
|
|
const instance = require(modelScript);
|
|
const model = instance(module.db);
|
|
if (module.models[model.modelName]) {
|
|
module.log.error('model name collision', { name: model.modelName });
|
|
process.exit(-1);
|
|
}
|
|
module.models.push(model);
|
|
});
|
|
};
|
|
|
|
module.exports.resetIndexes = async (dtp) => {
|
|
await SiteAsync.each(dtp.models, module.resetIndex);
|
|
};
|
|
|
|
module.resetIndex = async (model) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
module.log.info('dropping model indexes', { model: model.modelName });
|
|
model.collection.dropIndexes((err) => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
module.log.info('creating model indexes', { model: model.modelName });
|
|
model.ensureIndexes((err) => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
return resolve(model);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
module.connectRedis = async (dtp) => {
|
|
try {
|
|
const options = {
|
|
host: process.env.REDIS_HOST,
|
|
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
|
password: process.env.REDIS_PASSWORD,
|
|
keyPrefix: process.env.REDIS_KEY_PREFIX,
|
|
lazyConnect: false,
|
|
};
|
|
module.log.info('connecting to Redis', {
|
|
host: options.host,
|
|
port: options.port,
|
|
prefix: options.keyPrefix,
|
|
});
|
|
|
|
module.redis = dtp.redis = new Redis(options);
|
|
module.redis.setMaxListeners(64); // prevents warnings/errors with Bull Queue
|
|
|
|
module.ioEmitter = dtp.ioEmitter = ioEmitter(module.redis);
|
|
|
|
module.log.info('Redis connected');
|
|
} catch (error) {
|
|
module.log.error('failed to connect to Redis', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
module.getRedisKeys = (pattern) => {
|
|
return new Promise((resolve, reject) => {
|
|
return module.redis.keys(pattern, (err, response) => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
return resolve(response);
|
|
});
|
|
});
|
|
};
|
|
|
|
module.loadServices = async (dtp) => {
|
|
dtp.services = module.services = { };
|
|
const scripts = glob.sync(path.join(dtp.config.root, 'app', 'services', '*.js'));
|
|
const inits = [ ];
|
|
await SiteAsync.each(scripts, async (script) => {
|
|
const service = await require(script);
|
|
module.services[service.name] = service.create(dtp);
|
|
module.services[service.name].__dtp_service_name = service.name;
|
|
inits.push(module.services[service.name]);
|
|
});
|
|
await SiteAsync.each(inits, async (service) => {
|
|
await service.start();
|
|
});
|
|
};
|
|
|
|
module.loadControllers = async (dtp) => {
|
|
const scripts = glob.sync(path.join(dtp.config.root, 'app', 'controllers', '*.js'));
|
|
const inits = [ ];
|
|
|
|
dtp.controllers = { };
|
|
|
|
await SiteAsync.each(scripts, async (script) => {
|
|
const controller = await require(script);
|
|
controller.instance = await controller.create(dtp);
|
|
|
|
dtp.controllers[controller.name] = controller;
|
|
|
|
inits.push(controller);
|
|
});
|
|
|
|
await SiteAsync.each(inits, async (controller) => {
|
|
if (controller.isHome) {
|
|
return; // must run last
|
|
}
|
|
await controller.instance.start();
|
|
});
|
|
|
|
/*
|
|
* Start the Home controller
|
|
*/
|
|
await dtp.controllers.home.instance.start();
|
|
|
|
/*
|
|
* Default error handler
|
|
*/
|
|
module.log.info('registering ExpressJS error handler');
|
|
dtp.app.use((error, req, res, next) => { // jshint ignore:line
|
|
res.locals.errorCode = error.statusCode || error.status || 500;
|
|
module.log.error('ExpressJS error', { url: req.url, error });
|
|
res.status(res.locals.errorCode).render('error', {
|
|
message: error.message,
|
|
error,
|
|
errorCode: res.locals.errorCode,
|
|
});
|
|
// return next(error);
|
|
});
|
|
};
|
|
|
|
module.exports.startPlatform = async (dtp) => {
|
|
try {
|
|
|
|
module.log = new SiteLog(module, dtp.config.component);
|
|
|
|
if (process.env.NODE_ENV === 'local') {
|
|
module.log.alert('allowing self-signed certificates for host-to-host communications', { env: process.env.NODE_ENV });
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
}
|
|
|
|
dtp.config.jobQueues = require(path.join(dtp.config.root, 'config', 'job-queues'));
|
|
|
|
await module.connectDatabase(dtp);
|
|
await module.connectRedis(dtp);
|
|
|
|
await module.loadModels(dtp);
|
|
SiteLog.setModel(module.db.model('Log'));
|
|
|
|
await module.loadServices(dtp);
|
|
|
|
module.log.info(`Digital Telepresence Platform v${dtp.pkg.version} started for [${dtp.pkg.name}]`);
|
|
} catch (error) {
|
|
module.log.error('platform failed to start', { error });
|
|
return;
|
|
}
|
|
};
|
|
|
|
module.exports.startWebServer = async (dtp) => {
|
|
const IS_PRODUCTION = (process.env.NODE_ENV === 'production');
|
|
dtp.app = module.app = express();
|
|
|
|
module.app.set('views', path.join(dtp.config.root, 'app', 'views'));
|
|
module.app.set('view engine', 'pug');
|
|
module.app.set('x-powered-by', false);
|
|
|
|
/*
|
|
* Expose useful modules and information
|
|
*/
|
|
module.app.locals.DTP_SCRIPT_DEBUG = (process.env.NODE_ENV === 'local');
|
|
module.app.locals.env = process.env;
|
|
module.app.locals.dtp = dtp;
|
|
module.app.locals.pkg = require(path.join(dtp.config.root, 'package.json'));
|
|
module.app.locals.mongoose = require('mongoose');
|
|
module.app.locals.moment = require('moment');
|
|
module.app.locals.numeral = require('numeral');
|
|
module.app.locals.phoneNumberJS = require('libphonenumber-js');
|
|
module.app.locals.anchorme = require('anchorme').default;
|
|
module.app.locals.hljs = require('highlight.js');
|
|
|
|
/*
|
|
* Set up the protected markdown renderer that will refuse to process links and images
|
|
* for security reasons.
|
|
*/
|
|
var markedRenderer = new marked.Renderer();
|
|
markedRenderer.link = (href, title, text) => { return text; };
|
|
markedRenderer.image = (href, title, text) => { return text; };
|
|
marked.setOptions({ renderer: markedRenderer });
|
|
module.app.locals.marked = marked;
|
|
|
|
/*
|
|
* HTTP request logging
|
|
*/
|
|
if (process.env.DTP_LOG_FILE === 'enabled') {
|
|
const httpLogStream = rfs.createStream(process.env.DTP_LOG_FILE_NAME_HTTP || 'dtp-sites-access.log', {
|
|
interval: '1d',
|
|
path: process.env.DTP_LOG_FILE_PATH || '/tmp',
|
|
compress: 'gzip',
|
|
});
|
|
module.app.use(morgan(process.env.DTP_LOG_HTTP_FORMAT || 'combined', { stream: httpLogStream }));
|
|
}
|
|
|
|
function cacheOneDay (req, res, next) {
|
|
res.set('Cache-Control', 's-maxage=86400');
|
|
return next();
|
|
}
|
|
|
|
function serviceWorkerAllowed (req, res, next) {
|
|
res.set('Service-Worker-Allowed', '/');
|
|
return next();
|
|
}
|
|
|
|
/*
|
|
* Static file services (project)
|
|
*/
|
|
module.app.use(express.static(path.join(dtp.config.root, 'client')));
|
|
module.app.use('/dist', cacheOneDay, serviceWorkerAllowed, express.static(path.join(dtp.config.root, 'dist')));
|
|
module.app.use('/img', cacheOneDay, express.static(path.join(dtp.config.root, 'client', 'img')));
|
|
|
|
/*
|
|
* Static file services (vendor)
|
|
*/
|
|
|
|
module.app.use('/uikit/images', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'uikit', 'src', 'images')));
|
|
module.app.use('/uikit', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'uikit', 'dist')));
|
|
module.app.use('/chart.js', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'chart.js', 'dist')));
|
|
module.app.use('/chartjs-adapter-moment', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'chartjs-adapter-moment', 'dist')));
|
|
module.app.use('/pretty-checkbox', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'pretty-checkbox', 'dist')));
|
|
|
|
module.app.use('/fontawesome', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', '@fortawesome', 'fontawesome-free')));
|
|
module.app.use('/moment', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'moment', 'min')));
|
|
module.app.use('/numeral', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'numeral', 'min')));
|
|
|
|
module.app.use('/cropperjs', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'cropperjs', 'dist')));
|
|
module.app.use('/tinymce', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'tinymce')));
|
|
module.app.use('/highlight.js', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'highlight.js')));
|
|
|
|
module.app.use('/mpegts', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'mpegts.js', 'dist')));
|
|
|
|
/*
|
|
* ExpressJS middleware
|
|
*/
|
|
module.app.use(express.json({ }));
|
|
module.app.use(express.urlencoded({ extended: true }));
|
|
module.app.use(cookieParser());
|
|
module.app.use(compress());
|
|
module.app.use(methodOverride());
|
|
|
|
/*
|
|
* Express sessions
|
|
*/
|
|
|
|
module.log.info('initializing redis session store');
|
|
var sessionStore = new RedisSessionStore({ client: module.redis });
|
|
|
|
const SESSION_DURATION = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
module.sessionConfig = {
|
|
name: `dtp:${process.env.DTP_SITE_DOMAIN_KEY}.${process.env.NODE_ENV}`,
|
|
secret: process.env.HTTP_SESSION_SECRET,
|
|
resave: true,
|
|
proxy: IS_PRODUCTION || (process.env.HTTP_SESSION_TRUST_PROXY === 'enabled'),
|
|
saveUninitialized: true,
|
|
cookie: {
|
|
domain: process.env.DTP_SITE_DOMAIN_KEY,
|
|
path: '/',
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: process.env.HTTP_COOKIE_SAMESITE || false,
|
|
expires: SESSION_DURATION,
|
|
},
|
|
store: sessionStore,
|
|
};
|
|
module.log.info('configuring session handler', {
|
|
domain: module.sessionConfig.cookie.domain,
|
|
httpOnly: module.sessionConfig.cookie.httpOnly,
|
|
secure: module.sessionConfig.cookie.secure,
|
|
sameSite: module.sessionConfig.cookie.sameSite,
|
|
expires: module.sessionConfig.cookie.expires,
|
|
});
|
|
if (module.sessionConfig.proxy) {
|
|
module.log.info('session will be trusting first proxy');
|
|
module.app.set('trust proxy', true);
|
|
module.sessionConfig.cookie.secure = true;
|
|
}
|
|
module.app.use(session(module.sessionConfig));
|
|
|
|
/*
|
|
* PassportJS setup
|
|
*/
|
|
module.log.info('initializting PassportJS');
|
|
module.app.use(passport.initialize());
|
|
module.app.use(passport.session());
|
|
|
|
module.services.oauth2.registerPassport();
|
|
module.app.use(module.services.session.middleware());
|
|
module.app.use(module.services.userNotification.middleware({ withNotifications: false }));
|
|
|
|
/*
|
|
* Application logic middleware
|
|
*/
|
|
module.app.use(async (req, res, next) => {
|
|
const { cache: cacheService } = dtp.services;
|
|
try {
|
|
res.locals.request = req;
|
|
|
|
const settingsKey = `settings:${dtp.config.site.domainKey}:site`;
|
|
res.locals.site = Object.assign({ }, dtp.config.site);
|
|
|
|
const settings = await cacheService.getObject(settingsKey);
|
|
if (settings) {
|
|
res.locals.site = Object.assign(res.locals.site, settings);
|
|
}
|
|
return next();
|
|
} catch (error) {
|
|
module.log.error('failed to populate general request data', { error });
|
|
return next(error);
|
|
}
|
|
});
|
|
|
|
/*
|
|
* Call out to application to register their custom middleware at the right
|
|
* point in the processing chain.
|
|
*/
|
|
module.log.debug('typeof dtp.config.registerMiddleware', { type: (typeof dtp.config.registerMiddleware) });
|
|
if (dtp.config && (typeof dtp.config.registerMiddleware === 'function')) {
|
|
module.log.info('registering custom application middleware');
|
|
await dtp.config.registerMiddleware(dtp, module.app);
|
|
}
|
|
|
|
/*
|
|
* System Init
|
|
*/
|
|
try {
|
|
dtp.services.oauth2.attachRoutes(module.app);
|
|
await module.loadControllers(dtp);
|
|
} catch (error) {
|
|
module.log.error('failed to initialize application controller', { error });
|
|
return;
|
|
}
|
|
|
|
module.app.use(async (err, req, res, next) => { // jshint ignore:line
|
|
var errorCode = err.status || err.statusCode || err.code || 500;
|
|
module.log.error('HTTP error', { error: err });
|
|
res.status(errorCode).render('error', {
|
|
message: err.message,
|
|
error: err,
|
|
title: 'error'
|
|
});
|
|
});
|
|
|
|
if (process.env.HTTP_ENABLE === 'enabled') {
|
|
if (process.env.HTTP_REDIRECT_SSL === 'enabled') {
|
|
await module.createSslRedirectApp(dtp);
|
|
await module.createHttpServer(dtp, module.redirectApp);
|
|
} else {
|
|
await module.createHttpServer(dtp, module.app);
|
|
}
|
|
}
|
|
if (process.env.HTTPS_ENABLE === 'enabled') {
|
|
await module.createHttpsServer(dtp, module.app);
|
|
}
|
|
|
|
// prefer to attach Socket.io to the HTTPS server and fall back to HTTP
|
|
await module.createSocketServer(dtp, module.https || module.http);
|
|
|
|
if (module.http) {
|
|
await module.startHttpServer(dtp, module.http, dtp.config.http);
|
|
}
|
|
if (module.https) {
|
|
await module.startHttpServer(dtp, module.https, dtp.config.https);
|
|
}
|
|
|
|
module.log.info(`${dtp.config.component.name} platform online`, {
|
|
http: dtp.config.http.port,
|
|
https: dtp.config.https.port,
|
|
});
|
|
};
|
|
|
|
module.createHttpServer = async (dtp, app) => {
|
|
module.log.info('creating HTTP server');
|
|
module.http = require('http').createServer(app);
|
|
};
|
|
|
|
module.createHttpsServer = async (dtp, app) => {
|
|
const httpsOptions = {
|
|
cert: await fs.promises.readFile(
|
|
process.env.HTTPS_SSL_CRT || path.join(dtp.config.root, 'ssl', 'dtp-webapp.crt')
|
|
),
|
|
key: await fs.promises.readFile(
|
|
process.env.HTTPS_SSL_KEY || path.join(dtp.config.root, 'ssl', 'dtp-webapp.key')
|
|
),
|
|
};
|
|
|
|
module.log.info('creating HTTPS server');
|
|
module.https = require('https').createServer(httpsOptions, app);
|
|
|
|
return module.https;
|
|
};
|
|
|
|
module.createSslRedirectApp = async (/* dtp */) => {
|
|
module.log.info('creating HTTP SSL redirect app');
|
|
|
|
module.redirectApp = express();
|
|
|
|
module.redirectApp.use((req, res) => {
|
|
module.log.info('redirecting to SSL', { host: req.host, url: req.url });
|
|
res.redirect(`https://${process.env.DTP_SITE_DOMAIN}${req.url}`);
|
|
});
|
|
|
|
return module.redirectApp;
|
|
};
|
|
|
|
module.startHttpServer = async (dtp, server, config) => {
|
|
return new Promise((resolve, reject) => {
|
|
module.log.info('starting HTTP server', { port: config.port, bind: config.address });
|
|
server.listen(config.port, config.address, (err) => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
return resolve();
|
|
});
|
|
});
|
|
};
|
|
|
|
module.createSocketServer = async (dtp, http) => {
|
|
module.log.info('creating Socket.io server');
|
|
const { SiteIoServer } = require(path.join(__dirname, 'site-ioserver'));
|
|
|
|
module.io = new SiteIoServer(dtp);
|
|
await module.io.start(http);
|
|
|
|
return module.io;
|
|
};
|
|
|
|
module.exports.shutdown = async ( ) => {
|
|
module.log.info('platform shutting down');
|
|
};
|
|
|