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.
347 lines
11 KiB
347 lines
11 KiB
// site-platform.js
|
|
// Copyright (C) 2021 Digital Telepresence, LLC.
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
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 mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`;
|
|
module.db = await mongoose.connect(mongoConnectUri, {
|
|
socketTimeoutMS: 0,
|
|
keepAlive: true,
|
|
keepAliveInitialDelay: 300000,
|
|
dbName: process.env.MONGODB_DATABASE,
|
|
});
|
|
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 model = require(modelScript);
|
|
if (module.models[model.modelName]) {
|
|
module.log.error('model name collision', { name: model.modelName });
|
|
process.exit(-1);
|
|
}
|
|
module.log.info('data model loaded', {
|
|
name: model.modelName,
|
|
collection: model.collection.collectionName,
|
|
});
|
|
module.models[model.modelName] = 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: true,
|
|
};
|
|
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
|
|
|
|
await module.redis.connect();
|
|
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) => {
|
|
module.log.info('service', { 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]);
|
|
module.log.info('service loaded', { name: 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 = [ ];
|
|
await SiteAsync.each(scripts, async (script) => {
|
|
module.log.info('controller', { script });
|
|
const controller = await require(script)(dtp);
|
|
inits.push(controller);
|
|
});
|
|
await SiteAsync.each(inits, async (controller) => {
|
|
await controller.start();
|
|
});
|
|
};
|
|
|
|
module.exports.startPlatform = async (dtp) => {
|
|
try {
|
|
|
|
module.log = new SiteLog(module, dtp.config.componentName);
|
|
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(mongoose.model('Log'));
|
|
|
|
await module.loadServices(dtp);
|
|
} catch (error) {
|
|
module.log.error('failed to initialize the Digital Telepresence Platform', { error });
|
|
return;
|
|
}
|
|
};
|
|
|
|
module.exports.startWebServer = async (dtp) => {
|
|
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.pkg = require(path.join(dtp.config.root, 'package.json'));
|
|
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;
|
|
|
|
/*
|
|
* 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();
|
|
}
|
|
|
|
/*
|
|
* Static file services (project)
|
|
*/
|
|
module.app.use(express.static(path.join(dtp.config.root, 'client')));
|
|
module.app.use('/dist', cacheOneDay, 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', 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('/cropperjs', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'cropperjs', '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('/mpegts', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'mpegts.js', 'dist')));
|
|
module.app.use('/numeral', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'numeral', 'min')));
|
|
module.app.use('/tinymce', cacheOneDay, express.static(path.join(dtp.config.root, 'node_modules', 'tinymce')));
|
|
|
|
/*
|
|
* 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: `session.${process.env.DTP_SITE_DOMAIN_KEY}.${process.env.NODE_ENV}`,
|
|
secret: process.env.HTTP_SESSION_SECRET,
|
|
resave: true,
|
|
saveUninitialized: true,
|
|
cookie: {
|
|
domain: process.env.DTP_SITE_DOMAIN,
|
|
path: '/',
|
|
httpOnly: true,
|
|
secure: false,
|
|
sameSite: 'strict',
|
|
expires: SESSION_DURATION,
|
|
},
|
|
store: null,
|
|
};
|
|
module.sessionConfig.store = sessionStore;
|
|
if (process.env.NODE_ENV === 'production') {
|
|
module.app.set('trust proxy', 1);
|
|
// 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.app.use(module.services.session.middleware());
|
|
|
|
/*
|
|
* Application logic middleware
|
|
*/
|
|
module.app.use(async (req, res, next) => {
|
|
const { cache: cacheService } = dtp.services;
|
|
try {
|
|
res.locals.dtp = {
|
|
request: req,
|
|
};
|
|
res.locals.socialIcons = [
|
|
{
|
|
url: 'https://facebook.com',
|
|
label: 'Facebook',
|
|
icon: 'fa-facebook'
|
|
},
|
|
{
|
|
url: 'https://twitter.com',
|
|
label: 'Twitter',
|
|
icon: 'fa-twitter'
|
|
},
|
|
{
|
|
url: 'https://instagram.com',
|
|
label: 'Instagram',
|
|
icon: 'fa-instagram'
|
|
},
|
|
];
|
|
|
|
const settingsKey = `settings:${dtp.config.site.domainKey}:site`;
|
|
res.locals.site = 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);
|
|
}
|
|
});
|
|
|
|
/*
|
|
* System Init
|
|
*/
|
|
try {
|
|
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'
|
|
});
|
|
});
|
|
|
|
module.log.info('creating HTTP server');
|
|
module.http = require('http').createServer(module.app);
|
|
|
|
module.log.info('creating Socket.io server');
|
|
const { SiteIoServer } = require(path.join(__dirname, 'site-ioserver'));
|
|
module.io = new SiteIoServer(dtp);
|
|
await module.io.start(module.http);
|
|
|
|
module.log.info('starting HTTP server', {
|
|
port: dtp.config.http.port,
|
|
bind: dtp.config.http.address,
|
|
});
|
|
module.http.listen(dtp.config.http.port, dtp.config.http.address, ( ) => {
|
|
module.log.info(`${dtp.config.componentName} platform online`, { port: dtp.config.http.port });
|
|
});
|
|
};
|
|
|
|
module.exports.shutdown = async ( ) => {
|
|
module.log.info('platform shutting down');
|
|
};
|