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: /