Files
NodeBB/install/web.js
Barış Soner Uşaklı d20b07cfea Webpack5 (#10311)
* feat: webpack 5 part 1

* fix: gruntfile fixes

* fix: fix taskbar warning

add app.importScript
copy public/src/modules to build folder

* refactor: remove commented old code

* feat: reenable admin

* fix: acp settings pages, fix sortable on manage categories

embedded require in html not allowed

* fix: bundle serialize/deserizeli so plugins dont break

* test: fixe util tests

* test: fix require path

* test: more test fixes

* test: require correct utils module

* test: require correct utils

* test: log stack

* test: fix db require blowing up tests

* test: move and disable bundle test

* refactor: add aliases

* test: disable testing route

* fix: move webpack modules necessary for build, into `dependencies`

* test: fix one more test

remove 500-embed.tpl

* fix: restore use of assets/nodebb.min.js, at least for now

* fix: remove unnecessary line break

* fix: point to proper ACP bundle

* test: maybe fix build test

* test: composer

* refactor: dont need dist

* refactor: more cleanup

use everything from build/public folder

* get rid of conditional import in app.js

* fix: ace

* refactor: cropper alias

* test: lint and test fixes

* lint: fix

* refactor: rename function to app.require

* refactor: go back to using app.require

* chore: use github branch

* chore: use webpack branch

* feat: webpack webinstaller

* feat: add chunkFile name with contenthash

* refactor: move hooks to top

* refactor: get rid of template500Function

* fix(deps): use webpack5 branch of 2factor plugin

* chore: tagging v2.0.0-beta.0 pre-release version 💥 :shipit: 🎉 🚀

* refactor: disable cache on templates

loadTemplate is called once by benchpress and the result is cache internally

* refactor: add server side helpers.js

* feat: deprecate /plugins shorthand route, closes #10343

* refactor: use build/public for webpack

* test: fix filename

* fix: more specific selector

* lint: ignore

* refactor: fix comments

* test: add debug for random failing test

* refactor: cleanup

remove test page, remove dupe functions in utils.common

* lint: use relative path  for now

* chore: bump prerelease version

* feat: add translateKeys

* fix: optional params

* fix: get rid of extra timeago files

* refactor: cleanup, require timeago locale earlier

remove translator.prepareDOM, it is in header.tpl html tag

* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378)

* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels

- Existing hooks are preserved (to be deprecated at a later date, possibly)
- New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks

* docs: fix typo in comment

* test: spec changes

* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378)

* refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels

- Existing hooks are preserved (to be deprecated at a later date, possibly)
- New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks

* docs: fix typo in comment

* test: spec changes

* feat: allow app.require('bootbox'/'benchpressjs')

* refactor: require server side utils

* test: jquery ready

* change istaller to use build/public

* test: use document.addEventListener

* refactor: closes #10301

* refactor: generateTopicClass

* fix: column counts for other privileges

* fix: #10443, regression where sorted-list items did not render into the DOM in the predicted order [breaking]

* fix: typo in hook name

* refactor: introduce a generic autocomplete.init() method that can be called to add nodebb-style autocompletion but using different data sources (e.g. not user/groups/tags)

* fix: crash if `delay` not passed in (as it cannot be destructured)

* refactor: replace substr

* feat: set --panel-offset style in html element based on stored value in localStorage

* refactor: addDropupHandler() logic to be less naive

- Take into account height of the menu
- Don't apply dropUp logic if there's nothing in the dropdown
- Remove 'hidden' class (added by default in Persona for post tools) when menu items are added

closes #10423

* refactor: simplify utils.params [breaking]

Retrospective analysis of the usage of this method suggests that the options passed in are superfluous, and that only `url` is required. Using a browser built-in makes more sense to accomplish what this method sets out to do.

* feat: add support for returning full URLSearchParams for utils.params

* fix: utils.params() fallback handling

* fix: default empty obj for params()

* fix: remove \'loggedin\' and \'register\' qs parameters once they have been used, delay invocation of messages until ajaxify.end

* fix: utils.params() not allowing relative paths to be passed in

* refactor(DRY): new assertPasswordValidity utils method

* fix: incorrect error message returned on insufficient privilege on flag edit

* fix: read/update/delete access to flags API should be limited for moderators to only post flags in categories they moderate

- added failing tests and patched up middleware.assert.flags to fix

* refactor: flag api v3 tests to create new post and flags on every round

* fix: missing error:no-flag language key

* refactor: flags.canView to check flag existence, simplify middleware.assert.flag

* feat: flag deletion API endpoint, #10426

* feat: UI for flag deletion, closes #10426

* chore: update plugin versions

* chore: up emoji

* chore: update markdown

* chore: up emoji-android

* fix: regression caused by utils.params() refactor, supports arrays and pipes all values through utils.toType, adjusts tests to type check

Co-authored-by: Julian Lam <julian@nodebb.org>
2022-04-29 21:39:33 -04:00

282 lines
7.7 KiB
JavaScript

