A web application allowing people to create an account, configure a profile, and share a list of URLs on that profile.
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.
 
 
 
 

361 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.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) => {
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 || error.code || 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.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.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');
};