diff --git a/assets/browserconfig.xml b/assets/browserconfig.xml index d1d3ffa9..e5de2416 100644 --- a/assets/browserconfig.xml +++ b/assets/browserconfig.xml @@ -1,2 +1,11 @@ -#ffffff \ No newline at end of file + + + + + + + #ffffff + + + \ No newline at end of file diff --git a/assets/manifest.json b/assets/manifest.json index 7293c36d..a3511af5 100644 --- a/assets/manifest.json +++ b/assets/manifest.json @@ -42,5 +42,5 @@ "name": "Wiki", "short_name": "Wiki", "start_url": "/", - "theme_color": "#3f51b5" + "theme_color": "#0288d1" } diff --git a/client/js/components/editor.component.js b/client/js/components/editor.component.js index 455eaeec..41c8dc51 100644 --- a/client/js/components/editor.component.js +++ b/client/js/components/editor.component.js @@ -1,5 +1,3 @@ -'use strict' - /* global $, siteRoot */ let mde diff --git a/client/js/components/login.vue b/client/js/components/login.vue index be26dbf0..2a45f8b8 100644 --- a/client/js/components/login.vue +++ b/client/js/components/login.vue @@ -55,7 +55,8 @@ export default { this.selectedStrategy = key this.screen = 'login' if (!useForm) { - window.location.assign(siteConfig.path + 'login/' + key) + this.isLoading = true + window.location.assign(this.$helpers.resolvePath('login/' + key)) } else { this.$refs.iptEmail.focus() } diff --git a/client/js/components/navigator.vue b/client/js/components/navigator.vue index ebd8db33..076067bb 100644 --- a/client/js/components/navigator.vue +++ b/client/js/components/navigator.vue @@ -15,12 +15,24 @@ use(:xlink:href='subtitleIconClass') h2 {{subtitleText}} .navigator-action - .navigator-action-item + .navigator-action-item(:class='{"is-active": userMenuShown}', @click='toggleUserMenu') svg.icons.is-32(role='img') title User use(xlink:href='#nc-user-circle') + transition(name='navigator-action-item-dropdown') + ul.navigator-action-item-dropdown(v-show='userMenuShown', v-cloak) + li + label Account + svg.icons.is-24(role='img') + title Account + use(xlink:href='#nc-man-green') + li(@click='logout') + label Sign out + svg.icons.is-24(role='img') + title Sign Out + use(xlink:href='#nc-exit') transition(name='navigator-sd') - .navigator-sd(v-show='sdShown') + .navigator-sd(v-show='sdShown', v-cloak) .navigator-sd-actions a.is-active(href='', title='Search') svg.icons.is-24(role='img') @@ -77,7 +89,8 @@ export default { name: 'navigator', data() { return { - sdShown: false + sdShown: false, + userMenuShown: false } }, computed: { @@ -104,16 +117,39 @@ export default { methods: { toggleMainMenu() { this.sdShown = !this.sdShown + this.userMenuShown = false if (this.sdShown) { this.$nextTick(() => { + this.bindOutsideClick() this.$refs.iptSearch.focus() }) + } else { + this.unbindOutsideClick() } - // this.$store.dispatch('navigator/alert', { - // style: 'success', - // icon: 'gg-check', - // msg: 'Changes were saved successfully!' - // }) + }, + toggleUserMenu() { + this.userMenuShown = !this.userMenuShown + this.sdShown = false + if (this.userMenuShown) { + this.bindOutsideClick() + } else { + this.unbindOutsideClick() + } + }, + bindOutsideClick() { + document.addEventListener('mousedown', this.handleOutsideClick, false) + }, + unbindOutsideClick() { + document.removeEventListener('mousedown', this.handleOutsideClick, false) + }, + handleOutsideClick(ev) { + if (!this.$el.contains(ev.target)) { + this.sdShown = false + this.userMenuShown = false + } + }, + logout() { + window.location.assign(this.$helpers.resolvePath('logout')) } }, mounted() { diff --git a/client/js/components/setup.component.js b/client/js/components/setup.component.js index df7b30bc..ea686ad5 100644 --- a/client/js/components/setup.component.js +++ b/client/js/components/setup.component.js @@ -24,7 +24,7 @@ export default { final: { ok: false, error: '', - results: [] + redirectUrl: '' }, conf: { adminEmail: '', @@ -219,19 +219,32 @@ export default { self.final = { ok: false, error: '', - results: [] + redirectUrl: '' } this.$helpers._.delay(() => { axios.post('/finalize', self.conf).then(resp => { if (resp.data.ok === true) { - self.final.ok = true - self.final.results = resp.data.results + self.$helpers._.delay(() => { + self.final.ok = true + switch (resp.data.redirectPort) { + case 80: + self.final.redirectUrl = `http://${window.location.hostname}${resp.data.redirectPath}/login` + break + case 443: + self.final.redirectUrl = `https://${window.location.hostname}${resp.data.redirectPath}/login` + break + default: + self.final.redirectUrl = `http://${window.location.hostname}:${resp.data.redirectPort}${resp.data.redirectPath}/login` + break + } + self.loading = false + }, 5000) } else { self.final.ok = false self.final.error = resp.data.error + self.loading = false } - self.loading = false self.$nextTick() }).catch(err => { window.alert(err.message) @@ -239,18 +252,7 @@ export default { }, 1000) }, finish: function (ev) { - let self = this - self.state = 'restart' - - this.$helpers._.delay(() => { - axios.post('/restart', {}).then(resp => { - this.$helpers._.delay(() => { - window.location.assign(self.conf.host) - }, 30000) - }).catch(err => { - window.alert(err.message) - }) - }, 1000) + window.location.assign(this.final.redirectUrl) } } } diff --git a/client/js/helpers/common.js b/client/js/helpers/common.js deleted file mode 100644 index bfbeabc5..00000000 --- a/client/js/helpers/common.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -import filesize from 'filesize.js' -import toUpper from 'lodash/toUpper' - -module.exports = { - /** - * Convert bytes to humanized form - * @param {number} rawSize Size in bytes - * @returns {string} Humanized file size - */ - filesize(rawSize) { - return toUpper(filesize(rawSize)) - } -} diff --git a/client/js/helpers/form.js b/client/js/helpers/form.js deleted file mode 100644 index 57468775..00000000 --- a/client/js/helpers/form.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict' - -module.exports = { - /** - * Set Input Selection - * @param {DOMElement} input The input element - * @param {number} startPos The starting position - * @param {nunber} endPos The ending position - */ - setInputSelection: (input, startPos, endPos) => { - input.focus() - if (typeof input.selectionStart !== 'undefined') { - input.selectionStart = startPos - input.selectionEnd = endPos - } else if (document.selection && document.selection.createRange) { - // IE branch - input.select() - var range = document.selection.createRange() - range.collapse(true) - range.moveEnd('character', endPos) - range.moveStart('character', startPos) - range.select() - } - } -} diff --git a/client/js/helpers/index.js b/client/js/helpers/index.js index 6335809b..1b9c9452 100644 --- a/client/js/helpers/index.js +++ b/client/js/helpers/index.js @@ -1,10 +1,59 @@ -'use strict' +import filesize from 'filesize.js' +/* global siteConfig */ + +const _ = require('./lodash') const helpers = { - _: require('./lodash'), - common: require('./common'), - form: require('./form'), - pages: require('./pages') + /** + * Minimal set of lodash functions + */ + _, + /** + * Convert bytes to humanized form + * @param {number} rawSize Size in bytes + * @returns {string} Humanized file size + */ + filesize (rawSize) { + return _.toUpper(filesize(rawSize)) + }, + /** + * Convert raw path to safe path + * @param {string} rawPath Raw path + * @returns {string} Safe path + */ + makeSafePath (rawPath) { + let rawParts = _.split(_.trim(rawPath), '/') + rawParts = _.map(rawParts, (r) => { + return _.kebabCase(_.deburr(_.trim(r))) + }) + + return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/') + }, + resolvePath (path) { + if (_.startsWith(path, '/')) { path = path.substring(1) } + return `${siteConfig.path}${path}` + }, + /** + * Set Input Selection + * @param {DOMElement} input The input element + * @param {number} startPos The starting position + * @param {nunber} endPos The ending position + */ + setInputSelection (input, startPos, endPos) { + input.focus() + if (typeof input.selectionStart !== 'undefined') { + input.selectionStart = startPos + input.selectionEnd = endPos + } else if (document.selection && document.selection.createRange) { + // IE branch + input.select() + var range = document.selection.createRange() + range.collapse(true) + range.moveEnd('character', endPos) + range.moveStart('character', startPos) + range.select() + } + } } export default { diff --git a/client/js/helpers/lodash.js b/client/js/helpers/lodash.js index 218a1837..7ea20842 100644 --- a/client/js/helpers/lodash.js +++ b/client/js/helpers/lodash.js @@ -1,5 +1,3 @@ -'use strict' - // ==================================== // Load minimal lodash // ==================================== diff --git a/client/js/helpers/pages.js b/client/js/helpers/pages.js deleted file mode 100644 index 20781aec..00000000 --- a/client/js/helpers/pages.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -import deburr from 'lodash/deburr' -import filter from 'lodash/filter' -import isEmpty from 'lodash/isEmpty' -import join from 'lodash/join' -import kebabCase from 'lodash/kebabCase' -import map from 'lodash/map' -import split from 'lodash/split' -import trim from 'lodash/trim' - -module.exports = { - /** - * Convert raw path to safe path - * @param {string} rawPath Raw path - * @returns {string} Safe path - */ - makeSafePath: (rawPath) => { - let rawParts = split(trim(rawPath), '/') - rawParts = map(rawParts, (r) => { - return kebabCase(deburr(trim(r))) - }) - - return join(filter(rawParts, (r) => { return !isEmpty(r) }), '/') - } -} diff --git a/client/js/modules/localization.js b/client/js/modules/localization.js index 2ac94cd7..c13e2719 100644 --- a/client/js/modules/localization.js +++ b/client/js/modules/localization.js @@ -45,7 +45,7 @@ module.exports = { defaultNS: 'common', lng: siteConfig.lang, fallbackLng: siteConfig.lang, - ns: ['common', 'admin', 'auth'] + ns: ['common', 'auth'] }) return new VueI18Next(i18next) } diff --git a/client/js/pages/admin-edit-user.component.js b/client/js/pages/admin-edit-user.component.js index 86010577..17eeb998 100644 --- a/client/js/pages/admin-edit-user.component.js +++ b/client/js/pages/admin-edit-user.component.js @@ -1,5 +1,3 @@ -'use strict' - export default { name: 'admin-edit-user', props: ['usrdata'], diff --git a/client/js/pages/admin-profile.component.js b/client/js/pages/admin-profile.component.js index ec5843c9..935d2fed 100644 --- a/client/js/pages/admin-profile.component.js +++ b/client/js/pages/admin-profile.component.js @@ -1,5 +1,3 @@ -'use strict' - export default { name: 'admin-profile', props: ['email', 'name', 'provider', 'tfaIsActive'], diff --git a/client/js/pages/admin-settings.component.js b/client/js/pages/admin-settings.component.js index a6395a91..573a1f95 100644 --- a/client/js/pages/admin-settings.component.js +++ b/client/js/pages/admin-settings.component.js @@ -1,5 +1,3 @@ -'use strict' - export default { name: 'admin-settings', data() { diff --git a/client/js/pages/admin-theme.component.js b/client/js/pages/admin-theme.component.js index e70081bc..cccb702c 100644 --- a/client/js/pages/admin-theme.component.js +++ b/client/js/pages/admin-theme.component.js @@ -1,5 +1,3 @@ -'use strict' - export default { name: 'admin-theme', props: ['themedata'], diff --git a/client/js/pages/content-view.component.js b/client/js/pages/content-view.component.js index 9b7bc1bf..24fd66f3 100644 --- a/client/js/pages/content-view.component.js +++ b/client/js/pages/content-view.component.js @@ -1,5 +1,3 @@ -'use strict' - /* global $ */ export default { diff --git a/client/js/pages/source-view.component.js b/client/js/pages/source-view.component.js index 6cfb0cfa..e1ba5245 100644 --- a/client/js/pages/source-view.component.js +++ b/client/js/pages/source-view.component.js @@ -1,5 +1,3 @@ -'use strict' - export default { name: 'source-view', data() { diff --git a/client/js/store/modules/anchor.js b/client/js/store/modules/anchor.js index 999131b9..e6696a17 100644 --- a/client/js/store/modules/anchor.js +++ b/client/js/store/modules/anchor.js @@ -1,5 +1,3 @@ -'use strict' - export default { namespaced: true, state: { @@ -15,7 +13,6 @@ export default { }, actions: { open({ commit }, hash) { - console.info('MIGUEL!') commit('anchorChange', { shown: true, hash }) }, close({ commit }) { diff --git a/client/scss/components/navigator.scss b/client/scss/components/navigator.scss index 8e33e1a1..3280b142 100644 --- a/client/scss/components/navigator.scss +++ b/client/scss/components/navigator.scss @@ -3,6 +3,7 @@ top: 0; left: 0; width: 100%; + z-index: 100; &-bar { display: flex; @@ -123,6 +124,7 @@ display: flex; justify-content: flex-end; align-items: stretch; + position: relative; &-item { display: flex; @@ -130,6 +132,7 @@ align-items: center; width: 50px; cursor: pointer; + transition: all .4s ease; svg use { color: #FFF; @@ -143,6 +146,60 @@ fill: mc('blue', '500'); } } + + &.is-active { + background-color: #FFF; + + svg use { + color: mc('grey', '500'); + fill: mc('grey', '500'); + } + } + + &-dropdown { + position: absolute; + right: 0; + top: 100%; + width: 250px; + border-radius: 0 0 0 5px; + transition: all .4s ease; + transform-origin: top right; + + &-enter-active, &-leave-active { + transform: scale(1); + } + &-enter, &-leave-to { + transform: scale(.1, 0); + } + + li { + background-color: #FFF; + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 .8rem 0 1rem; + + & + li { + border-top: 1px solid mc('grey', '100'); + } + + &:hover { + background-color: mc('grey', '100'); + } + + label { + font-size: .8rem; + font-weight: 600; + color: mc('blue', '800'); + text-transform: uppercase; + } + + &:last-child { + border-radius: 0 0 0 5px; + } + } + } } } diff --git a/client/svg/icons.svg b/client/svg/icons.svg index a43d671e..9559bb12 100644 --- a/client/svg/icons.svg +++ b/client/svg/icons.svg @@ -166,7 +166,7 @@ - + @@ -288,7 +288,7 @@ - + @@ -440,4 +440,12 @@ + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index b763c21d..118e1a99 100644 --- a/package.json +++ b/package.json @@ -103,11 +103,13 @@ "passport-auth0": "0.6.1", "passport-azure-ad-oauth2": "0.0.4", "passport-discord": "0.1.3", + "passport-dropbox-oauth2": "1.1.0", "passport-facebook": "2.1.1", "passport-github2": "0.1.11", "passport-google-oauth20": "1.0.0", "passport-ldapauth": "2.0.0", "passport-local": "1.0.0", + "passport-oauth2": "1.4.0", "passport-slack": "0.0.7", "passport-twitch": "1.0.3", "passport-windowslive": "1.0.2", diff --git a/server/controllers/admin.js b/server/controllers/admin.js deleted file mode 100644 index 962c6161..00000000 --- a/server/controllers/admin.js +++ /dev/null @@ -1,307 +0,0 @@ -'use strict' - -/* global wiki */ - -var express = require('express') -var router = express.Router() -const Promise = require('bluebird') -const validator = require('validator') -const _ = require('lodash') -const axios = require('axios') -const path = require('path') -const fs = Promise.promisifyAll(require('fs-extra')) -const os = require('os') -const filesize = require('filesize.js') - -/** - * Admin - */ -router.get('/', (req, res) => { - res.redirect('/admin/profile') -}) - -router.get('/profile', (req, res) => { - if (res.locals.isGuest) { - return res.render('error-forbidden') - } - - res.render('pages/admin/profile', { adminTab: 'profile' }) -}) - -router.post('/profile', (req, res) => { - if (res.locals.isGuest) { - return res.render('error-forbidden') - } - - 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 wiki.db.User.hashPassword(nPwd).then((pwd) => { - usr.password = pwd - return usr.save() - }) - } - } else { - return usr.save() - } - }).then(() => { - return res.json({ msg: 'OK' }) - }).catch((err) => { - res.status(400).json({ msg: err.message }) - }) -}) - -router.get('/stats', (req, res) => { - if (res.locals.isGuest) { - return res.render('error-forbidden') - } - - Promise.all([ - 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' - }) || true - }).catch((err) => { - throw err - }) -}) - -router.get('/users', (req, res) => { - if (!res.locals.rights.manage) { - return res.render('error-forbidden') - } - - wiki.db.User.find({}) - .select('-password -rights') - .sort('name email') - .exec().then((usrs) => { - res.render('pages/admin/users', { adminTab: 'users', usrs }) - }) -}) - -router.get('/users/:id', (req, res) => { - if (!res.locals.rights.manage) { - return res.render('error-forbidden') - } - - if (!validator.isMongoId(req.params.id)) { - return res.render('error-forbidden') - } - - wiki.db.User.findById(req.params.id) - .select('-password -providerId') - .exec().then((usr) => { - let usrOpts = { - canChangeEmail: (usr.email !== 'guest' && usr.provider === 'local' && usr.email !== req.app.locals.appconfig.admin), - canChangeName: (usr.email !== 'guest'), - canChangePassword: (usr.email !== 'guest' && usr.provider === 'local'), - canChangeRole: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin)), - canBeDeleted: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin)) - } - - res.render('pages/admin/users-edit', { adminTab: 'users', usr, usrOpts }) - }).catch(err => { // eslint-disable-line handle-callback-err - return res.status(404).end() || true - }) -}) - -/** - * Create / Authorize a new user - */ -router.post('/users/create', (req, res) => { - if (!res.locals.rights.manage) { - return res.status(401).json({ msg: 'Unauthorized' }) - } - - let nUsr = { - email: _.toLower(_.trim(req.body.email)), - provider: _.trim(req.body.provider), - password: req.body.password, - name: _.trim(req.body.name) - } - - if (!validator.isEmail(nUsr.email)) { - return res.status(400).json({ msg: 'Invalid email address' }) - } else if (!validator.isIn(nUsr.provider, ['local', 'google', 'windowslive', 'facebook', 'github', 'slack'])) { - return res.status(400).json({ msg: 'Invalid provider' }) - } else if (nUsr.provider === 'local' && !validator.isLength(nUsr.password, { min: 6 })) { - return res.status(400).json({ msg: 'Password too short or missing' }) - } else if (nUsr.provider === 'local' && !validator.isLength(nUsr.name, { min: 2 })) { - return res.status(400).json({ msg: 'Name is missing' }) - } - - 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') ? wiki.db.User.hashPassword(nUsr.password) : Promise.resolve(true) - return pwdGen.then(nPwd => { - if (nUsr.provider !== 'local') { - nUsr.password = '' - nUsr.name = '-- pending --' - } else { - nUsr.password = nPwd - } - - nUsr.rights = [{ - role: 'read', - path: '/', - exact: false, - deny: false - }] - - return wiki.db.User.create(nUsr).then(() => { - return res.json({ ok: true }) - }) - }).catch(err => { - wiki.logger.warn(err) - return res.status(500).json({ msg: err }) - }) - }).catch(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: wiki.lang.t('errors:unauthorized') }) - } - - if (!validator.isMongoId(req.params.id)) { - return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') }) - } - - 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(wiki.lang.t('errors:newpasswordtooshort'))) - } else { - return wiki.db.User.hashPassword(nPwd).then((pwd) => { - usr.password = pwd - return usr.save() - }) - } - } else { - return usr.save() - } - }).then((usr) => { - // Update guest rights for future requests - if (usr.provider === 'local' && usr.email === 'guest') { - wiki.rights.guest = usr - } - return usr - }).then(() => { - return res.json({ msg: 'OK' }) - }).catch((err) => { - res.status(400).json({ msg: err.message }) - }) -}) - -/** - * Delete / Deauthorize a user - */ -router.delete('/users/:id', (req, res) => { - if (!res.locals.rights.manage) { - return res.status(401).json({ msg: wiki.lang.t('errors:unauthorized') }) - } - - if (!validator.isMongoId(req.params.id)) { - return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') }) - } - - 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 }) - }) -}) - -router.get('/settings', (req, res) => { - if (!res.locals.rights.manage) { - return res.render('error-forbidden') - } - res.render('pages/admin/settings', { adminTab: 'settings' }) -}) - -router.get('/system', (req, res) => { - if (!res.locals.rights.manage) { - return res.render('error-forbidden') - } - - let hostInfo = { - cpus: os.cpus(), - hostname: os.hostname(), - nodeversion: process.version, - os: `${os.type()} (${os.platform()}) ${os.release()} ${os.arch()}`, - totalmem: filesize(os.totalmem()), - cwd: process.cwd() - } - - 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, - latest: resp.data.tag_name, - latestPublishedAt: resp.data.published_at - } - - res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion }) - }).catch(err => { - wiki.logger.warn(err) - res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion: { current: 'v' + packageObj.version } }) - }) - }) -}) - -router.post('/system/install', (req, res) => { - if (!res.locals.rights.manage) { - return res.render('error-forbidden') - } - - // let sysLib = require(path.join(ROOTPATH, 'libs/system.js')) - // sysLib.install('v1.0-beta.7') - res.status(400).send('Sorry, Upgrade/Re-Install via the web UI is not yet ready. You must use the npm upgrade method in the meantime.').end() -}) - -router.get('/theme', (req, res) => { - if (!res.locals.rights.manage) { - return res.render('error-forbidden') - } - res.render('pages/admin/theme', { adminTab: 'theme' }) -}) - -router.post('/theme', (req, res) => { - if (res.locals.isGuest) { - return res.render('error-forbidden') - } - - if (!validator.isIn(req.body.primary, wiki.data.colors)) { - return res.status(406).json({ msg: 'Primary color is invalid.' }) - } else if (!validator.isIn(req.body.alt, wiki.data.colors)) { - return res.status(406).json({ msg: 'Alternate color is invalid.' }) - } else if (!validator.isIn(req.body.footer, wiki.data.colors)) { - return res.status(406).json({ msg: 'Footer color is invalid.' }) - } - - wiki.config.theme.primary = req.body.primary - wiki.config.theme.alt = req.body.alt - wiki.config.theme.footer = req.body.footer - wiki.config.theme.code.dark = req.body.codedark === 'true' - wiki.config.theme.code.colorize = req.body.codecolorize === 'true' - - return res.json({ msg: 'OK' }) -}) - -module.exports = router diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 9188102a..92e62b67 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -34,10 +34,7 @@ const bruteforce = new ExpressBrute(EBstore, { * Login form */ router.get('/login', function (req, res, next) { - res.render('auth/login', { - authStrategies: _.reject(wiki.auth.strategies, { key: 'local' }), - hasMultipleStrategies: Object.keys(wiki.config.auth.strategies).length > 1 - }) + res.render('auth/login') }) router.post('/login', bruteforce.prevent, function (req, res, next) { diff --git a/server/controllers/pages.js b/server/controllers/common.js similarity index 100% rename from server/controllers/pages.js rename to server/controllers/common.js diff --git a/server/controllers/uploads.js b/server/controllers/uploads.js deleted file mode 100644 index 36f63177..00000000 --- a/server/controllers/uploads.js +++ /dev/null @@ -1,162 +0,0 @@ -'use strict' - -/* global wiki */ - -const express = require('express') -const router = express.Router() - -const readChunk = require('read-chunk') -const fileType = require('file-type') -const Promise = require('bluebird') -const fs = Promise.promisifyAll(require('fs-extra')) -const path = require('path') -const _ = require('lodash') - -const validPathRe = new RegExp('^([a-z0-9/-' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']+\\.[a-z0-9]+)$') -const validPathThumbsRe = new RegExp('^([a-z0-9]+\\.png)$') - -// ========================================== -// SERVE UPLOADS FILES -// ========================================== - -router.get('/t/*', (req, res, next) => { - let fileName = req.params[0] - if (!validPathThumbsRe.test(fileName)) { - return res.sendStatus(404).end() - } - - // todo: Authentication-based access - - res.sendFile(fileName, { - root: wiki.disk.getThumbsPath(), - dotfiles: 'deny' - }, (err) => { - if (err) { - res.status(err.status).end() - } - }) -}) - -// router.post('/img', wiki.disk.uploadImgHandler, (req, res, next) => { -// let destFolder = _.chain(req.body.folder).trim().toLower().value() - -// wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => { -// if (!destFolderPath) { -// res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') }) -// return true -// } - -// Promise.map(req.files, (f) => { -// let destFilename = '' -// let destFilePath = '' - -// return wiki.disk.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => { -// destFilename = fname -// destFilePath = path.resolve(destFolderPath, destFilename) - -// return readChunk(f.path, 0, 262) -// }).then((buf) => { -// // -> Check MIME type by magic number - -// let mimeInfo = fileType(buf) -// if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) { -// return Promise.reject(new Error(wiki.lang.t('errors:invalidfiletype'))) -// } -// return true -// }).then(() => { -// // -> Move file to final destination - -// return fs.moveAsync(f.path, destFilePath, { clobber: false }) -// }).then(() => { -// return { -// ok: true, -// filename: destFilename, -// filesize: f.size -// } -// }).reflect() -// }, {concurrency: 3}).then((results) => { -// let uplResults = _.map(results, (r) => { -// if (r.isFulfilled()) { -// return r.value() -// } else { -// return { -// ok: false, -// msg: r.reason().message -// } -// } -// }) -// res.json({ ok: true, results: uplResults }) -// return true -// }).catch((err) => { -// res.json({ ok: false, msg: err.message }) -// return true -// }) -// }) -// }) - -// router.post('/file', wiki.disk.uploadFileHandler, (req, res, next) => { -// let destFolder = _.chain(req.body.folder).trim().toLower().value() - -// wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => { -// if (!destFolderPath) { -// res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') }) -// return true -// } - -// Promise.map(req.files, (f) => { -// let destFilename = '' -// let destFilePath = '' - -// return wiki.disk.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => { -// destFilename = fname -// destFilePath = path.resolve(destFolderPath, destFilename) - -// // -> Move file to final destination - -// return fs.moveAsync(f.path, destFilePath, { clobber: false }) -// }).then(() => { -// return { -// ok: true, -// filename: destFilename, -// filesize: f.size -// } -// }).reflect() -// }, {concurrency: 3}).then((results) => { -// let uplResults = _.map(results, (r) => { -// if (r.isFulfilled()) { -// return r.value() -// } else { -// return { -// ok: false, -// msg: r.reason().message -// } -// } -// }) -// res.json({ ok: true, results: uplResults }) -// return true -// }).catch((err) => { -// res.json({ ok: false, msg: err.message }) -// return true -// }) -// }) -// }) - -// router.get('/*', (req, res, next) => { -// let fileName = req.params[0] -// if (!validPathRe.test(fileName)) { -// return res.sendStatus(404).end() -// } - -// // todo: Authentication-based access - -// res.sendFile(fileName, { -// root: wiki.git.getRepoPath() + '/uploads/', -// dotfiles: 'deny' -// }, (err) => { -// if (err) { -// res.status(err.status).end() -// } -// }) -// }) - -module.exports = router diff --git a/server/controllers/ws.js b/server/controllers/ws.js deleted file mode 100644 index 95065ee0..00000000 --- a/server/controllers/ws.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict' - -/* global appconfig, entries, rights, search, upl */ -/* eslint-disable standard/no-callback-literal */ - -const _ = require('lodash') - -module.exports = (socket) => { - // Check if Guest - if (!socket.request.user.logged_in) { - socket.request.user = _.assign(rights.guest, socket.request.user) - } - - // ----------------------------------------- - // SEARCH - // ----------------------------------------- - - if (appconfig.public || socket.request.user.logged_in) { - socket.on('search', (data, cb) => { - cb = cb || _.noop - search.find(data.terms).then((results) => { - return cb(results) || true - }) - }) - } - - // ----------------------------------------- - // TREE VIEW (LIST ALL PAGES) - // ----------------------------------------- - - if (appconfig.public || socket.request.user.logged_in) { - socket.on('treeFetch', (data, cb) => { - cb = cb || _.noop - entries.getFromTree(data.basePath, socket.request.user).then((f) => { - return cb(f) || true - }) - }) - } - - // ----------------------------------------- - // UPLOADS - // ----------------------------------------- - - if (socket.request.user.logged_in) { - socket.on('uploadsGetFolders', (data, cb) => { - cb = cb || _.noop - upl.getUploadsFolders().then((f) => { - return cb(f) || true - }) - }) - - socket.on('uploadsCreateFolder', (data, cb) => { - cb = cb || _.noop - upl.createUploadsFolder(data.foldername).then((f) => { - return cb(f) || true - }) - }) - - socket.on('uploadsGetImages', (data, cb) => { - cb = cb || _.noop - upl.getUploadsFiles('image', data.folder).then((f) => { - return cb(f) || true - }) - }) - - socket.on('uploadsGetFiles', (data, cb) => { - cb = cb || _.noop - upl.getUploadsFiles('binary', data.folder).then((f) => { - return cb(f) || true - }) - }) - - socket.on('uploadsDeleteFile', (data, cb) => { - cb = cb || _.noop - upl.deleteUploadsFile(data.uid).then((f) => { - return cb(f) || true - }) - }) - - socket.on('uploadsFetchFileFromURL', (data, cb) => { - cb = cb || _.noop - upl.downloadFromUrl(data.folder, data.fetchUrl).then((f) => { - return cb({ ok: true }) || true - }).catch((err) => { - return cb({ - ok: false, - msg: err.message - }) || true - }) - }) - - socket.on('uploadsRenameFile', (data, cb) => { - cb = cb || _.noop - upl.moveUploadsFile(data.uid, data.folder, data.filename).then((f) => { - return cb({ ok: true }) || true - }).catch((err) => { - return cb({ - ok: false, - msg: err.message - }) || true - }) - }) - - socket.on('uploadsMoveFile', (data, cb) => { - cb = cb || _.noop - upl.moveUploadsFile(data.uid, data.folder).then((f) => { - return cb({ ok: true }) || true - }).catch((err) => { - return cb({ - ok: false, - msg: err.message - }) || true - }) - }) - } -} diff --git a/server/extensions/authentication/dropbox.js b/server/extensions/authentication/dropbox.js new file mode 100644 index 00000000..5367e892 --- /dev/null +++ b/server/extensions/authentication/dropbox.js @@ -0,0 +1,30 @@ +/* global wiki */ + +// ------------------------------------ +// Dropbox Account +// ------------------------------------ + +const DropboxStrategy = require('passport-dropbox-oauth2').Strategy + +module.exports = { + key: 'dropbox', + title: 'Dropbox', + useForm: false, + props: ['clientId', 'clientSecret'], + init (passport, conf) { + passport.use('dropbox', + new DropboxStrategy({ + apiVersion: '2', + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL + }, (accessToken, refreshToken, profile, cb) => { + wiki.db.User.processProfile(profile).then((user) => { + return cb(null, user) || true + }).catch((err) => { + return cb(err, null) || true + }) + }) + ) + } +} diff --git a/server/extensions/authentication/oauth2.js b/server/extensions/authentication/oauth2.js new file mode 100644 index 00000000..f2e9f12e --- /dev/null +++ b/server/extensions/authentication/oauth2.js @@ -0,0 +1,31 @@ +/* global wiki */ + +// ------------------------------------ +// OAuth2 Account +// ------------------------------------ + +const OAuth2Strategy = require('passport-oauth2').Strategy + +module.exports = { + key: 'oauth2', + title: 'OAuth2', + useForm: false, + props: ['clientId', 'clientSecret', 'authorizationURL', 'tokenURL'], + init (passport, conf) { + passport.use('oauth2', + new OAuth2Strategy({ + authorizationURL: conf.authorizationURL, + tokenURL: conf.tokenURL, + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL + }, (accessToken, refreshToken, profile, cb) => { + wiki.db.User.processProfile(profile).then((user) => { + return cb(null, user) || true + }).catch((err) => { + return cb(err, null) || true + }) + }) + ) + } +} diff --git a/server/extensions/renderer/common/mathjax.js b/server/extensions/renderer/common/mathjax.js new file mode 100644 index 00000000..ffa8cf2b --- /dev/null +++ b/server/extensions/renderer/common/mathjax.js @@ -0,0 +1,86 @@ +const mathjax = require('mathjax-node') +const _ = require('lodash') + +// ------------------------------------ +// Mathjax +// ------------------------------------ + +/* global wiki */ + +const mathRegex = [ + { + format: 'TeX', + regex: /\\\[([\s\S]*?)\\\]/g + }, + { + format: 'inline-TeX', + regex: /\\\((.*?)\\\)/g + }, + { + format: 'MathML', + regex: //g + } +] + +module.exports = { + key: 'common/mathjax', + title: 'Mathjax', + dependsOn: [], + props: [], + init (conf) { + mathjax.config({ + MathJax: { + jax: ['input/TeX', 'input/MathML', 'output/SVG'], + extensions: ['tex2jax.js', 'mml2jax.js'], + TeX: { + extensions: ['AMSmath.js', 'AMSsymbols.js', 'noErrors.js', 'noUndefined.js'] + }, + SVG: { + scale: 120, + font: 'STIX-Web' + } + } + }) + }, + async render (content) { + let matchStack = [] + let replaceStack = [] + let currentMatch + let mathjaxState = {} + + _.forEach(mathRegex, mode => { + do { + currentMatch = mode.regex.exec(content) + if (currentMatch) { + matchStack.push(currentMatch[0]) + replaceStack.push( + new Promise((resolve, reject) => { + mathjax.typeset({ + math: (mode.format === 'MathML') ? currentMatch[0] : currentMatch[1], + format: mode.format, + speakText: false, + svg: true, + state: mathjaxState, + timeout: 30 * 1000 + }, result => { + if (!result.errors) { + resolve(result.svg) + } else { + resolve(currentMatch[0]) + wiki.logger.warn(result.errors.join(', ')) + } + }) + }) + ) + } + } while (currentMatch) + }) + + return (matchStack.length > 0) ? Promise.all(replaceStack).then(results => { + _.forEach(matchStack, (repMatch, idx) => { + content = content.replace(repMatch, results[idx]) + }) + return content + }) : Promise.resolve(content) + } +} diff --git a/server/extensions/renderer/markdown/abbreviations.js b/server/extensions/renderer/markdown/abbreviations.js new file mode 100644 index 00000000..17bc71e8 --- /dev/null +++ b/server/extensions/renderer/markdown/abbreviations.js @@ -0,0 +1,15 @@ +const mdAbbr = require('markdown-it-abbr') + +// ------------------------------------ +// Markdown - Abbreviations +// ------------------------------------ + +module.exports = { + key: 'markdown/abbreviations', + title: 'Abbreviations', + dependsOn: [], + props: [], + init (md, conf) { + md.use(mdAbbr) + } +} diff --git a/server/extensions/renderer/markdown/emoji.js b/server/extensions/renderer/markdown/emoji.js new file mode 100644 index 00000000..b954e4ec --- /dev/null +++ b/server/extensions/renderer/markdown/emoji.js @@ -0,0 +1,15 @@ +const mdEmoji = require('markdown-it-emoji') + +// ------------------------------------ +// Markdown - Emoji +// ------------------------------------ + +module.exports = { + key: 'markdown/emoji', + title: 'Emoji', + dependsOn: [], + props: [], + init (md, conf) { + md.use(mdEmoji) + } +} diff --git a/server/extensions/renderer/markdown/expand-tabs.js b/server/extensions/renderer/markdown/expand-tabs.js new file mode 100644 index 00000000..deed8168 --- /dev/null +++ b/server/extensions/renderer/markdown/expand-tabs.js @@ -0,0 +1,18 @@ +const mdExpandTabs = require('markdown-it-expand-tabs') +const _ = require('lodash') + +// ------------------------------------ +// Markdown - Expand Tabs +// ------------------------------------ + +module.exports = { + key: 'markdown/expand-tabs', + title: 'Expand Tabs', + dependsOn: [], + props: ['tabWidth'], + init (md, conf) { + md.use(mdExpandTabs, { + tabWidth: _.toInteger(conf.tabWidth || 4) + }) + } +} diff --git a/server/extensions/renderer/markdown/footnotes.js b/server/extensions/renderer/markdown/footnotes.js new file mode 100644 index 00000000..06274355 --- /dev/null +++ b/server/extensions/renderer/markdown/footnotes.js @@ -0,0 +1,15 @@ +const mdFootnote = require('markdown-it-footnote') + +// ------------------------------------ +// Markdown - Footnotes +// ------------------------------------ + +module.exports = { + key: 'markdown/footnotes', + title: 'Footnotes', + dependsOn: [], + props: [], + init (md, conf) { + md.use(mdFootnote) + } +} diff --git a/server/extensions/renderer/markdown/mathjax.js b/server/extensions/renderer/markdown/mathjax.js new file mode 100644 index 00000000..5c74879b --- /dev/null +++ b/server/extensions/renderer/markdown/mathjax.js @@ -0,0 +1,15 @@ +const mdMathjax = require('markdown-it-mathjax')() + +// ------------------------------------ +// Markdown - Mathjax Preprocessor +// ------------------------------------ + +module.exports = { + key: 'markdown/mathjax', + title: 'Mathjax Preprocessor', + dependsOn: [], + props: [], + init (md, conf) { + md.use(mdMathjax) + } +} diff --git a/server/extensions/renderer/markdown/tasks-lists.js b/server/extensions/renderer/markdown/tasks-lists.js new file mode 100644 index 00000000..9c5a2318 --- /dev/null +++ b/server/extensions/renderer/markdown/tasks-lists.js @@ -0,0 +1,15 @@ +const mdTaskLists = require('markdown-it-task-lists') + +// ------------------------------------ +// Markdown - Task Lists +// ------------------------------------ + +module.exports = { + key: 'markdown/task-lists', + title: 'Task Lists', + dependsOn: [], + props: [], + init (md, conf) { + md.use(mdTaskLists) + } +} diff --git a/server/master.js b/server/master.js index c2ac6308..6fe2a928 100644 --- a/server/master.js +++ b/server/master.js @@ -29,7 +29,6 @@ module.exports = async () => { const path = require('path') const session = require('express-session') const SessionRedisStore = require('connect-redis')(session) - // const graceful = require('node-graceful') const graphqlApollo = require('apollo-server-express') const graphqlSchema = require('./modules/graphql') @@ -106,7 +105,6 @@ module.exports = async () => { app.locals.moment = require('moment') app.locals.moment.locale(wiki.config.site.lang) app.locals.config = wiki.config - app.use(mw.flash) // ---------------------------------------- // Controllers @@ -126,21 +124,20 @@ module.exports = async () => { })(req, res, next) }) app.use('/graphiql', graphqlApollo.graphiqlExpress({ endpointURL: '/graphql' })) - // app.use('/uploads', mw.auth, ctrl.uploads) - app.use('/admin', mw.auth, ctrl.admin) - app.use('/', mw.auth, ctrl.pages) + + app.use('/', mw.auth, ctrl.common) // ---------------------------------------- // Error handling // ---------------------------------------- - app.use(function (req, res, next) { + app.use((req, res, next) => { var err = new Error('Not Found') err.status = 404 next(err) }) - app.use(function (err, req, res, next) { + app.use((err, req, res, next) => { res.status(err.status || 500) res.render('error', { message: err.message, @@ -180,17 +177,5 @@ module.exports = async () => { wiki.logger.info('HTTP Server: [ RUNNING ]') }) - // ---------------------------------------- - // Graceful shutdown - // ---------------------------------------- - - // graceful.on('exit', () => { - // wiki.logger.info('- SHUTTING DOWN - Performing git sync...') - // return global.git.resync().then(() => { - // wiki.logger.info('- SHUTTING DOWN - Git sync successful. Now safe to exit.') - // process.exit() - // }) - // }) - return true } diff --git a/server/setup.js b/server/setup.js index 1ced310f..5f270fcf 100644 --- a/server/setup.js +++ b/server/setup.js @@ -247,6 +247,9 @@ module.exports = () => { confRaw = yaml.safeDump(conf) await fs.writeFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), confRaw) + _.set(wiki.config, 'port', req.body.port) + _.set(wiki.config, 'paths.repo', req.body.pathRepo) + // Populate config namespaces wiki.config.auth = wiki.config.auth || {} wiki.config.features = wiki.config.features || {} @@ -313,29 +316,24 @@ module.exports = () => { }) wiki.logger.info('Setup is complete!') - res.json({ ok: true }) + res.json({ + ok: true, + redirectPath: wiki.config.site.path, + redirectPort: wiki.config.port + }).end() + + wiki.logger.info('Stopping Setup...') + server.destroy(() => { + wiki.logger.info('Setup stopped. Starting Wiki.js...') + _.delay(() => { + wiki.kernel.bootMaster() + }, 1000) + }) } catch (err) { res.json({ ok: false, error: err.message }) } }) - /** - * Restart in normal mode - */ - app.post('/restart', (req, res) => { - res.status(204).end() - /* server.destroy(() => { - spinner.text = 'Setup wizard terminated. Restarting in normal mode...' - _.delay(() => { - const exec = require('execa') - exec.stdout('node', ['wiki', 'start']).then(result => { - spinner.succeed('Wiki.js is now running in normal mode!') - process.exit(0) - }) - }, 1000) - }) */ - }) - // ---------------------------------------- // Error handling // ---------------------------------------- diff --git a/server/views/master.pug b/server/views/master.pug index 8e8e38a3..bdf7f422 100644 --- a/server/views/master.pug +++ b/server/views/master.pug @@ -4,8 +4,8 @@ html meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(charset='UTF-8') meta(name='viewport', content='width=device-width, initial-scale=1') - meta(name='theme-color', content='#009688') - meta(name='msapplication-TileColor', content='#009688') + meta(name='theme-color', content='#0288d1') + meta(name='msapplication-TileColor', content='#0288d1') meta(name='msapplication-TileImage', content=config.site.path + 'favicons/ms-icon-144x144.png') title= config.site.title diff --git a/server/views/setup.pug b/server/views/setup.pug index e397dce5..be342a19 100644 --- a/server/views/setup.pug +++ b/server/views/setup.pug @@ -339,31 +339,15 @@ block body h4 Finalizing your installation... .is-logo(v-if='!loading && final.ok') svg.icons.is-64: use(xlink:href='#nc-check-bold') - h4 All done! + h4 Installation complete! p(v-if='!loading && final.ok'): strong Wiki.js was configured successfully and is now ready for use. - p(v-if='!loading && final.ok') - | Click the Start button below to launch your newly configured wiki. + p(v-if='!loading && final.ok') Click the #[strong Start] button below to access your newly configured wiki. p(v-if='!loading && !final.ok') #[svg.icons.is-18.is-text: use(xlink:href='#nc-square-remove')] Error: {{ final.error }} .panel-footer .progress-bar: div(v-bind:style='{width: currentProgress}') - button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToAdmin', v-bind:disabled='loading') Back + button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToAdmin', v-if='!loading && !final.ok', v-bind:disabled='loading') Back button.button.is-small.is-teal(v-on:click='proceedToFinal', v-if='!loading && !final.ok') Try Again button.button.is-small.is-green(v-on:click='finish', v-if='loading || final.ok', v-bind:disabled='loading') Start - - //- ============================================== - //- RESTART - //- ============================================== - - template(v-else-if='state === "restart"') - .panel - h2.panel-title.is-featured - span Restarting... - i - .panel-content.is-text - p #[i.icon-loader.animated.rotateIn.infinite] Restarting Wiki.js in normal mode... - p You'll automatically be redirected to the homepage when ready. This usually takes about 30 seconds. - .panel-footer - button.button.is-small.is-green(disabled='disabled') Start footer small Wiki.js Installation Wizard diff --git a/yarn.lock b/yarn.lock index 9cc2de1b..67a78c4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6184,6 +6184,13 @@ passport-discord@0.1.3: dependencies: passport-oauth2 "^1.2.0" +passport-dropbox-oauth2@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/passport-dropbox-oauth2/-/passport-dropbox-oauth2-1.1.0.tgz#77c737636e4841944dfb82dfc42c3d8ab782c10e" + dependencies: + passport-oauth "^1.0.0" + pkginfo "^0.2.3" + passport-facebook@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/passport-facebook/-/passport-facebook-2.1.1.tgz#c39d0b52ae4d59163245a4e21a7b9b6321303311" @@ -6225,7 +6232,7 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" -passport-oauth2@1.x.x, passport-oauth2@^1.1.2, passport-oauth2@^1.2.0: +passport-oauth2@1.4.0, passport-oauth2@1.x.x, passport-oauth2@^1.1.2, passport-oauth2@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad" dependencies: @@ -6472,7 +6479,7 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" -pkginfo@0.2.x: +pkginfo@0.2.x, pkginfo@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.2.3.tgz#7239c42a5ef6c30b8f328439d9b9ff71042490f8"