Browse Source

feat: setup wizard cleanup + upgrade UI

[ci skip]
pull/621/head
NGPixel 7 years ago
parent
commit
ca8451720f
6 changed files with 77 additions and 218 deletions
  1. 12
      client/js/app.js
  2. 76
      client/js/components/config-manager.component.js
  3. 4
      server/app/data.yml
  4. 43
      server/configure.js
  5. 64
      server/controllers/admin.js
  6. 96
      server/views/configure/index.pug

12
client/js/app.js

@ -8,6 +8,7 @@ import CONSTANTS from './constants'
import Vue from 'vue'
import VueResource from 'vue-resource'
import VueClipboards from 'vue-clipboards'
import VeeValidate from 'vee-validate'
import { ApolloClient, createBatchingNetworkInterface } from 'apollo-client'
import store from './store'
@ -83,6 +84,17 @@ Vue.use(VueResource)
Vue.use(VueClipboards)
Vue.use(localization.VueI18Next)
Vue.use(helpers)
Vue.use(VeeValidate, {
enableAutoClasses: true,
classNames: {
touched: 'is-touched', // the control has been blurred
untouched: 'is-untouched', // the control hasn't been blurred
valid: 'is-valid', // model is valid
invalid: 'is-invalid', // model is invalid
pristine: 'is-pristine', // control has not been interacted with
dirty: 'is-dirty' // control has been interacted with
}
})
// ====================================
// Register Vue Components

76
client/js/components/config-manager.component.js

@ -1,22 +1,7 @@
'use strict'
/* global siteConfig */
import VeeValidate from 'vee-validate'
import axios from 'axios'
Vue.use(VeeValidate, {
enableAutoClasses: true,
classNames: {
touched: 'is-touched', // the control has been blurred
untouched: 'is-untouched', // the control hasn't been blurred
valid: 'is-valid', // model is valid
invalid: 'is-invalid', // model is invalid
pristine: 'is-pristine', // control has not been interacted with
dirty: 'is-dirty' // control has been interacted with
}
})
export default {
name: 'configManager',
data() {
@ -42,12 +27,12 @@ export default {
results: []
},
conf: {
upgrade: false,
title: siteConfig.title || 'Wiki',
host: siteConfig.host || 'http://',
port: siteConfig.port || 80,
lang: siteConfig.lang || 'en',
public: (siteConfig.public === true),
db: siteConfig.db || 'mongodb://localhost:27017/wiki',
pathData: './data',
pathRepo: './repo',
gitUseRemote: (siteConfig.git !== false),
@ -82,28 +67,19 @@ export default {
perc = (this.syscheck.ok) ? '15%' : '5%'
break
case 'general':
perc = '20%'
perc = '25%'
break
case 'considerations':
perc = '30%'
break
case 'db':
perc = '35%'
break
case 'dbcheck':
perc = (this.dbcheck.ok) ? '50%' : '40%'
break
case 'paths':
perc = '55%'
break
case 'git':
perc = '60%'
perc = '50%'
break
case 'gitcheck':
perc = (this.gitcheck.ok) ? '75%' : '65%'
perc = (this.gitcheck.ok) ? '70%' : '55%'
break
case 'admin':
perc = '80%'
perc = '75%'
break
}
return perc
@ -176,48 +152,6 @@ export default {
this.state = 'considerations'
this.loading = false
},
proceedToDb: function (ev) {
let self = this
self.state = 'db'
self.loading = false
self.$nextTick(() => {
self.$validator.validateAll('db')
})
},
proceedToDbcheck: function (ev) {
let self = this
this.state = 'dbcheck'
this.loading = true
self.dbcheck = {
ok: false,
error: ''
}
this.$helpers._.delay(() => {
axios.post('/dbcheck', {
db: self.conf.db
}).then(resp => {
if (resp.data.ok === true) {
self.dbcheck.ok = true
} else {
self.dbcheck.ok = false
self.dbcheck.error = resp.data.error
}
self.loading = false
self.$nextTick()
}).catch(err => {
window.alert(err.message)
})
}, 1000)
},
proceedToPaths: function (ev) {
let self = this
self.state = 'paths'
self.loading = false
self.$nextTick(() => {
self.$validator.validateAll('paths')
})
},
proceedToGit: function (ev) {
let self = this
self.state = 'git'

4
server/app/data.yml

@ -26,6 +26,7 @@ defaults:
readonly: false
site:
path: ''
lang: en
title: Wiki.js
configNamespaces:
- auth
@ -86,6 +87,9 @@ langs:
-
id: ko
name: Korean - 한국어
-
id: fa
name: Persian (Fārsi) - فارسی
-
id: pt
name: Portuguese - Português

43
server/configure.js

@ -53,7 +53,9 @@ module.exports = () => {
// ----------------------------------------
app.get('*', (req, res) => {
res.render('configure/index')
fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => {
res.render('configure/index', { packageObj })
})
})
/**
@ -63,7 +65,7 @@ module.exports = () => {
Promise.mapSeries([
() => {
const semver = require('semver')
if (!semver.satisfies(semver.clean(process.version), '>=6.9.0')) {
if (!semver.satisfies(semver.clean(process.version), '>=6.11.1')) {
throw new Error('Node.js version is too old. Minimum is 6.11.1.')
}
return 'Node.js ' + process.version + ' detected. Minimum is 6.11.1.'
@ -113,43 +115,6 @@ module.exports = () => {
})
})
/**
* Check the DB connection
*/
app.post('/dbcheck', (req, res) => {
let mongo = require('mongodb').MongoClient
let mongoURI = cfgHelper.parseConfigValue(req.body.db)
mongo.connect(mongoURI, {
autoReconnect: false,
reconnectTries: 2,
reconnectInterval: 1000,
connectTimeoutMS: 5000,
socketTimeoutMS: 5000
}, (err, db) => {
if (err === null) {
// Try to create a test collection
db.createCollection('test', (err, results) => {
if (err === null) {
// Try to drop test collection
db.dropCollection('test', (err, results) => {
if (err === null) {
res.json({ ok: true })
} else {
res.json({ ok: false, error: 'Unable to delete test collection. Verify permissions. ' + err.message })
}
db.close()
})
} else {
res.json({ ok: false, error: 'Unable to create test collection. Verify permissions. ' + err.message })
db.close()
}
})
} else {
res.json({ ok: false, error: err.message })
}
})
})
/**
* Check the Git connection
*/

64
server/controllers/admin.js

@ -1,6 +1,6 @@
'use strict'
/* global db, lang, rights, winston */
/* global wiki */
var express = require('express')
var router = express.Router()
@ -33,14 +33,14 @@ router.post('/profile', (req, res) => {
return res.render('error-forbidden')
}
return db.User.findById(req.user.id).then((usr) => {
return wiki.db.User.findById(req.user.id).then((usr) => {
usr.name = _.trim(req.body.name)
if (usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password)
if (nPwd.length < 6) {
return Promise.reject(new Error('New Password too short!'))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
return wiki.db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd
return usr.save()
})
@ -61,9 +61,9 @@ router.get('/stats', (req, res) => {
}
Promise.all([
db.Entry.count(),
db.UplFile.count(),
db.User.count()
wiki.db.Entry.count(),
wiki.db.UplFile.count(),
wiki.db.User.count()
]).spread((totalEntries, totalUploads, totalUsers) => {
return res.render('pages/admin/stats', {
totalEntries, totalUploads, totalUsers, adminTab: 'stats'
@ -78,7 +78,7 @@ router.get('/users', (req, res) => {
return res.render('error-forbidden')
}
db.User.find({})
wiki.db.User.find({})
.select('-password -rights')
.sort('name email')
.exec().then((usrs) => {
@ -95,7 +95,7 @@ router.get('/users/:id', (req, res) => {
return res.render('error-forbidden')
}
db.User.findById(req.params.id)
wiki.db.User.findById(req.params.id)
.select('-password -providerId')
.exec().then((usr) => {
let usrOpts = {
@ -137,12 +137,12 @@ router.post('/users/create', (req, res) => {
return res.status(400).json({ msg: 'Name is missing' })
}
db.User.findOne({ email: nUsr.email, provider: nUsr.provider }).then(exUsr => {
wiki.db.User.findOne({ email: nUsr.email, provider: nUsr.provider }).then(exUsr => {
if (exUsr) {
return res.status(400).json({ msg: 'User already exists!' }) || true
}
let pwdGen = (nUsr.provider === 'local') ? db.User.hashPassword(nUsr.password) : Promise.resolve(true)
let pwdGen = (nUsr.provider === 'local') ? wiki.db.User.hashPassword(nUsr.password) : Promise.resolve(true)
return pwdGen.then(nPwd => {
if (nUsr.provider !== 'local') {
nUsr.password = ''
@ -158,37 +158,37 @@ router.post('/users/create', (req, res) => {
deny: false
}]
return db.User.create(nUsr).then(() => {
return wiki.db.User.create(nUsr).then(() => {
return res.json({ ok: true })
})
}).catch(err => {
winston.warn(err)
wiki.logger.warn(err)
return res.status(500).json({ msg: err })
})
}).catch(err => {
winston.warn(err)
wiki.logger.warn(err)
return res.status(500).json({ msg: err })
})
})
router.post('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.status(401).json({ msg: lang.t('errors:unauthorized') })
return res.status(401).json({ msg: wiki.lang.t('errors:unauthorized') })
}
if (!validator.isMongoId(req.params.id)) {
return res.status(400).json({ msg: lang.t('errors:invaliduserid') })
return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') })
}
return db.User.findById(req.params.id).then((usr) => {
return wiki.db.User.findById(req.params.id).then((usr) => {
usr.name = _.trim(req.body.name)
usr.rights = JSON.parse(req.body.rights)
if (usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password)
if (nPwd.length < 6) {
return Promise.reject(new Error(lang.t('errors:newpasswordtooshort')))
return Promise.reject(new Error(wiki.lang.t('errors:newpasswordtooshort')))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
return wiki.db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd
return usr.save()
})
@ -199,7 +199,7 @@ router.post('/users/:id', (req, res) => {
}).then((usr) => {
// Update guest rights for future requests
if (usr.provider === 'local' && usr.email === 'guest') {
rights.guest = usr
wiki.rights.guest = usr
}
return usr
}).then(() => {
@ -214,14 +214,14 @@ router.post('/users/:id', (req, res) => {
*/
router.delete('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.status(401).json({ msg: lang.t('errors:unauthorized') })
return res.status(401).json({ msg: wiki.lang.t('errors:unauthorized') })
}
if (!validator.isMongoId(req.params.id)) {
return res.status(400).json({ msg: lang.t('errors:invaliduserid') })
return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') })
}
return db.User.findByIdAndRemove(req.params.id).then(() => {
return wiki.db.User.findByIdAndRemove(req.params.id).then(() => {
return res.json({ ok: true })
}).catch((err) => {
res.status(500).json({ ok: false, msg: err.message })
@ -249,7 +249,7 @@ router.get('/system', (req, res) => {
cwd: process.cwd()
}
fs.readJsonAsync(path.join(ROOTPATH, 'package.json')).then(packageObj => {
fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => {
axios.get('https://api.github.com/repos/Requarks/wiki/releases/latest').then(resp => {
let sysversion = {
current: 'v' + packageObj.version,
@ -259,7 +259,7 @@ router.get('/system', (req, res) => {
res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion })
}).catch(err => {
winston.warn(err)
wiki.logger.warn(err)
res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion: { current: 'v' + packageObj.version } })
})
})
@ -287,19 +287,19 @@ router.post('/theme', (req, res) => {
return res.render('error-forbidden')
}
if (!validator.isIn(req.body.primary, appdata.colors)) {
if (!validator.isIn(req.body.primary, wikin>pan class="p">.data.colors)) {
return res.status(406).json({ msg: 'Primary color is invalid.' })
} else if (!validator.isIn(req.body.alt, appdata.colors)) {
} else if (!validator.isIn(req.body.alt, wikin>pan class="p">.data.colors)) {
return res.status(406).json({ msg: 'Alternate color is invalid.' })
} else if (!validator.isIn(req.body.footer, appdata.colors)) {
} else if (!validator.isIn(req.body.footer, wikin>pan class="p">.data.colors)) {
return res.status(406).json({ msg: 'Footer color is invalid.' })
}
appconfig.theme.primary = req.body.primary
appconfig.theme.alt = req.body.alt
appconfig.theme.footer = req.body.footer
appconfig.theme.code.dark = req.body.codedark === 'true'
appconfig.theme.code.colorize = req.body.codecolorize === 'true'
wikin>pan class="p">.config.theme.primary = req.body.primary
wikin>pan class="p">.config.theme.alt = req.body.alt
wikin>pan class="p">.config.theme.footer = req.body.footer
wikin>pan class="p">.config.theme.code.dark = req.body.codedark === 'true'
wikin>pan class="p">.config.theme.code.colorize = req.body.codecolorize === 'true'
return res.json({ msg: 'OK' })
})

96
server/views/configure/index.pug

@ -23,6 +23,14 @@ block body
.panel-content.is-text
p This installation wizard will guide you through the steps needed to get your wiki up and running in no time!
p Detailed information about installation and usage can be found on the #[a(href='https://docs.wiki.requarks.io/') official documentation site]. #[br] Should you have any question or would like to report something that doesn't look right, feel free to create a new issue on the #[a(href='https://github.com/Requarks/wiki/issues') GitHub project].
.panel-content.form-sections
section
p #[i.nc-icon-outline.tech_cd-reader] You are about to install Wiki.js #[strong= packageObj.version].
section
p.control.is-fullwidth
input#ipt-upgrade(type='checkbox', v-model='conf.upgrade', name='ipt-upgrade')
label.label(for='ipt-upgrade') Upgrade from Wiki.js 1.x
span.desc Check this box if you are upgrading from Wiki.js 1.x and wish to migrate your existing data.
.panel-footer
.progress-bar: div(v-bind:style='{width: currentProgress}')
button.button.is-small.is-light-blue(v-on:click='proceedToSyscheck', v-bind:disabled='loading') Start
@ -49,6 +57,7 @@ block body
.progress-bar: div(v-bind:style='{width: currentProgress}')
button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToWelcome', v-bind:disabled='loading') Back
button.button.is-small.is-teal(v-on:click='proceedToSyscheck', v-if='!loading && !syscheck.ok') Check Again
button.button.is-small.is-red.is-outlined(v-on:click='proceedToGeneral', v-if='!loading && !syscheck.ok') Continue Anyway
button.button.is-small.is-light-blue(v-on:click='proceedToGeneral', v-if='loading || syscheck.ok', v-bind:disabled='loading') Continue
//- ==============================================
@ -70,7 +79,7 @@ block body
p.control.is-fullwidth
label.label Host
input(type='text', placeholder='http://', v-model='conf.host', data-vv-scope='general', name='ipt-host', v-validate='{ required: true, min: 4 }')
span.desc The full URL to your wiki, without the trailing slash. E.g.: http://wiki.domain.com. Note that sub-folders are #[u not supported].
span.desc The full URL to your wiki, without the trailing slash, e.g.: http://wiki.domain.com. Make sure to include the port if different than 80/443.
section
p.control
label.label Port
@ -79,10 +88,15 @@ block body
section
p.control
label.label Site UI Language
select(v-model='conf.site.lang')
select(v-model='conf.lang')
each lg in data.langs
option(value=lg.id)= lg.name
span.desc The language in which navigation, help and other UI elements will be displayed.
section
p.control.is-fullwidth
label.label Local Repository Path
input(type='text', placeholder='e.g. ./repo', v-model='conf.pathRepo', data-vv-scope='general', name='ipt-repopath', v-validate='{ required: true, min: 2 }')
span.desc The path where the local git repository will be created, used to store content in markdown files and uploads.#[br] #[strong It is recommended to leave the default value].
section
p.control.is-fullwidth
input#ipt-public(type='checkbox', v-model='conf.public', data-vv-scope='general', name='ipt-public')
@ -111,88 +125,18 @@ block body
li - Do not rewrite URLs after the domain. This can cause unexpected issues in Wiki.js navigation.
li - Do not remove or alter the client IP when proxying the requests. This can cause the authentication brute force protection to engage unexpectedly.
template(v-if='considerations.https')
h3 The site will not be using HTTPS? #[i.icon-warning-outline.animated.fadeOut.infinite]
h3 The site will not be using HTTPS? #[i.nc-icon-outline.ui-3_alert.animated.fadeOut.infinite]
p The host URL you specified is not HTTPS. It is highly recommended to use HTTPS. You must use a web server / proxy (e.g. nginx / apache / IIS) in front of Wiki.js to use HTTPS. Wiki.js does not provide HTTPS handling by itself.
template(v-if='considerations.port')
h3 You are using a non-standard port.
p If you are not planning on using a web server / proxy in front of Wiki.js, be aware that users will need to specify the port when accessing the wiki. Make sure this is the intended behavior. Otherwise set a standard HTTP port such as 80.
template(v-if='considerations.localhost')
h3 Are you sure you want to use localhost as the host base URL? #[i.icon-warning-outline.animated.fadeOut.infinite]
h3 Are you sure you want to use localhost as the host base URL? #[i.nc-icon-outline.ui-3_alert.animated.fadeOut.infinite]
p The host URL you specified is localhost. Unless you are a developer running Wiki.js locally on your machine, this is not recommended!
.panel-footer
.progress-bar: div(v-bind:style='{width: currentProgress}')
button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToGeneral', v-bind:disabled='loading') Back
button.button.is-small.is-light-blue(v-on:click='proceedToDb', v-bind:disabled='loading') Continue
//- ==============================================
//- DATABASE
//- ==============================================
template(v-else-if='state === "db"')
.panel
h2.panel-title.is-featured
span Database
i(v-if='loading')
.panel-content.is-text
p Wiki.js stores administrative data such as users, permissions and assets metadata in a MongoDB database. Article contents and uploads are <u>not</u> stored in the DB. Instead, they are stored on-disk and synced automatically with a remote git repository of your choice.
.panel-content.form-sections
section
p.control.is-fullwidth
label.label MongoDB Connection String
input(type='text', placeholder='e.g. mongodb://localhost:27017/wiki', v-model='conf.db', data-vv-scope='db', name='ipt-db', v-validate='{ required: true, min: 3 }')
span.desc The connection string to your MongoDB server. Leave the default localhost value if MongoDB is installed on the same server.<br />You can also specify an environment variable as the connection string, e.g. $(MONGO_URI).
.panel-footer
.progress-bar: div(v-bind:style='{width: currentProgress}')
button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToConsiderations', v-bind:disabled='loading') Back
button.button.is-small.is-light-blue(v-on:click='proceedToDbcheck', v-bind:disabled='loading || errors.any("db")') Connect
//- ==============================================
//- DATABASE CHECK
//- ==============================================
template(v-else-if='state === "dbcheck"')
.panel
h2.panel-title.is-featured
span Database Check
i(v-if='loading')
.panel-content.is-text
p(v-if='loading') #[i.icon-loader.animated.rotateIn.infinite] Testing the connection to MongoDB...
p(v-if='!loading && dbcheck.ok')
i.icon-check
strong Connected successfully!
p(v-if='!loading && !dbcheck.ok') #[i.icon-square-cross] Error: {{ dbcheck.error }}
.panel-footer
.progress-bar: div(v-bind:style='{width: currentProgress}')
button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToDb', v-bind:disabled='loading') Back
button.button.is-small.is-teal(v-on:click='proceedToDbcheck', v-if='!loading && !dbcheck.ok') Try Again
button.button.is-small.is-light-blue(v-on:click='proceedToPaths', v-if='loading || dbcheck.ok', v-bind:disabled='loading') Continue
//- ==============================================
//- PATHS
//- ==============================================
template(v-else-if='state === "paths"')
.panel
h2.panel-title.is-featured
span Paths
i(v-if='loading')
.panel-content.is-text
p It is recommended to leave the default values.
.panel-content.form-sections
section
p.control.is-fullwidth
label.label Local Data Path
input(type='text', placeholder='e.g. ./data', v-model='conf.pathData', data-vv-scope='paths', name='ipt-datapath', v-validate='{ required: true, min: 2 }')
span.desc The path where cache (processed content, thumbnails, search index, etc.) will be stored on disk.
section
p.control.is-fullwidth
label.label Local Repository Path
input(type='text', placeholder='e.g. ./repo', v-model='conf.pathRepo', data-vv-scope='paths', name='ipt-repopath', v-validate='{ required: true, min: 2 }')
span.desc The path where the local git repository will be created, used to store content in markdown files and uploads.
.panel-footer
.progress-bar: div(v-bind:style='{width: currentProgress}')
button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToDb', v-bind:disabled='loading') Back
button.button.is-small.is-light-blue(v-on:click='proceedToGit', v-bind:disabled='loading || errors.any("paths")') Continue
button.button.is-small.is-light-blue(v-on:click='proceedToGit', v-bind:disabled='loading') Continue
//- ==============================================
//- GIT
@ -256,7 +200,7 @@ block body
span.desc The default/fallback email to use when creating commits to the git repository.
.panel-footer
.progress-bar: div(v-bind:style='{width: currentProgress}')
button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToPaths', v-bind:disabled='loading') Back
button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToGeneral', v-bind:disabled='loading') Back
button.button.is-small.is-light-blue.is-outlined(v-on:click='conf.gitUseRemote = false; proceedToGitCheck()', v-bind:disabled='loading') Skip this step
button.button.is-small.is-light-blue(v-on:click='conf.gitUseRemote = true; proceedToGitCheck()', v-bind:disabled='loading || errors.any("git")') Continue

Loading…
Cancel
Save