Commit 82ea0b50 authored by NGPixel's avatar NGPixel

feat: config wizard UI improv. + upgrade from Mongo

parent ba1d83eb
...@@ -3,8 +3,9 @@ extends: ...@@ -3,8 +3,9 @@ extends:
- plugin:vue/recommended - plugin:vue/recommended
env: env:
node: true node: true
es6: true
jest: true jest: true
parserOptions:
ecmaVersion: 2017
globals: globals:
document: false document: false
navigator: false navigator: false
......
<svg
xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'>
<g fill-rule='evenodd'>
<g fill='#1976d2' fill-opacity='0.52'>
<path opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/>
<path d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/>
</g>
</g>
</svg>
\ No newline at end of file
...@@ -43,7 +43,6 @@ export default { ...@@ -43,7 +43,6 @@ export default {
gitUrl: '', gitUrl: '',
gitUseRemote: (siteConfig.git !== false), gitUseRemote: (siteConfig.git !== false),
lang: siteConfig.lang || 'en', lang: siteConfig.lang || 'en',
mongo: 'mongodb://',
path: siteConfig.path || '/', path: siteConfig.path || '/',
pathRepo: './repo', pathRepo: './repo',
port: siteConfig.port || 80, port: siteConfig.port || 80,
...@@ -51,7 +50,9 @@ export default { ...@@ -51,7 +50,9 @@ export default {
selfregister: (siteConfig.selfregister === true), selfregister: (siteConfig.selfregister === true),
telemetry: true, telemetry: true,
title: siteConfig.title || 'Wiki', title: siteConfig.title || 'Wiki',
upgrade: false upgrade: false,
upgMongo: 'mongodb://',
upgUserGroups: false
}, },
considerations: { considerations: {
https: false, https: false,
......
.config-manager { .config-manager {
background-image: linear-gradient(to bottom right, mc('blue', '500'), mc('blue', '700')); background-color: #1565c0;
background-repeat: no-repeat; background-image: url('../svg/config-bg.svg');
width: 100%; width: 100%;
min-height: 100%; min-height: 100%;
padding-top: 1rem; 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 { .welcome {
text-align: center; text-align: center;
padding: 1rem 0 2rem 0; padding: 1rem 0 2rem 0;
...@@ -81,4 +69,21 @@ ...@@ -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;
}
} }
...@@ -167,7 +167,7 @@ ...@@ -167,7 +167,7 @@
input[type=checkbox] + label { input[type=checkbox] + label {
&:before, &:after { &:before, &:after {
border-radius: 0; border-radius: 3px;
} }
} }
......
...@@ -39,8 +39,14 @@ redis: ...@@ -39,8 +39,14 @@ redis:
db: 0 db: 0
password: null password: null
# Enable for right to left languages (e.g. arabic): # ---------------------------------------------------------------------
langRtl: false # Configuration Mode
# ---------------------------------------------------------------------
# Possible values:
# - interactive (default)
# - file
configMode: interactive
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Background Workers # Background Workers
...@@ -55,5 +61,5 @@ workers: 0 ...@@ -55,5 +61,5 @@ workers: 0
# Read the docs BEFORE changing these settings! # Read the docs BEFORE changing these settings!
ha: ha:
nodeuid: primary node: primary
readonly: false readonly: false
...@@ -20,9 +20,10 @@ defaults: ...@@ -20,9 +20,10 @@ defaults:
port: 6379 port: 6379
db: 0 db: 0
password: null password: null
configMode: interactive
workers: 0 workers: 0
ha: ha:
nodeuid: primary node: primary
readonly: false readonly: false
site: site:
path: '' path: ''
......
...@@ -8,6 +8,8 @@ module.exports = () => { ...@@ -8,6 +8,8 @@ module.exports = () => {
title: 'Wiki.js' title: 'Wiki.js'
} }
wiki.system = require('./modules/system')
// ---------------------------------------- // ----------------------------------------
// Load modules // Load modules
// ---------------------------------------- // ----------------------------------------
...@@ -18,11 +20,12 @@ module.exports = () => { ...@@ -18,11 +20,12 @@ module.exports = () => {
const favicon = require('serve-favicon') const favicon = require('serve-favicon')
const http = require('http') const http = require('http')
const Promise = require('bluebird') const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra')) const fs = require('fs-extra')
const yaml = require('js-yaml') const yaml = require('js-yaml')
const _ = require('lodash') const _ = require('lodash')
const cfgHelper = require('./helpers/config') const cfgHelper = require('./helpers/config')
const filesize = require('filesize.js') const filesize = require('filesize.js')
const crypto = Promise.promisifyAll(require('crypto'))
// ---------------------------------------- // ----------------------------------------
// Define Express App // Define Express App
...@@ -58,14 +61,13 @@ module.exports = () => { ...@@ -58,14 +61,13 @@ module.exports = () => {
// Controllers // Controllers
// ---------------------------------------- // ----------------------------------------
app.get('*', (req, res) => { app.get('*', async (req, res) => {
fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => { let packageObj = await fs.readJson(path.join(wiki.ROOTPATH, 'package.json'))
res.render('configure/index', { res.render('configure/index', {
packageObj, packageObj,
telemetryClientID: wiki.telemetry.cid telemetryClientID: wiki.telemetry.cid
}) })
}) })
})
/** /**
* Perform basic system checks * Perform basic system checks
...@@ -120,7 +122,7 @@ module.exports = () => { ...@@ -120,7 +122,7 @@ module.exports = () => {
throw new Error('config.yml file is not writable by Node.js process or was not created properly.') 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.') }).return('config.yml is writable by the setup process.')
} }
], test => { return test() }).then(results => { ], test => test()).then(results => {
res.json({ ok: true, results }) res.json({ ok: true, results })
}).catch(err => { }).catch(err => {
res.json({ ok: false, error: err.message }) res.json({ ok: false, error: err.message })
...@@ -151,10 +153,10 @@ module.exports = () => { ...@@ -151,10 +153,10 @@ module.exports = () => {
Promise.mapSeries([ 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 => { return exec.stdout('git', ['init'], { cwd: gitDir }).then(result => {
...@@ -181,7 +183,10 @@ module.exports = () => { ...@@ -181,7 +183,10 @@ module.exports = () => {
}, },
() => { () => {
if (req.body.gitUseRemote === false) { return false } 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 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.' return 'Git SSH Private Key path has been set successfully.'
}) })
...@@ -220,120 +225,38 @@ module.exports = () => { ...@@ -220,120 +225,38 @@ module.exports = () => {
/** /**
* Finalize * Finalize
*/ */
app.post('/finalize', (req, res) => { app.post('/finalize', async (req, res) => {
wiki.telemetry.sendEvent('setup', 'finalize') wiki.telemetry.sendEvent('setup', 'finalize')
const bcrypt = require('bcryptjs-then') try {
const crypto = Promise.promisifyAll(require('crypto')) // Upgrade from Wiki.js 1.x?
let mongo = require('mongodb').MongoClient if (req.body.upgrade) {
let parsedMongoConStr = cfgHelper.parseConfigValue(req.body.db) await wiki.system.upgradeFromMongo({
mongoCnStr: cfgHelper.parseConfigValue(req.body.upgMongo)
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)
} }
})
}), // Load configuration file
fs.readFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), 'utf8').then(confRaw => { let confRaw = await fs.readFile(path.join(wiki.ROOTPATH, 'config.yml'), 'utf8')
let conf = yaml.safeLoad(confRaw) let conf = yaml.safeLoad(confRaw)
conf.title = req.body.title
// Update config
conf.host = req.body.host conf.host = req.body.host
conf.port = req.body.port conf.port = req.body.port
conf.paths = { conf.paths.repo = req.body.pathRepo
repo: req.body.pathRepo,
data: req.body.pathData // Generate session secret
} let sessionSecret = (await crypto.randomBytesAsync(32)).toString('hex')
conf.uploads = { console.info(sessionSecret)
maxImageFileSize: (conf.uploads && _.isNumber(conf.uploads.maxImageFileSize)) ? conf.uploads.maxImageFileSize : 3,
maxOtherFileSize: (conf.uploads && _.isNumber(conf.uploads.maxOtherFileSize)) ? conf.uploads.maxOtherFileSize : 100 // Save updated config to file
}
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) confRaw = yaml.safeDump(conf)
return fs.writeFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), confRaw) await fs.writeFile(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(() => {
res.json({ ok: true }) res.json({ ok: true })
}).catch(err => { } catch (err) {
res.json({ ok: false, error: err.message }) res.json({ ok: false, error: err.message })
}) }
}) })
/** /**
......
'use strict' /* global wiki */
/* global winston, ROOTPATH, appconfig */
const Promise = require('bluebird') const Promise = require('bluebird')
const crypto = require('crypto') // const pm2 = Promise.promisifyAll(require('pm2'))
const fs = Promise.promisifyAll(require('fs-extra')) // const _ = require('lodash')
const https = require('follow-redirects').https const cfgHelper = require('../helpers/config')
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')
module.exports = { 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 * @param {Object} opts Options object
* @returns {Promise} Promise of the operation
*/ */
install (targetTag) { async upgradeFromMongo (opts) {
let self = this wiki.telemetry.sendEvent('setup', 'upgradeFromMongo')
self._installDir = path.resolve(ROOTPATH, appconfig.paths.data, 'install')
return fs.ensureDirAsync(self._installDir).then(() => { let mongo = require('mongodb').MongoClient
return fs.emptyDirAsync(self._installDir) let parsedMongoConStr = cfgHelper.parseConfigValue(opts.mongoCnStr)
}).then(() => {
let remoteURL = _.replace(self._remoteFile, '{0}', targetTag)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
/** // Connect to MongoDB
* Fetch tarball and extract to temporary folder
*/ return mongo.connect(parsedMongoConStr, {
https.get(remoteURL, resp => { autoReconnect: false,
if (resp.statusCode !== 200) { reconnectTries: 2,
return reject(new Error('Remote file not found')) 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!')
} }
winston.info('[SERVER.System] Install tarball found. Downloading...')
resp.pipe(zlib.createGunzip()) // Fetch all users
.pipe(tar.Extract({ path: self._installDir })) let userData = await users.find({}).toArray()
.on('error', err => reject(err)) console.info(userData)
.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)
})
},
/** resolve(true)
* Replace file if different } catch (err) {
*/ reject(err)
replaceFile () { db.close()
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<String>} 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
}
}) })
} }
} }
...@@ -187,7 +187,7 @@ block body ...@@ -187,7 +187,7 @@ block body
label.label Authentication label.label Authentication
select(v-model='conf.gitAuthType') select(v-model='conf.gitAuthType')
option(value='ssh') SSH using Private Key file (recommended) 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='sshdb') SSH using Private Key in database
option(value='basic') Basic Credentials option(value='basic') Basic Credentials
span.desc The authentication method used to connect to your remote Git repository. span.desc The authentication method used to connect to your remote Git repository.
...@@ -317,11 +317,11 @@ block body ...@@ -317,11 +317,11 @@ block body
section section
p.control.is-fullwidth p.control.is-fullwidth
label.label Connection String to Wiki.js 1.x MongoDB database 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. ] 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 section
p.control.is-fullwidth 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 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. 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 .panel-footer
...@@ -370,6 +370,6 @@ block body ...@@ -370,6 +370,6 @@ block body
.panel-footer .panel-footer
button.button.is-small.is-green(disabled='disabled') Start button.button.is-small.is-green(disabled='disabled') Start
.footer footer
small Wiki.js Installation Wizard small Wiki.js Installation Wizard
small(v-if='conf.telemetry') Telemetry Client ID: !{telemetryClientID} small(v-if='conf.telemetry') Telemetry Client ID: !{telemetryClientID}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment