Files
NodeBB/install/web.js

303 lines
7.8 KiB
JavaScript
Raw Normal View History

2017-02-18 01:56:23 -07:00
'use strict';
2015-04-21 14:32:21 -04:00
2020-10-14 23:20:45 -04:00
const winston = require('winston');
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const childProcess = require('child_process');
const less = require('less');
const util = require('util');
2020-10-14 23:20:45 -04:00
const lessRenderAsync = util.promisify(
(style, opts, cb) => less.render(String(style), opts, cb)
);
const uglify = require('uglify-es');
const nconf = require('nconf');
const Benchpress = require('benchpressjs');
const mkdirp = require('mkdirp');
2020-10-14 23:20:45 -04:00
const { paths } = require('../src/constants');
const app = express();
let server;
const formats = [
winston.format.colorize(),
];
const timestampFormat = winston.format((info) => {
2021-02-04 00:06:15 -07:00
const dateString = `${new Date().toISOString()} [${global.process.pid}]`;
2021-02-03 23:59:08 -07:00
info.level = `${dateString} - ${info.level}`;
return info;
});
formats.push(timestampFormat());
formats.push(winston.format.splat());
formats.push(winston.format.simple());
winston.configure({
2017-02-17 19:31:21 -07:00
level: 'verbose',
format: winston.format.combine.apply(null, formats),
transports: [
2018-10-16 15:17:14 -04:00
new winston.transports.Console({
handleExceptions: true,
}),
new winston.transports.File({
filename: 'logs/webinstall.log',
2018-10-16 15:17:14 -04:00
handleExceptions: true,
}),
],
2015-09-02 18:41:05 -04:00
});
2020-10-14 23:20:45 -04:00
const web = module.exports;
2018-05-23 11:39:58 -04:00
2020-10-14 23:20:45 -04:00
const scripts = [
'node_modules/jquery/dist/jquery.js',
'node_modules/xregexp/xregexp-all.js',
'public/src/modules/slugify.js',
2016-08-12 01:14:01 +03:00
'public/src/utils.js',
2017-02-17 19:31:21 -07:00
'public/src/installer/install.js',
2019-03-19 14:01:31 -04:00
'node_modules/zxcvbn/dist/zxcvbn.js',
2016-08-12 01:14:01 +03:00
];
2015-04-21 14:32:21 -04:00
2020-10-14 23:20:45 -04:00
let installing = false;
let success = false;
let error = false;
let launchUrl;
2018-05-23 11:39:58 -04:00
const viewsDir = path.join(paths.baseDir, 'build/public/templates');
2020-10-14 23:20:45 -04:00
web.install = async function (port) {
2015-04-22 15:52:12 -04:00
port = port || 4567;
2021-02-03 23:59:08 -07:00
winston.info(`Launching web installer on port ${port}`);
2015-04-21 14:32:21 -04:00
2015-04-21 19:50:58 -04:00
app.use(express.static('public', {}));
2021-02-04 00:01:39 -07:00
app.engine('tpl', (filepath, options, callback) => {
2020-10-14 23:20:45 -04:00
filepath = filepath.replace(/\.tpl$/, '.js');
Benchpress.__express(filepath, options, callback);
2017-09-13 13:50:00 -06:00
});
2015-04-21 19:50:58 -04:00
app.set('view engine', 'tpl');
2020-10-14 23:20:45 -04:00
app.set('views', viewsDir);
2015-04-21 19:50:58 -04:00
app.use(bodyParser.urlencoded({
2017-02-17 19:31:21 -07:00
extended: true,
2015-09-02 18:17:58 -04:00
}));
2020-10-14 23:20:45 -04:00
try {
await Promise.all([
compileTemplate(),
2020-10-14 23:20:45 -04:00
compileLess(),
compileJS(),
copyCSS(),
loadDefaults(),
]);
2015-04-21 16:21:04 -04:00
setupRoutes();
launchExpress(port);
2020-10-14 23:20:45 -04:00
} catch (err) {
winston.error(err.stack);
}
2015-04-21 14:32:21 -04:00
};
2015-04-21 14:52:57 -04:00
function launchExpress(port) {
2021-02-04 00:01:39 -07:00
server = app.listen(port, () => {
2015-05-24 10:15:16 -04:00
winston.info('Web installer listening on http://%s:%s', '0.0.0.0', port);
2015-04-21 14:52:57 -04:00
});
}
function setupRoutes() {
2015-04-21 19:50:58 -04:00
app.get('/', welcome);
app.post('/', install);
2015-04-22 14:49:31 -04:00
app.post('/launch', launch);
2018-01-24 21:05:08 -05:00
app.get('/ping', ping);
app.get('/sping', ping);
}
function ping(req, res) {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
2015-04-21 14:52:57 -04:00
}
2015-04-21 19:50:58 -04:00
function welcome(req, res) {
2021-02-04 00:06:15 -07:00
const dbs = ['redis', 'mongo', 'postgres'];
const databases = dbs.map((databaseName) => {
const questions = require(`../src/database/${databaseName}`).questions.filter(question => question && !question.hideOnWebInstall);
2017-11-22 12:19:08 -05:00
2016-08-12 01:14:01 +03:00
return {
2017-11-22 12:19:08 -05:00
name: databaseName,
questions: questions,
2016-08-12 01:14:01 +03:00
};
2015-04-21 19:10:47 -04:00
});
2021-02-04 00:06:15 -07:00
const defaults = require('./data/defaults');
2016-08-12 01:14:01 +03:00
2015-04-22 15:29:29 -04:00
res.render('install/index', {
2021-02-03 23:59:08 -07:00
url: nconf.get('url') || (`${req.protocol}://${req.get('host')}`),
2018-05-26 13:12:33 -04:00
launchUrl: launchUrl,
skipGeneralSetup: !!nconf.get('url'),
2015-04-22 15:29:29 -04:00
databases: databases,
skipDatabaseSetup: !!nconf.get('database'),
2018-05-23 12:39:04 -04:00
error: error,
success: success,
2016-08-12 01:14:01 +03:00
values: req.body,
2017-02-17 19:31:21 -07:00
minimumPasswordLength: defaults.minimumPasswordLength,
2019-03-19 14:01:31 -04:00
minimumPasswordStrength: defaults.minimumPasswordStrength,
2018-05-23 11:39:58 -04:00
installing: installing,
2015-04-21 19:10:47 -04:00
});
2015-04-21 14:52:57 -04:00
}
2015-04-21 19:50:58 -04:00
function install(req, res) {
2018-05-23 11:39:58 -04:00
if (installing) {
return welcome(req, res);
}
2018-01-16 14:27:44 -05:00
req.setTimeout(0);
2018-05-23 11:39:58 -04:00
installing = true;
2021-02-04 00:06:15 -07:00
const setupEnvVars = nconf.get();
2021-02-04 01:34:30 -07:00
for (const [key, value] of Object.entries(req.body)) {
if (!process.env.hasOwnProperty(key)) {
setupEnvVars[key.replace(':', '__')] = value;
2015-04-22 11:36:19 -04:00
}
}
2015-04-22 11:22:55 -04:00
2018-03-21 17:00:49 -04:00
// Flatten any objects in setupEnvVars
const pushToRoot = function (parentKey, key) {
2021-02-03 23:59:08 -07:00
setupEnvVars[`${parentKey}__${key}`] = setupEnvVars[parentKey][key];
2018-03-21 17:00:49 -04:00
};
2021-02-04 01:34:30 -07:00
for (const [parentKey, value] of Object.entries(setupEnvVars)) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.keys(value).forEach(key => pushToRoot(parentKey, key));
delete setupEnvVars[parentKey];
} else if (Array.isArray(value)) {
setupEnvVars[parentKey] = JSON.stringify(value);
2018-03-21 17:00:49 -04:00
}
}
2018-03-22 18:45:00 -04:00
winston.info('Starting setup process');
winston.info(setupEnvVars);
2018-05-26 13:12:33 -04:00
launchUrl = setupEnvVars.url;
2018-03-22 18:45:00 -04:00
2021-02-04 00:06:15 -07:00
const child = require('child_process').fork('app', ['--setup'], {
2018-01-15 15:05:33 -05:00
env: setupEnvVars,
2015-04-22 11:22:55 -04:00
});
2015-04-22 11:36:19 -04:00
2021-02-04 00:01:39 -07:00
child.on('close', (data) => {
2018-05-23 11:39:58 -04:00
installing = false;
2018-05-23 12:39:04 -04:00
success = data === 0;
error = data !== 0;
welcome(req, res);
});
2015-04-21 19:50:58 -04:00
}
2020-10-14 23:20:45 -04:00
async function launch(req, res) {
try {
res.json({});
server.close();
req.setTimeout(0);
2021-02-04 00:06:15 -07:00
let child;
2020-10-14 23:20:45 -04:00
if (!nconf.get('launchCmd')) {
child = childProcess.spawn('node', ['loader.js'], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
});
2015-09-02 18:17:58 -04:00
2020-10-14 23:20:45 -04:00
console.log('\nStarting NodeBB');
console.log(' "./nodebb stop" to stop the NodeBB server');
console.log(' "./nodebb log" to view server output');
console.log(' "./nodebb restart" to restart NodeBB');
} else {
// Use launchCmd instead, if specified
child = childProcess.exec(nconf.get('launchCmd'), {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
});
2016-12-22 11:45:22 -05:00
}
2015-09-02 18:17:58 -04:00
2020-10-14 23:20:45 -04:00
const filesToDelete = [
'installer.css',
'installer.min.js',
'bootstrap.min.css',
];
await Promise.all(
filesToDelete.map(
filename => fs.promises.unlink(path.join(__dirname, '../public', filename))
)
);
2016-12-22 11:45:22 -05:00
child.unref();
process.exit(0);
2020-10-14 23:20:45 -04:00
} catch (err) {
winston.error(err.stack);
throw err;
}
2015-04-22 14:49:31 -04:00
}
// this is necessary because otherwise the compiled templates won't be available on a clean install
async function compileTemplate() {
const sourceFile = path.join(__dirname, '../src/views/install/index.tpl');
const destTpl = path.join(viewsDir, 'install/index.tpl');
const destJs = path.join(viewsDir, 'install/index.js');
const source = await fs.promises.readFile(sourceFile, 'utf8');
const [compiled] = await Promise.all([
Benchpress.precompile(source, { filename: 'install/index.tpl' }),
mkdirp(path.dirname(destJs)),
]);
await Promise.all([
fs.promises.writeFile(destJs, compiled),
fs.promises.writeFile(destTpl, source),
]);
}
2020-10-14 23:20:45 -04:00
async function compileLess() {
try {
const installSrc = path.join(__dirname, '../public/less/install.less');
const style = await fs.promises.readFile(installSrc);
const css = await lessRenderAsync(style, { filename: path.resolve(installSrc) });
await fs.promises.writeFile(path.join(__dirname, '../public/installer.css'), css.css);
} catch (err) {
2021-02-03 23:59:08 -07:00
winston.error(`Unable to compile LESS: \n${err.stack}`);
2020-10-14 23:20:45 -04:00
throw err;
}
2015-04-21 16:21:04 -04:00
}
2015-04-21 14:32:21 -04:00
2020-10-14 23:20:45 -04:00
async function compileJS() {
let code = '';
2015-04-21 17:02:36 -04:00
2020-10-14 23:20:45 -04:00
for (const srcPath of scripts) {
// eslint-disable-next-line no-await-in-loop
const buffer = await fs.promises.readFile(path.join(__dirname, '..', srcPath));
code += buffer.toString();
}
const minified = uglify.minify(code, {
compress: false,
2017-08-04 22:23:55 -04:00
});
2020-10-14 23:20:45 -04:00
if (!minified.code) {
throw new Error('[[error:failed-to-minify]]');
}
await fs.promises.writeFile(path.join(__dirname, '../public/installer.min.js'), minified.code);
2015-04-21 17:02:36 -04:00
}
2020-10-14 23:20:45 -04:00
async function copyCSS() {
const src = await fs.promises.readFile(
path.join(__dirname, '../node_modules/bootstrap/dist/css/bootstrap.min.css'), 'utf8'
);
await fs.promises.writeFile(path.join(__dirname, '../public/bootstrap.min.css'), src);
}
2020-10-14 23:20:45 -04:00
async function loadDefaults() {
const setupDefaultsPath = path.join(__dirname, '../setup.json');
try {
2021-02-03 23:51:03 -07:00
await fs.promises.access(setupDefaultsPath, fs.constants.F_OK + fs.constants.R_OK);
2020-10-14 23:20:45 -04:00
} catch (err) {
// setup.json not found or inaccessible, proceed with no defaults
if (err.code !== 'ENOENT') {
throw err;
2018-03-21 17:00:49 -04:00
}
2020-10-14 23:20:45 -04:00
}
winston.info('[installer] Found setup.json, populating default values');
nconf.file({
file: setupDefaultsPath,
2018-03-21 17:00:49 -04:00
});
}