From 82ea0b50fbb45e9152a45d20fb11ea9f87146a22 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 19 Nov 2017 17:08:23 -0500 Subject: [PATCH] feat: config wizard UI improv. + upgrade from Mongo --- .eslintrc.yml | 3 +- assets/svg/config-bg.svg | 9 + .../js/components/config-manager.component.js | 5 +- client/scss/components/config-manager.scss | 33 ++-- client/scss/components/form.scss | 2 +- config.sample.yml | 12 +- server/app/data.yml | 3 +- server/configure.js | 161 +++++------------ server/modules/system.js | 163 +++++------------- server/views/configure/index.pug | 8 +- 10 files changed, 131 insertions(+), 268 deletions(-) create mode 100644 assets/svg/config-bg.svg diff --git a/.eslintrc.yml b/.eslintrc.yml index bb336b51..22be4749 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,8 +3,9 @@ extends: - plugin:vue/recommended env: node: true - es6: true jest: true +parserOptions: + ecmaVersion: 2017 globals: document: false navigator: false diff --git a/assets/svg/config-bg.svg b/assets/svg/config-bg.svg new file mode 100644 index 00000000..ac056bf0 --- /dev/null +++ b/assets/svg/config-bg.svg @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/client/js/components/config-manager.component.js b/client/js/components/config-manager.component.js index 3a8e129a..99620c11 100644 --- a/client/js/components/config-manager.component.js +++ b/client/js/components/config-manager.component.js @@ -43,7 +43,6 @@ export default { gitUrl: '', gitUseRemote: (siteConfig.git !== false), lang: siteConfig.lang || 'en', - mongo: 'mongodb://', path: siteConfig.path || '/', pathRepo: './repo', port: siteConfig.port || 80, @@ -51,7 +50,9 @@ export default { selfregister: (siteConfig.selfregister === true), telemetry: true, title: siteConfig.title || 'Wiki', - upgrade: false + upgrade: false, + upgMongo: 'mongodb://', + upgUserGroups: false }, considerations: { https: false, diff --git a/client/scss/components/config-manager.scss b/client/scss/components/config-manager.scss index 67f50bc1..a89e62f5 100644 --- a/client/scss/components/config-manager.scss +++ b/client/scss/components/config-manager.scss @@ -1,22 +1,10 @@ .config-manager { - background-image: linear-gradient(to bottom right, mc('blue', '500'), mc('blue', '700')); - background-repeat: no-repeat; + background-color: #1565c0; + background-image: url('../svg/config-bg.svg'); width: 100%; min-height: 100%; padding-top: 1rem; - &::before { - content: ''; - position: absolute; - background-image: url('../svg/login-bg.svg'); - background-position: center bottom; - background-size: cover; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - } - .welcome { text-align: center; padding: 1rem 0 2rem 0; @@ -81,4 +69,21 @@ } } + + footer { + background-color: mc('blue','800'); + border-top: 1px solid mc('blue', '700'); + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 25px; + height: 70px; + font-size: 13px; + font-weight: 500; + color: mc('blue','200'); + position: absolute; + right: 0; + bottom: 0; + left: 0; + } } diff --git a/client/scss/components/form.scss b/client/scss/components/form.scss index cc63bb85..87df9b1d 100644 --- a/client/scss/components/form.scss +++ b/client/scss/components/form.scss @@ -167,7 +167,7 @@ input[type=checkbox] + label { &:before, &:after { - border-radius: 0; + border-radius: 3px; } } diff --git a/config.sample.yml b/config.sample.yml index 2cafcb3a..26b9fb3c 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -39,8 +39,14 @@ redis: db: 0 password: null -# Enable for right to left languages (e.g. arabic): -langRtl: false +# --------------------------------------------------------------------- +# Configuration Mode +# --------------------------------------------------------------------- +# Possible values: +# - interactive (default) +# - file + +configMode: interactive # --------------------------------------------------------------------- # Background Workers @@ -55,5 +61,5 @@ workers: 0 # Read the docs BEFORE changing these settings! ha: - nodeuid: primary + node: primary readonly: false diff --git a/server/app/data.yml b/server/app/data.yml index f16e0c61..82ef2ba2 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -20,9 +20,10 @@ defaults: port: 6379 db: 0 password: null + configMode: interactive workers: 0 ha: - nodeuid: primary + node: primary readonly: false site: path: '' diff --git a/server/configure.js b/server/configure.js index f7135b9f..94023cd5 100644 --- a/server/configure.js +++ b/server/configure.js @@ -8,6 +8,8 @@ module.exports = () => { title: 'Wiki.js' } + wiki.system = require('./modules/system') + // ---------------------------------------- // Load modules // ---------------------------------------- @@ -18,11 +20,12 @@ module.exports = () => { const favicon = require('serve-favicon') const http = require('http') const Promise = require('bluebird') - const fs = Promise.promisifyAll(require('fs-extra')) + const fs = require('fs-extra') const yaml = require('js-yaml') const _ = require('lodash') const cfgHelper = require('./helpers/config') const filesize = require('filesize.js') + const crypto = Promise.promisifyAll(require('crypto')) // ---------------------------------------- // Define Express App @@ -58,12 +61,11 @@ module.exports = () => { // Controllers // ---------------------------------------- - app.get('*', (req, res) => { - fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => { - res.render('configure/index', { - packageObj, - telemetryClientID: wiki.telemetry.cid - }) + app.get('*', async (req, res) => { + let packageObj = await fs.readJson(path.join(wiki.ROOTPATH, 'package.json')) + res.render('configure/index', { + packageObj, + telemetryClientID: wiki.telemetry.cid }) }) @@ -120,7 +122,7 @@ module.exports = () => { throw new Error('config.yml file is not writable by Node.js process or was not created properly.') }).return('config.yml is writable by the setup process.') } - ], test => { return test() }).then(results => { + ], test => test()).then(results => { res.json({ ok: true, results }) }).catch(err => { res.json({ ok: false, error: err.message }) @@ -151,10 +153,10 @@ module.exports = () => { Promise.mapSeries([ () => { - return fs.ensureDirAsync(dataDir).return('Data directory path is valid.') + return fs.ensureDir(dataDir).then(() => 'Data directory path is valid.') }, () => { - return fs.ensureDirAsync(gitDir).return('Git directory path is valid.') + return fs.ensureDir(gitDir).then(() => 'Git directory path is valid.') }, () => { return exec.stdout('git', ['init'], { cwd: gitDir }).then(result => { @@ -181,7 +183,10 @@ module.exports = () => { }, () => { if (req.body.gitUseRemote === false) { return false } - if (req.body.gitAuthType === 'ssh') { + if (_.includes(['sshenv', 'sshdb'], req.body.gitAuthType)) { + req.body.gitAuthSSHKey = path.join(dataDir, 'ssh/key.pem') + } + if (_.startsWith(req.body.gitAuthType, 'ssh')) { return exec.stdout('git', ['config', '--local', 'core.sshCommand', 'ssh -i "' + req.body.gitAuthSSHKey + '" -o StrictHostKeyChecking=no'], { cwd: gitDir }).then(result => { return 'Git SSH Private Key path has been set successfully.' }) @@ -220,120 +225,38 @@ module.exports = () => { /** * Finalize */ - app.post('/finalize', (req, res) => { + app.post('/finalize', async (req, res) => { wiki.telemetry.sendEvent('setup', 'finalize') - const bcrypt = require('bcryptjs-then') - const crypto = Promise.promisifyAll(require('crypto')) - let mongo = require('mongodb').MongoClient - let parsedMongoConStr = cfgHelper.parseConfigValue(req.body.db) - - Promise.join( - new Promise((resolve, reject) => { - mongo.connect(parsedMongoConStr, { - autoReconnect: false, - reconnectTries: 2, - reconnectInterval: 1000, - connectTimeoutMS: 5000, - socketTimeoutMS: 5000 - }, (err, db) => { - if (err === null) { - db.createCollection('users', { strict: false }, (err, results) => { - if (err === null) { - bcrypt.hash(req.body.adminPassword).then(adminPwdHash => { - db.collection('users').findOneAndUpdate({ - provider: 'local', - email: req.body.adminEmail - }, { - provider: 'local', - email: req.body.adminEmail, - name: 'Administrator', - password: adminPwdHash, - rights: [{ - role: 'admin', - path: '/', - exact: false, - deny: false - }], - updatedAt: new Date(), - createdAt: new Date() - }, { - upsert: true, - returnOriginal: false - }, (err, results) => { - if (err === null) { - resolve(true) - } else { - reject(err) - } - db.close() - }) - }) - } else { - reject(err) - db.close() - } - }) - } else { - reject(err) - } + try { + // Upgrade from Wiki.js 1.x? + if (req.body.upgrade) { + await wiki.system.upgradeFromMongo({ + mongoCnStr: cfgHelper.parseConfigValue(req.body.upgMongo) }) - }), - fs.readFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), 'utf8').then(confRaw => { - let conf = yaml.safeLoad(confRaw) - conf.title = req.body.title - conf.host = req.body.host - conf.port = req.body.port - conf.paths = { - repo: req.body.pathRepo, - data: req.body.pathData - } - conf.uploads = { - maxImageFileSize: (conf.uploads && _.isNumber(conf.uploads.maxImageFileSize)) ? conf.uploads.maxImageFileSize : 3, - maxOtherFileSize: (conf.uploads && _.isNumber(conf.uploads.maxOtherFileSize)) ? conf.uploads.maxOtherFileSize : 100 - } - conf.lang = req.body.lang - conf.public = (req.body.public === true) - if (conf.auth && conf.auth.local) { - conf.auth.local = { enabled: true } - } else { - conf.auth = { local: { enabled: true } } - } - conf.db = req.body.db - if (req.body.gitUseRemote === false) { - conf.git = false - } else { - conf.git = { - url: req.body.gitUrl, - branch: req.body.gitBranch, - auth: { - type: req.body.gitAuthType, - username: req.body.gitAuthUser, - password: req.body.gitAuthPass, - privateKey: req.body.gitAuthSSHKey, - sslVerify: (req.body.gitAuthSSL === true) - }, - showUserEmail: (req.body.gitShowUserEmail === true), - serverEmail: req.body.gitServerEmail - } - } - return crypto.randomBytesAsync(32).then(buf => { - conf.sessionSecret = buf.toString('hex') - confRaw = yaml.safeDump(conf) - return fs.writeFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), confRaw) - }) - }) - ).then(() => { - if (process.env.IS_HEROKU) { - return fs.outputJsonAsync(path.join(wiki.SERVERPATH, 'app/heroku.json'), { configured: true }) - } else { - return true } - }).then(() => { + + // Load configuration file + let confRaw = await fs.readFile(path.join(wiki.ROOTPATH, 'config.yml'), 'utf8') + let conf = yaml.safeLoad(confRaw) + + // Update config + conf.host = req.body.host + conf.port = req.body.port + conf.paths.repo = req.body.pathRepo + + // Generate session secret + let sessionSecret = (await crypto.randomBytesAsync(32)).toString('hex') + console.info(sessionSecret) + + // Save updated config to file + confRaw = yaml.safeDump(conf) + await fs.writeFile(path.join(wiki.ROOTPATH, 'config.yml'), confRaw) + res.json({ ok: true }) - }).catch(err => { + } catch (err) { res.json({ ok: false, error: err.message }) - }) + } }) /** diff --git a/server/modules/system.js b/server/modules/system.js index 3c76645f..a507208c 100644 --- a/server/modules/system.js +++ b/server/modules/system.js @@ -1,136 +1,53 @@ -'use strict' - -/* global winston, ROOTPATH, appconfig */ +/* global wiki */ const Promise = require('bluebird') -const crypto = require('crypto') -const fs = Promise.promisifyAll(require('fs-extra')) -const https = require('follow-redirects').https -const klaw = require('klaw') -const path = require('path') -const pm2 = Promise.promisifyAll(require('pm2')) -const tar = require('tar') -const through2 = require('through2') -const zlib = require('zlib') -const _ = require('lodash') +// const pm2 = Promise.promisifyAll(require('pm2')) +// const _ = require('lodash') +const cfgHelper = require('../helpers/config') module.exports = { - - _remoteFile: 'https://github.com/Requarks/wiki/releases/download/{0}/wiki-js.tar.gz', - _installDir: '', - /** - * Install a version of Wiki.js + * Upgrade from Wiki.js 1.x - MongoDB database * - * @param {any} targetTag The version to install - * @returns {Promise} Promise of the operation + * @param {Object} opts Options object */ - install (targetTag) { - let self = this + async upgradeFromMongo (opts) { + wiki.telemetry.sendEvent('setup', 'upgradeFromMongo') - self._installDir = path.resolve(ROOTPATH, appconfig.paths.data, 'install') + let mongo = require('mongodb').MongoClient + let parsedMongoConStr = cfgHelper.parseConfigValue(opts.mongoCnStr) - return fs.ensureDirAsync(self._installDir).then(() => { - return fs.emptyDirAsync(self._installDir) - }).then(() => { - let remoteURL = _.replace(self._remoteFile, '{0}', targetTag) - - return new Promise((resolve, reject) => { - /** - * Fetch tarball and extract to temporary folder - */ - https.get(remoteURL, resp => { - if (resp.statusCode !== 200) { - return reject(new Error('Remote file not found')) - } - winston.info('[SERVER.System] Install tarball found. Downloading...') - - resp.pipe(zlib.createGunzip()) - .pipe(tar.Extract({ path: self._installDir })) - .on('error', err => reject(err)) - .on('end', () => { - winston.info('[SERVER.System] Tarball extracted. Comparing files...') - /** - * Replace old files - */ - klaw(self._installDir) - .on('error', err => reject(err)) - .on('end', () => { - winston.info('[SERVER.System] All files were updated successfully.') - resolve(true) - }) - .pipe(self.replaceFile()) - }) - }) - }) - }).then(() => { - winston.info('[SERVER.System] Cleaning install leftovers...') - return fs.removeAsync(self._installDir).then(() => { - winston.info('[SERVER.System] Restarting Wiki.js...') - return pm2.restartAsync('wiki').catch(err => { // eslint-disable-line handle-callback-err - winston.error('Unable to restart Wiki.js via pm2... Do a manual restart!') - process.exit() - }) - }) - }).catch(err => { - winston.warn(err) - }) - }, - - /** - * Replace file if different - */ - replaceFile () { - let self = this - return through2.obj((item, enc, next) => { - if (!item.stats.isDirectory()) { - self.digestFile(item.path).then(sourceHash => { - let destFilePath = _.replace(item.path, self._installDir, ROOTPATH) - return self.digestFile(destFilePath).then(targetHash => { - if (sourceHash === targetHash) { - winston.log('verbose', '[SERVER.System] Skipping ' + destFilePath) - return fs.removeAsync(item.path).then(() => { - return next() || true - }) - } else { - winston.log('verbose', '[SERVER.System] Updating ' + destFilePath + '...') - return fs.moveAsync(item.path, destFilePath, { overwrite: true }).then(() => { - return next() || true - }) - } - }) - }).catch(err => { - throw err - }) - } else { - next() - } - }) - }, - - /** - * Generate the hash of a file - * - * @param {String} filePath The absolute path of the file - * @return {Promise} Promise of the hash result - */ - digestFile: (filePath) => { return new Promise((resolve, reject) => { - let hash = crypto.createHash('sha1') - hash.setEncoding('hex') - fs.createReadStream(filePath) - .on('error', err => { reject(err) }) - .on('end', () => { - hash.end() - resolve(hash.read()) - }) - .pipe(hash) - }).catch(err => { - if (err.code === 'ENOENT') { - return '0' - } else { - throw err - } + // Connect to MongoDB + + return mongo.connect(parsedMongoConStr, { + autoReconnect: false, + reconnectTries: 2, + reconnectInterval: 1000, + connectTimeoutMS: 5000, + socketTimeoutMS: 5000 + }, async (err, db) => { + try { + if (err !== null) { throw err } + + let users = db.collection('users') + + // Check if users table is populated + let userCount = await users.count() + if (userCount < 1) { + throw new Error('Users table is empty or invalid!') + } + + // Fetch all users + let userData = await users.find({}).toArray() + console.info(userData) + + resolve(true) + } catch (err) { + reject(err) + db.close() + } + }) }) } } diff --git a/server/views/configure/index.pug b/server/views/configure/index.pug index da5e2f4d..bf4b0816 100644 --- a/server/views/configure/index.pug +++ b/server/views/configure/index.pug @@ -187,7 +187,7 @@ block body label.label Authentication select(v-model='conf.gitAuthType') option(value='ssh') SSH using Private Key file (recommended) - option(value='sshenv') SSH using Private Key in env. variable + option(value='sshenv') SSH using Private Key in environment variable option(value='sshdb') SSH using Private Key in database option(value='basic') Basic Credentials span.desc The authentication method used to connect to your remote Git repository. @@ -317,11 +317,11 @@ block body section p.control.is-fullwidth label.label Connection String to Wiki.js 1.x MongoDB database - input(type='text', placeholder='mongodb://', v-model='conf.mongo', data-vv-scope='upgrade', name='ipt-mongo', v-validate='{ required: true, min: 2 }') + input(type='text', placeholder='mongodb://', v-model='conf.upgMongo', data-vv-scope='upgrade', name='ipt-mongo', v-validate='{ required: true, min: 2 }') span.desc A MongoDB database connection string where a Wiki.js 1.x installation is located. #[strong No alterations will be made to this database. ] section p.control.is-fullwidth - input#ipt-public(type='checkbox', v-model='conf.public', data-vv-scope='upgrade', name='ipt-public') + input#ipt-public(type='checkbox', v-model='conf.upgUserGroups', data-vv-scope='upgrade', name='ipt-public') label.label(for='ipt-public') Create groups based on individual permissions span.desc User groups will be created based on existing users permissions. If multiple users have the exact same permission rules, they will be put in the same user group. .panel-footer @@ -370,6 +370,6 @@ block body .panel-footer button.button.is-small.is-green(disabled='disabled') Start - .footer + footer small Wiki.js Installation Wizard small(v-if='conf.telemetry') Telemetry Client ID: !{telemetryClientID}