'use strict';
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 webpack = require('webpack');
const nconf = require('nconf');
const Benchpress = require('benchpressjs');
const mkdirp = require('mkdirp');
const { paths } = require('../src/constants');
const app = express();
let server;
const formats = [
winston.format.colorize(),
];
const timestampFormat = winston.format((info) => {
const dateString = `${new Date().toISOString()} [${global.process.pid}]`;
info.level = `${dateString} - ${info.level}`;
return info;
});
formats.push(timestampFormat());
formats.push(winston.format.splat());
formats.push(winston.format.simple());
winston.configure({
level: 'verbose',
format: winston.format.combine.apply(null, formats),
transports: [
new winston.transports.Console({
handleExceptions: true,
}),
new winston.transports.File({
filename: 'logs/webinstall.log',
handleExceptions: true,
}),
],
});
const web = module.exports;
let installing = false;
let success = false;
let error = false;
let launchUrl;
const viewsDir = path.join(paths.baseDir, 'build/public/templates');
web.install = async function (port) {
port = port || 4567;
winston.info(`Launching web installer on port ${port}`);
app.use(express.static('public', {}));
app.use('/assets', express.static(path.join(__dirname, '../build/webpack'), {}));
app.engine('tpl', (filepath, options, callback) => {
filepath = filepath.replace(/\.tpl$/, '.js');
Benchpress.__express(filepath, options, callback);
});
app.set('view engine', 'tpl');
app.set('views', viewsDir);
app.use(bodyParser.urlencoded({
extended: true,
}));
try {
await Promise.all([
compileTemplate(),
compileLess(),
runWebpack(),
copyCSS(),
loadDefaults(),
]);
setupRoutes();
launchExpress(port);
} catch (err) {
winston.error(err.stack);
}
};
async function runWebpack() {
const util = require('util');
const webpackCfg = require('../webpack.installer');
const compiler = webpack(webpackCfg);
const webpackRun = util.promisify(compiler.run).bind(compiler);
await webpackRun();
}
function launchExpress(port) {
server = app.listen(port, () => {
winston.info('Web installer listening on http://%s:%s', '0.0.0.0', port);
});
}
function setupRoutes() {
app.get('/', welcome);
app.post('/', install);
app.post('/launch', launch);
app.get('/ping', ping);
app.get('/sping', ping);
}
function ping(req, res) {
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
}
function welcome(req, res) {
const dbs = ['mongo', 'redis', 'postgres'];
const databases = dbs.map((databaseName) => {
const questions = require(`../src/database/${databaseName}`).questions.filter(question => question && !question.hideOnWebInstall);
return {
name: databaseName,
questions: questions,
};
});
const defaults = require('./data/defaults.json');
res.render('install/index', {
url: nconf.get('url') || (`${req.protocol}://${req.get('host')}`),
launchUrl: launchUrl,
skipGeneralSetup: !!nconf.get('url'),
databases: databases,
skipDatabaseSetup: !!nconf.get('database'),
error: error,
success: success,
values: req.body,
minimumPasswordLength: defaults.minimumPasswordLength,
minimumPasswordStrength: defaults.minimumPasswordStrength,
installing: installing,
});
}
function install(req, res) {
if (installing) {
return welcome(req, res);
}
req.setTimeout(0);
installing = true;
const database = nconf.get('database') || req.body.database || 'mongo';
const setupEnvVars = {
...process.env,
NODEBB_URL: nconf.get('url') || req.body.url || (`${req.protocol}://${req.get('host')}`),
NODEBB_PORT: nconf.get('port') || 4567,
NODEBB_ADMIN_USERNAME: nconf.get('admin:username') || req.body['admin:username'],
NODEBB_ADMIN_PASSWORD: nconf.get('admin:password') || req.body['admin:password'],
NODEBB_ADMIN_EMAIL: nconf.get('admin:email') || req.body['admin:email'],
NODEBB_DB: database,
NODEBB_DB_HOST: nconf.get(`${database}:host`) || req.body[`${database}:host`],
NODEBB_DB_PORT: nconf.get(`${database}:port`) || req.body[`${database}:port`],
NODEBB_DB_USER: nconf.get(`${database}:username`) || req.body[`${database}:username`],
NODEBB_DB_PASSWORD: nconf.get(`${database}:password`) || req.body[`${database}:password`],
NODEBB_DB_NAME: nconf.get(`${database}:database`) || req.body[`${database}:database`],
NODEBB_DB_SSL: nconf.get(`${database}:ssl`) || req.body[`${database}:ssl`],
defaultPlugins: JSON.stringify(nconf.get('defaultplugins') || nconf.get('defaultPlugins') || []),
};
winston.info('Starting setup process');
launchUrl = setupEnvVars.NODEBB_URL;
const child = require('child_process').fork('app', ['--setup'], {
env: setupEnvVars,
});
child.on('close', (data) => {
installing = false;
success = data === 0;
error = data !== 0;
welcome(req, res);
});
}
async function launch(req, res) {
try {
res.json({});
server.close();
req.setTimeout(0);
let child;
if (!nconf.get('launchCmd')) {
child = childProcess.spawn('node', ['loader.js'], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
});
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'],
});
}
const filesToDelete = [
'installer.css',
'installer.min.js',
'bootstrap.min.css',
];
await Promise.all(
filesToDelete.map(
filename => fs.promises.unlink(path.join(__dirname, '../public', filename))
)
);
child.unref();
process.exit(0);
} catch (err) {
winston.error(err.stack);
throw err;
}
}
// 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),
]);
}
async function compileLess() {
try {
const installSrc = path.join(__dirname, '../public/less/install.less');
const style = await fs.promises.readFile(installSrc);
const css = await less.render(String(style), { filename: path.resolve(installSrc) });
await fs.promises.writeFile(path.join(__dirname, '../public/installer.css'), css.css);
} catch (err) {
winston.error(`Unable to compile LESS: \n${err.stack}`);
throw err;
}
}
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);
}
async function loadDefaults() {
const setupDefaultsPath = path.join(__dirname, '../setup.json');
try {
// eslint-disable-next-line no-bitwise
await fs.promises.access(setupDefaultsPath, fs.constants.F_OK | fs.constants.R_OK);
} catch (err) {
// setup.json not found or inaccessible, proceed with no defaults
if (err.code !== 'ENOENT') {
throw err;
}
return;
}
winston.info('[installer] Found setup.json, populating default values');
nconf.file({
file: setupDefaultsPath,
});
}