feat: config wizard UI improv. + upgrade from Mongo

This commit is contained in:
NGPixel 2017-11-19 17:08:23 -05:00
parent ba1d83ebcb
commit 82ea0b50fb
10 changed files with 131 additions and 268 deletions

View File

@ -3,8 +3,9 @@ extends:
- plugin:vue/recommended
env:
node: true
es6: true
jest: true
parserOptions:
ecmaVersion: 2017
globals:
document: false
navigator: false

9
assets/svg/config-bg.svg Normal file
View File

@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -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,

View File

@ -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;
}
}

View File

@ -167,7 +167,7 @@
input[type=checkbox] + label {
&:before, &:after {
border-radius: 0;
border-radius: 3px;
}
}

View File

@ -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

View File

@ -20,9 +20,10 @@ defaults:
port: 6379
db: 0
password: null
configMode: interactive
workers: 0
ha:
nodeuid: primary
node: primary
readonly: false
site:
path: ''

View File

@ -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 })
})
}
})
/**

View File

@ -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<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
}
// 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()
}
})
})
}
}

View File

@ -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}