diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..06dfb47f --- /dev/null +++ b/.babelrc @@ -0,0 +1,20 @@ +{ + "comments": false, + "presets": [ + ["env", { + "targets": { + "browsers": [ + "last 6 Chrome major versions", + "last 6 Firefox major versions", + "last 4 Safari major versions", + "last 4 Edge major versions", + "last 3 iOS major versions", + "last 3 Android major versions", + "last 2 ChromeAndroid major versions", + "Explorer 11" + ] + } + }], + "stage-2" + ] +} diff --git a/.eslintrc.json b/.eslintrc.json index 283168f4..8bf80646 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,27 +1,14 @@ { - "extends": "standard", + "extends": "requarks", "env": { "node": true, "es6": true, "jest": true }, "globals": { - // Client "document": false, "navigator": false, "window": false, - "siteLang": false, - "socket": true, - "wikijs": true, - "FuseBox": false, - // Server - "appconfig": true, - "appdata": true, - "ROOTPATH": true, - "SERVERPATH": true, - "IS_DEBUG": true - }, - "rules": { - "space-before-function-paren": 0 + "FuseBox": false } } diff --git a/.npmrc b/.npmrc index 22b81d18..6d1bedf0 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -save-prefix = "~" +save-exact = true +save-prefix = "" diff --git a/.yarnrc b/.yarnrc index 95888f66..d60dfee7 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1 +1,2 @@ -save-prefix "~" +save-exact true +save-prefix "" diff --git a/CHANGELOG.md b/CHANGELOG.md index 63458e51..1d7cde22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -237,11 +237,6 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Updated dependencies + snyk policy -[v1.0.11]: https://github.com/Requarks/wiki/releases/tag/v1.0.11 -[v1.0.10]: https://github.com/Requarks/wiki/releases/tag/v1.0.10 -[v1.0.9]: https://github.com/Requarks/wiki/releases/tag/v1.0.9 -[v1.0.8]: https://github.com/Requarks/wiki/releases/tag/v1.0.8 -[v1.0.7]: https://github.com/Requarks/wiki/releases/tag/v1.0.7 [v1.0.6]: https://github.com/Requarks/wiki/releases/tag/v1.0.6 [v1.0.5]: https://github.com/Requarks/wiki/releases/tag/v1.0.5 [v1.0.4]: https://github.com/Requarks/wiki/releases/tag/v1.0.4 diff --git a/assets/images/bg.jpg b/assets/images/bg.jpg new file mode 100644 index 00000000..d601fcfe Binary files /dev/null and b/assets/images/bg.jpg differ diff --git a/assets/images/bg_1.jpg b/assets/images/bg_1.jpg deleted file mode 100644 index 1b85b7f0..00000000 Binary files a/assets/images/bg_1.jpg and /dev/null differ diff --git a/assets/images/bg_2.jpg b/assets/images/bg_2.jpg deleted file mode 100644 index df2c53be..00000000 Binary files a/assets/images/bg_2.jpg and /dev/null differ diff --git a/assets/images/bg_3.jpg b/assets/images/bg_3.jpg deleted file mode 100644 index 04be9fe7..00000000 Binary files a/assets/images/bg_3.jpg and /dev/null differ diff --git a/assets/svg/auth-icon-azure.svg b/assets/svg/auth-icon-azure.svg new file mode 100644 index 00000000..13f1c52e --- /dev/null +++ b/assets/svg/auth-icon-azure.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/svg/auth-icon-facebook.svg b/assets/svg/auth-icon-facebook.svg new file mode 100644 index 00000000..fa706aa2 --- /dev/null +++ b/assets/svg/auth-icon-facebook.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/assets/svg/auth-icon-github.svg b/assets/svg/auth-icon-github.svg new file mode 100644 index 00000000..18e9450e --- /dev/null +++ b/assets/svg/auth-icon-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/auth-icon-google.svg b/assets/svg/auth-icon-google.svg new file mode 100644 index 00000000..06dc52f0 --- /dev/null +++ b/assets/svg/auth-icon-google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/auth-icon-ldap.svg b/assets/svg/auth-icon-ldap.svg new file mode 100644 index 00000000..679fc237 --- /dev/null +++ b/assets/svg/auth-icon-ldap.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/assets/svg/auth-icon-local.svg b/assets/svg/auth-icon-local.svg new file mode 100644 index 00000000..da2fe997 --- /dev/null +++ b/assets/svg/auth-icon-local.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/assets/svg/auth-icon-microsoft.svg b/assets/svg/auth-icon-microsoft.svg new file mode 100644 index 00000000..0d47e89a --- /dev/null +++ b/assets/svg/auth-icon-microsoft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/auth-icon-slack.svg b/assets/svg/auth-icon-slack.svg new file mode 100644 index 00000000..47232d6d --- /dev/null +++ b/assets/svg/auth-icon-slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svg/login-bg-dark.svg b/assets/svg/login-bg-dark.svg new file mode 100644 index 00000000..8ab32dde --- /dev/null +++ b/assets/svg/login-bg-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/svg/login-bg.svg b/assets/svg/login-bg.svg new file mode 100644 index 00000000..bb1c6ffb --- /dev/null +++ b/assets/svg/login-bg.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/configure.js b/client/configure.js deleted file mode 100644 index 714ff901..00000000 --- a/client/configure.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict' - -require('./scss/configure.scss') -require('./js/configure.js') diff --git a/client/index.js b/client/index.js index 48f04872..7f5c2c19 100644 --- a/client/index.js +++ b/client/index.js @@ -1,17 +1,4 @@ 'use strict' -let logic = document.documentElement.dataset.logic - -switch (logic) { - case 'error': - require('./scss/error.scss') - break - case 'login': - require('./scss/login.scss') - require('./js/login.js') - break - default: - require('./scss/app.scss') - require('./js/app.js') - break -} +require('./scss/app.scss') +require('./js/app.js') diff --git a/client/js/app.js b/client/js/app.js index 9a9c3b1b..67601a6f 100644 --- a/client/js/app.js +++ b/client/js/app.js @@ -1,28 +1,28 @@ 'use strict' -/* global $, siteRoot */ +/* global siteConfig */ /* eslint-disable no-new */ +import CONSTANTS from './constants' + import Vue from 'vue' import VueResource from 'vue-resource' import VueClipboards from 'vue-clipboards' -import VueLodash from 'vue-lodash' +import VeeValidate from 'vee-validate' +import { ApolloClient, createBatchingNetworkInterface } from 'apollo-client' import store from './store' -import io from 'socket-io-client' -import i18next from 'i18next' -import i18nextXHR from 'i18next-xhr-backend' -import VueI18Next from '@panter/vue-i18next' -import 'jquery-contextmenu' -import 'jquery-simple-upload' -import 'jquery-smooth-scroll' -import 'jquery-sticky' + +// ==================================== +// Load Modules +// ==================================== + +import localization from './modules/localization' // ==================================== // Load Helpers // ==================================== import helpers from './helpers' -import _ from './helpers/lodash' // ==================================== // Load Vue Components @@ -36,6 +36,7 @@ import editorFileComponent from './components/editor-file.vue' import editorVideoComponent from './components/editor-video.vue' import historyComponent from './components/history.vue' import loadingSpinnerComponent from './components/loading-spinner.vue' +import loginComponent from './components/login.vue' import modalCreatePageComponent from './components/modal-create-page.vue' import modalCreateUserComponent from './components/modal-create-user.vue' import modalDeletePageComponent from './components/modal-delete-page.vue' @@ -53,19 +54,48 @@ import adminEditUserComponent from './pages/admin-edit-user.component.js' import adminProfileComponent from './pages/admin-profile.component.js' import adminSettingsComponent from './pages/admin-settings.component.js' import adminThemeComponent from './pages/admin-theme.component.js' +import configManagerComponent from './components/config-manager.component.js' import contentViewComponent from './pages/content-view.component.js' import editorComponent from './components/editor.component.js' import sourceViewComponent from './pages/source-view.component.js' +// ==================================== +// Initialize Global Vars +// ==================================== + +window.wiki = null +window.CONSTANTS = CONSTANTS + +// ==================================== +// Initialize Apollo Client (GraphQL) +// ==================================== + +window.graphQL = new ApolloClient({ + networkInterface: createBatchingNetworkInterface({ + uri: window.location.protocol + '//' + window.location.host + siteConfig.path + '/graphql' + }), + connectToDevTools: true +}) + // ==================================== // Initialize Vue Modules // ==================================== Vue.use(VueResource) Vue.use(VueClipboards) -Vue.use(VueI18Next) -Vue.use(VueLodash, _) +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 @@ -78,6 +108,7 @@ Vue.component('adminSettings', adminSettingsComponent) Vue.component('adminTheme', adminThemeComponent) Vue.component('anchor', anchorComponent) Vue.component('colorPicker', colorPickerComponent) +Vue.component('configManager', configManagerComponent) Vue.component('contentView', contentViewComponent) Vue.component('editor', editorComponent) Vue.component('editorCodeblock', editorCodeblockComponent) @@ -85,6 +116,7 @@ Vue.component('editorFile', editorFileComponent) Vue.component('editorVideo', editorVideoComponent) Vue.component('history', historyComponent) Vue.component('loadingSpinner', loadingSpinnerComponent) +Vue.component('login', loginComponent) Vue.component('modalCreatePage', modalCreatePageComponent) Vue.component('modalCreateUser', modalCreateUserComponent) Vue.component('modalDeletePage', modalDeletePageComponent) @@ -99,52 +131,26 @@ Vue.component('sourceView', sourceViewComponent) Vue.component('toggle', toggleComponent) Vue.component('tree', treeComponent) -// ==================================== -// Load Localization strings -// ==================================== - -i18next - .use(i18nextXHR) - .init({ - backend: { - loadPath: siteRoot + '/js/i18n/{{lng}}.json' - }, - lng: siteLang, - fallbackLng: siteLang - }) - -$(() => { +document.addEventListener('DOMContentLoaded', ev => { // ==================================== // Notifications // ==================================== - $(window).bind('beforeunload', () => { + window.addEventListener('beforeunload', () => { store.dispatch('startLoading') }) - $(document).ajaxSend(() => { - store.dispatch('startLoading') - }).ajaxComplete(() => { - store.dispatch('stopLoading') - }) - - // ==================================== - // Establish WebSocket connection - // ==================================== - - let socket = io(window.location.origin) - window.socket = socket // ==================================== // Bootstrap Vue // ==================================== - const i18n = new VueI18Next(i18next) - window.wikijs = new Vue({ + const i18n = localization.init() + window.wiki = new Vue({ mixins: [helpers], components: {}, store, i18n, - el: '#root', + el: '#app', methods: { changeTheme(opts) { this.$el.className = `has-stickynav is-primary-${opts.primary} is-alternate-${opts.alt}` @@ -153,9 +159,7 @@ $(() => { } }, mounted() { - $('a:not(.toc-anchor)').smoothScroll({ speed: 500, offset: -50 }) - $('#header').sticky({ topSpacing: 0 }) - $('.sidebar-pagecontents').sticky({ topSpacing: 15, bottomSpacing: 75 }) + } }) }) diff --git a/client/js/app.old.js b/client/js/app.old.js new file mode 100644 index 00000000..fc69cfc3 --- /dev/null +++ b/client/js/app.old.js @@ -0,0 +1,153 @@ +'use strict' + +/* global $, siteConfig */ +/* eslint-disable no-new */ + +import Vue from 'vue' +import VueResource from 'vue-resource' +import VueClipboards from 'vue-clipboards' +import VueLodash from 'vue-lodash' +import store from './store' +import i18next from 'i18next' +import i18nextXHR from 'i18next-xhr-backend' +import VueI18Next from '@panter/vue-i18next' +import 'jquery-contextmenu' +import 'jquery-simple-upload' +import 'jquery-smooth-scroll' +import 'jquery-sticky' + +// ==================================== +// Load Helpers +// ==================================== + +import helpers from './helpers' +import _ from './helpers/lodash' + +// ==================================== +// Load Vue Components +// ==================================== + +import alertComponent from './components/alert.vue' +import anchorComponent from './components/anchor.vue' +import colorPickerComponent from './components/color-picker.vue' +import editorCodeblockComponent from './components/editor-codeblock.vue' +import editorFileComponent from './components/editor-file.vue' +import editorVideoComponent from './components/editor-video.vue' +import historyComponent from './components/history.vue' +import loadingSpinnerComponent from './components/loading-spinner.vue' +import modalCreatePageComponent from './components/modal-create-page.vue' +import modalCreateUserComponent from './components/modal-create-user.vue' +import modalDeleteUserComponent from './components/modal-delete-user.vue' +import modalDiscardPageComponent from './components/modal-discard-page.vue' +import modalMovePageComponent from './components/modal-move-page.vue' +import modalProfile2faComponent from './components/modal-profile-2fa.vue' +import modalUpgradeSystemComponent from './components/modal-upgrade-system.vue' +import pageLoaderComponent from './components/page-loader.vue' +import searchComponent from './components/search.vue' +import toggleComponent from './components/toggle.vue' +import treeComponent from './components/tree.vue' + +import adminEditUserComponent from './pages/admin-edit-user.component.js' +import adminProfileComponent from './pages/admin-profile.component.js' +import adminSettingsComponent from './pages/admin-settings.component.js' +import adminThemeComponent from './pages/admin-theme.component.js' +import contentViewComponent from './pages/content-view.component.js' +import editorComponent from './components/editor.component.js' +import sourceViewComponent from './pages/source-view.component.js' + +// ==================================== +// Initialize Vue Modules +// ==================================== + +Vue.use(VueResource) +Vue.use(VueClipboards) +Vue.use(VueI18Next) +Vue.use(VueLodash, _) +Vue.use(helpers) + +// ==================================== +// Register Vue Components +// ==================================== + +Vue.component('alert', alertComponent) +Vue.component('adminEditUser', adminEditUserComponent) +Vue.component('adminProfile', adminProfileComponent) +Vue.component('adminSettings', adminSettingsComponent) +Vue.component('adminTheme', adminThemeComponent) +Vue.component('anchor', anchorComponent) +Vue.component('colorPicker', colorPickerComponent) +Vue.component('contentView', contentViewComponent) +Vue.component('editor', editorComponent) +Vue.component('editorCodeblock', editorCodeblockComponent) +Vue.component('editorFile', editorFileComponent) +Vue.component('editorVideo', editorVideoComponent) +Vue.component('history', historyComponent) +Vue.component('loadingSpinner', loadingSpinnerComponent) +Vue.component('modalCreatePage', modalCreatePageComponent) +Vue.component('modalCreateUser', modalCreateUserComponent) +Vue.component('modalDeleteUser', modalDeleteUserComponent) +Vue.component('modalDiscardPage', modalDiscardPageComponent) +Vue.component('modalMovePage', modalMovePageComponent) +Vue.component('modalProfile2fa', modalProfile2faComponent) +Vue.component('modalUpgradeSystem', modalUpgradeSystemComponent) +Vue.component('pageLoader', pageLoaderComponent) +Vue.component('search', searchComponent) +Vue.component('sourceView', sourceViewComponent) +Vue.component('toggle', toggleComponent) +Vue.component('tree', treeComponent) + +// ==================================== +// Load Localization strings +// ==================================== + +i18next + .use(i18nextXHR) + .init({ + backend: { + loadPath: siteConfig.path + '/js/i18n/{{lng}}.json' + }, + lng: siteConfig.lang, + fallbackLng: siteConfig.lang + }) + +$(() => { + // ==================================== + // Notifications + // ==================================== + + $(window).bind('beforeunload', () => { + store.dispatch('startLoading') + }) + $(document).ajaxSend(() => { + store.dispatch('startLoading') + }).ajaxComplete(() => { + store.dispatch('stopLoading') + }) + + // ==================================== + // Bootstrap Vue + // ==================================== + + const i18n = new VueI18Next(i18next) + if (document.querySelector('#root')) { + window.wikijs = new Vue({ + mixins: [helpers], + components: {}, + store, + i18n, + el: '#root', + methods: { + changeTheme(opts) { + this.$el.className = `has-stickynav is-primary-${opts.primary} is-alternate-${opts.alt}` + this.$refs.header.className = `nav is-${opts.primary}` + this.$refs.footer.className = `footer is-${opts.footer}` + } + }, + mounted() { + $('a:not(.toc-anchor)').smoothScroll({ speed: 500, offset: -50 }) + $('#header').sticky({ topSpacing: 0 }) + $('.sidebar-pagecontents').sticky({ topSpacing: 15, bottomSpacing: 75 }) + } + }) + } +}) diff --git a/client/js/components/config-manager.component.js b/client/js/components/config-manager.component.js new file mode 100644 index 00000000..a6cea82c --- /dev/null +++ b/client/js/components/config-manager.component.js @@ -0,0 +1,239 @@ +/* global siteConfig */ + +import axios from 'axios' + +export default { + name: 'configManager', + data() { + return { + loading: false, + state: 'welcome', + syscheck: { + ok: false, + error: '', + results: [] + }, + dbcheck: { + ok: false, + error: '' + }, + gitcheck: { + ok: false, + error: '' + }, + final: { + ok: false, + error: '', + results: [] + }, + conf: { + telemetry: true, + upgrade: false, + title: siteConfig.title || 'Wiki', + host: siteConfig.host || 'http://', + port: siteConfig.port || 80, + lang: siteConfig.lang || 'en', + public: (siteConfig.public === true), + pathData: './data', + pathRepo: './repo', + gitUseRemote: (siteConfig.git !== false), + gitUrl: '', + gitBranch: 'master', + gitAuthType: 'ssh', + gitAuthSSHKey: '', + gitAuthUser: '', + gitAuthPass: '', + gitAuthSSL: true, + gitShowUserEmail: true, + gitServerEmail: '', + adminEmail: '', + adminPassword: '', + adminPasswordConfirm: '' + }, + considerations: { + https: false, + port: false, + localhost: false + } + } + }, + computed: { + currentProgress: function () { + let perc = '0%' + switch (this.state) { + case 'welcome': + perc = '0%' + break + case 'syscheck': + perc = (this.syscheck.ok) ? '15%' : '5%' + break + case 'general': + perc = '25%' + break + case 'considerations': + perc = '30%' + break + case 'git': + perc = '50%' + break + case 'gitcheck': + perc = (this.gitcheck.ok) ? '70%' : '55%' + break + case 'admin': + perc = '75%' + break + } + return perc + } + }, + mounted: function () { + /* if (appconfig.paths) { + this.conf.pathData = appconfig.paths.data || './data' + this.conf.pathRepo = appconfig.paths.repo || './repo' + } + if (appconfig.git !== false && _.isPlainObject(appconfig.git)) { + this.conf.gitUrl = appconfig.git.url || '' + this.conf.gitBranch = appconfig.git.branch || 'master' + this.conf.gitShowUserEmail = (appconfig.git.showUserEmail !== false) + this.conf.gitServerEmail = appconfig.git.serverEmail || '' + if (_.isPlainObject(appconfig.git.auth)) { + this.conf.gitAuthType = appconfig.git.auth.type || 'ssh' + this.conf.gitAuthSSHKey = appconfig.git.auth.privateKey || '' + this.conf.gitAuthUser = appconfig.git.auth.username || '' + this.conf.gitAuthPass = appconfig.git.auth.password || '' + this.conf.gitAuthSSL = (appconfig.git.auth.sslVerify !== false) + } + } */ + }, + methods: { + proceedToWelcome: function (ev) { + this.state = 'welcome' + this.loading = false + }, + proceedToSyscheck: function (ev) { + let self = this + this.state = 'syscheck' + this.loading = true + self.syscheck = { + ok: false, + error: '', + results: [] + } + + this.$helpers._.delay(() => { + axios.post('/syscheck').then(resp => { + if (resp.data.ok === true) { + self.syscheck.ok = true + self.syscheck.results = resp.data.results + } else { + self.syscheck.ok = false + self.syscheck.error = resp.data.error + } + self.loading = false + self.$nextTick() + }).catch(err => { + window.alert(err.message) + }) + }, 1000) + }, + proceedToGeneral: function (ev) { + let self = this + self.state = 'general' + self.loading = false + self.$nextTick(() => { + self.$validator.validateAll('general') + }) + }, + proceedToConsiderations: function (ev) { + this.considerations = { + https: !this.$helpers._.startsWith(this.conf.host, 'https'), + port: false, // TODO + localhost: this.$helpers._.includes(this.conf.host, 'localhost') + } + this.state = 'considerations' + this.loading = false + }, + proceedToGit: function (ev) { + let self = this + self.state = 'git' + self.loading = false + self.$nextTick(() => { + self.$validator.validateAll('git') + }) + }, + proceedToGitCheck: function (ev) { + let self = this + this.state = 'gitcheck' + this.loading = true + self.gitcheck = { + ok: false, + results: [], + error: '' + } + + this.$helpers._.delay(() => { + axios.post('/gitcheck', self.conf).then(resp => { + if (resp.data.ok === true) { + self.gitcheck.ok = true + self.gitcheck.results = resp.data.results + } else { + self.gitcheck.ok = false + self.gitcheck.error = resp.data.error + } + self.loading = false + self.$nextTick() + }).catch(err => { + window.alert(err.message) + }) + }, 1000) + }, + proceedToAdmin: function (ev) { + let self = this + self.state = 'admin' + self.loading = false + self.$nextTick(() => { + self.$validator.validateAll('admin') + }) + }, + proceedToFinal: function (ev) { + let self = this + self.state = 'final' + self.loading = true + self.final = { + ok: false, + error: '', + results: [] + } + + 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 + } else { + self.final.ok = false + self.final.error = resp.data.error + } + self.loading = false + self.$nextTick() + }).catch(err => { + window.alert(err.message) + }) + }, 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) + } + } +} diff --git a/client/js/components/editor-video.vue b/client/js/components/editor-video.vue index 38e9fef8..50e998ed 100644 --- a/client/js/components/editor-video.vue +++ b/client/js/components/editor-video.vue @@ -34,9 +34,9 @@ + diff --git a/client/js/components/page-loader.vue b/client/js/components/page-loader.vue index 8f7071e3..e4171b6d 100644 --- a/client/js/components/page-loader.vue +++ b/client/js/components/page-loader.vue @@ -5,20 +5,19 @@ span {{ msg }} - diff --git a/client/js/constants/graphql.js b/client/js/constants/graphql.js new file mode 100644 index 00000000..e189cce7 --- /dev/null +++ b/client/js/constants/graphql.js @@ -0,0 +1,22 @@ +import gql from 'graphql-tag' + +export default { + GQL_QUERY_AUTHENTICATION: gql` + query($mode: String!) { + authentication(mode:$mode) { + key + useForm + title + icon + } + } + `, + GQL_QUERY_TRANSLATIONS: gql` + query($locale: String!, $namespace: String!) { + translations(locale:$locale, namespace:$namespace) { + key + value + } + } + ` +} diff --git a/client/js/constants/index.js b/client/js/constants/index.js new file mode 100644 index 00000000..56e9acc7 --- /dev/null +++ b/client/js/constants/index.js @@ -0,0 +1,5 @@ +import GRAPHQL from './graphql' + +export default { + GRAPHQL +} diff --git a/client/js/helpers/index.js b/client/js/helpers/index.js index 6f802e8b..6335809b 100644 --- a/client/js/helpers/index.js +++ b/client/js/helpers/index.js @@ -1,6 +1,7 @@ 'use strict' const helpers = { + _: require('./lodash'), common: require('./common'), form: require('./form'), pages: require('./pages') diff --git a/client/js/login.js b/client/js/login.js deleted file mode 100644 index 892e3306..00000000 --- a/client/js/login.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -/* global $ */ - -$(() => { - $('#login-user').focus() -}) diff --git a/client/js/modules/localization.js b/client/js/modules/localization.js new file mode 100644 index 00000000..2ac94cd7 --- /dev/null +++ b/client/js/modules/localization.js @@ -0,0 +1,52 @@ +import i18next from 'i18next' +import i18nextXHR from 'i18next-xhr-backend' +import i18nextCache from 'i18next-localstorage-cache' +import VueI18Next from '@panter/vue-i18next' +import loSet from 'lodash/set' + +/* global siteConfig, graphQL, CONSTANTS */ + +module.exports = { + VueI18Next, + init() { + i18next + .use(i18nextXHR) + .use(i18nextCache) + .init({ + backend: { + loadPath: '{{lng}}/{{ns}}', + parse: (data) => data, + ajax: (url, opts, cb, data) => { + let langParams = url.split('/') + graphQL.query({ + query: CONSTANTS.GRAPHQL.GQL_QUERY_TRANSLATIONS, + variables: { + locale: langParams[0], + namespace: langParams[1] + } + }).then(resp => { + let ns = {} + if (resp.data.translations.length > 0) { + resp.data.translations.forEach(entry => { + loSet(ns, entry.key, entry.value) + }) + } + return cb(ns, {status: '200'}) + }).catch(err => { + console.error(err) + return cb(null, {status: '404'}) + }) + } + }, + cache: { + enabled: true, + expiration: 60 * 60 * 1000 + }, + defaultNS: 'common', + lng: siteConfig.lang, + fallbackLng: siteConfig.lang, + ns: ['common', 'admin', 'auth'] + }) + return new VueI18Next(i18next) + } +} diff --git a/client/scss/app.scss b/client/scss/app.scss index a86c88e8..ab58c08e 100644 --- a/client/scss/app.scss +++ b/client/scss/app.scss @@ -15,6 +15,7 @@ @import 'components/button'; @import 'components/collapsable-nav'; @import 'components/color-picker'; +@import 'components/config-manager'; @import 'components/footer'; @import 'components/form'; @import 'components/grid'; @@ -43,6 +44,7 @@ @import 'layout/_loader'; @import 'layout/_rtl'; -@import 'pages/_welcome'; +@import 'pages/login'; +@import 'pages/welcome'; @import 'base/print'; diff --git a/client/scss/base/base.scss b/client/scss/base/base.scss index 6d8a688e..54740abe 100644 --- a/client/scss/base/base.scss +++ b/client/scss/base/base.scss @@ -11,10 +11,15 @@ [v-cloak], .is-hidden { display: none; } -#root { +#app { padding-bottom: 67px; position: relative; min-height: 100%; + + &.is-fullscreen { + width: 100vw; + height: 100vh; + } } body { diff --git a/client/scss/components/button.scss b/client/scss/components/button.scss index 97338a0e..0f5c9a09 100644 --- a/client/scss/components/button.scss +++ b/client/scss/components/button.scss @@ -4,7 +4,7 @@ .button { border: 1px solid mc('orange','700'); border-radius: 3px; display: inline-flex; - height: 30px; + height: 40px; align-items: center; padding: 0 15px; font-size: 13px; @@ -61,7 +61,11 @@ .button { background-color: mc($color,'800'); color: #FFF; animation: none; - } + } + + &:focus { + box-shadow: inset 0 0 0 3px rgba(255,255,255, .4); + } } } @@ -74,7 +78,13 @@ .button { &.is-featured { animation: btnInvertedPulse .6s ease alternate infinite; - } + } + + &.is-fullwidth { + width: 100%; + text-align: center; + justify-content: center; + } &.is-disabled, &:disabled { background-color: mc('grey', '300'); @@ -87,7 +97,11 @@ .button { background-color: mc('grey', '300') !important; color: mc('grey', '500') !important; } - } + } + + &.is-small { + height: 30px; + } } diff --git a/client/scss/components/config-manager.scss b/client/scss/components/config-manager.scss new file mode 100644 index 00000000..8ee89d91 --- /dev/null +++ b/client/scss/components/config-manager.scss @@ -0,0 +1,61 @@ +.config-manager { + .welcome { + text-align: center; + padding: 50px 0 15px 0; + color: mc('grey', '700'); + + h2 { + margin: 0; + } + + } + + i.icon-loader { + display: inline-block; + color: mc('indigo', '500') + } + i.icon-check { + color: mc('green', '500') + } + i.icon-square-cross { + color: mc('red', '500') + } + i.icon-warning-outline { + color: mc('orange', '500') + } + + .tst-welcome-leave-active, .tst-welcome-enter-active { + transition: all .5s; + overflow-y: hidden; + } + .tst-welcome-leave, .tst-welcome-enter-to { + opacity: 1; + max-height: 200px; + } + .tst-welcome-leave-to, .tst-welcome-enter { + opacity: 0; + max-height: 0; + padding-top: 0; + } + + .progress-bar { + width: 150px; + height: 10px; + background-color: mc('indigo', '50'); + border:1px solid mc('indigo', '100'); + border-radius: 3px; + position: absolute; + left: 15px; + top: 21px; + padding: 1px; + + > div { + width: 5px; + height: 6px; + background-color: mc('indigo', '200'); + border-radius: 2px; + transition: all 1s ease; + } + + } +} diff --git a/client/scss/login.scss b/client/scss/login.scss deleted file mode 100644 index 725ce498..00000000 --- a/client/scss/login.scss +++ /dev/null @@ -1,13 +0,0 @@ -@charset "utf-8"; - -$primary: 'indigo'; - -@import "base/variables"; -@import "base/colors"; -@import "base/reset"; -@import "base/mixins"; -@import "base/fonts"; -@import "base/base"; - -@import "libs/animate"; -@import 'pages/login'; diff --git a/client/scss/pages/_login.scss b/client/scss/pages/_login.scss index abfd9c93..be90d44f 100644 --- a/client/scss/pages/_login.scss +++ b/client/scss/pages/_login.scss @@ -1,306 +1,264 @@ +.login { + background-image: linear-gradient(to right, mc('blue', '400'), mc('blue', '600')); + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; -body { - padding: 0; - margin: 0; - font-family: $core-font-standard; - font-size: 14px; -} - -a { - color: #FFF; - transition: color 0.4s ease; - text-decoration: none; - - &:hover { - color: mc('orange','600'); - text-decoration: underline; - } - -} - -#bg { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1; - background-color: #000; - - > div { - background-size: cover; - background-position: center center; - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - opacity: 0; - visibility: hidden; - transition: opacity 3s ease, visibility 3s; - animation: bg 30s linear infinite; - - &:nth-child(1) { - animation-delay: 10s; - } - - &:nth-child(2) { - animation-delay: 20s; - } - - } - -} - -#root { - position: fixed; - top: 15vh; - left: 10vw; - z-index: 2; - color: #FFF; - display: flex; - flex-direction: column; - - h1 { - font-size: 4rem; - font-weight: bold; - color: #FFF; - padding: 0; - margin: 0; - animation: headerIntro 3s ease; - } - - h2 { - font-size: 1.5rem; - font-weight: normal; - color: rgba(255,255,255,0.7); - padding: 0; - margin: 0 0 25px 0; - animation: headerIntro 3s ease; - } - - h3 { - font-size: 1.25rem; - font-weight: normal; - color: #FB8C00; - padding: 0; - margin: 0; - animation: shake 1s ease; - - > .fa { - margin-right: 7px; - } - - } - - h4 { - font-size: 0.8rem; - font-weight: normal; - color: rgba(255,255,255,0.7); - padding: 0; - margin: 0 0 15px 0; - animation: fadeIn 3s ease; - } - - form { - display: flex; - flex-direction: column; - } - - input[type=text], input[type=password] { - width: 350px; - max-width: 80vw; - border: 1px solid rgba(255,255,255,0.3); - border-radius: 3px; - background-color: rgba(0,0,0,0.2); - padding: 0 15px; - height: 40px; - margin: 0 0 10px 0; - color: #FFF; - font-weight: bold; - font-size: 14px; - transition: all 0.4s ease; - - &:focus { - outline: none; - border-color: mc('orange','600'); - } - - } - - button { - background-color: mc('orange','600'); - color: #FFF; - border: 1px solid lighten(mc('orange','600'), 10%); - border-radius: 3px; - height: 40px; - width: 125px; - padding: 0; - font-weight: bold; - margin: 15px 0 0 0; - transition: all 0.4s ease; - cursor: pointer; - - span { - font-weight: bold; - } - - &:focus { - outline: none; - border-color: #FFF; - } - - &:hover { - background-color: darken(mc('orange','600'), 10%); - } - - } - - #social { - margin-top: 25px; - - > span { - display: block; - font-weight: bold; - color: rgba(255,255,255,0.7); - } - - button { - margin-right: 5px; - width: auto; - padding: 0 15px; - - > i { - margin-right: 10px; - font-size: 16px; - } - - &.ms { - background-color: mc('blue','600'); - border-color: lighten(mc('blue','600'), 10%); - - &:focus { - border-color: #FFF; - } - - &:hover { - background-color: darken(mc('blue','600'), 10%); - } - - } - - &.google { - background-color: mc('light-blue','600'); - border-color: lighten(mc('light-blue','600'), 10%); - - &:focus { - border-color: #FFF; - } - - &:hover { - background-color: darken(mc('light-blue','600'), 10%); - } - - } - - &.facebook { - background-color: mc('indigo','600'); - border-color: lighten(mc('indigo','600'), 10%); - - &:focus { - border-color: #FFF; - } - - &:hover { - background-color: darken(mc('indigo','600'), 10%); - } - - } - - &.github { - background-color: mc('blue-grey','700'); - border-color: lighten(mc('blue-grey','700'), 10%); - - &:focus { - border-color: #FFF; - } - - &:hover { - background-color: darken(mc('blue-grey','700'), 10%); - } - } - - &.slack { - background-color: mc('purple','700'); - border-color: lighten(mc('purple','700'), 10%); - - &:focus { - border-color: #FFF; - } - - &:hover { - background-color: darken(mc('purple','700'), 10%); - } - } - - } - - } - -} - -#copyright { - display: flex; - align-items: center; - justify-content: flex-start; - position: absolute; - left: 10vw; - bottom: 10vh; - z-index: 2; - color: rgba(255,255,255,0.5); - font-weight: bold; - - .icon { - font-size: 1.2rem; - margin: 0 8px; - } - - a { - opacity: 0.75; - } - -} - -@include keyframes(bg) { - 0% { - @include prefix(transform, scale(1,1)); - visibility: visible; - opacity: 0; - }, - 5% { - opacity: 0.5; - }, - 33% { - opacity: 0.5; - }, - 38% { - @include prefix(transform, scale(1.2, 1.2)); - opacity: 0; - }, - 39% { - visibility: hidden; - } - 100% { - visibility: hidden; - opacity: 0; - } -} - -@include keyframes(headerIntro) { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } + &.is-error { + background-image: linear-gradient(to right, mc('red', '400'), mc('red', '600')); + } + + &::before { + content: ''; + position: absolute; + background-image: url('../svg/login-bg.svg'); + background-position: center bottom; + background-size: cover; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + } + + &-container { + position: relative; + display: flex; + width: 400px; + align-items: stretch; + box-shadow: 0 14px 28px rgba(0,0,0,0.2); + border-radius: 6px; + + &.is-expanded { + width: 650px; + + .login-frame { + border-radius: 0 6px 6px 0; + border-left: none; + } + } + + @include until($tablet) { + width: 350px; + + &.is-expanded { + width: 400px; + } + } + } + + &-error { + position: absolute; + bottom: 105%; + width: 100%; + min-height: 50px; + background-image: radial-gradient(ellipse at top left, rgba(255,255,255,.9) 0%,rgba(255,255,255,.8) 100%); + box-shadow: 0 5px 10px rgba(0,0,0,0.2); + border-radius: 6px; + color: mc('red', '800'); + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + + strong { + font-weight: 600; + text-transform: uppercase; + display: block; + padding: 0 1rem 0 0; + border-right: 1px solid mc('red', '200'); + } + span { + padding: 0 0 0 1rem; + display: block; + } + } + + &-providers { + display: flex; + flex-direction: column; + width: 250px; + + border-right: none; + border-radius: 6px 0 0 6px; + z-index: 1; + overflow: hidden; + + @include until($tablet) { + width: 50px; + } + + button { + flex: 1 1; + padding: 5px 15px; + border: none; + color: #FFF; + background: linear-gradient(to right, rgba(mc('light-blue', '800'), .7), rgba(mc('light-blue', '800'), 1)); + border-top: 1px solid rgba(mc('light-blue', '900'), .5); + font-family: $core-font-standard; + font-weight: 600; + text-align: left; + min-height: 40px; + display: flex; + justify-content: flex-start; + align-items: center; + transition: all .4s ease; + + &:focus { + outline: none; + } + + @include until($tablet) { + justify-content: center; + } + + &:hover { + background-color: mc('light-blue', '900'); + } + + &:first-child { + border-top: none; + + &.is-active { + border-top: 1px solid rgba(255,255,255, .5); + } + } + + &.is-active { + background-image: linear-gradient(to right, rgba(255,255,255,1) 0%,rgba(255,255,255,.77) 100%); + color: mc('light-blue', '700'); + cursor: default; + + &:hover { + background-color: transparent; + } + + svg path { + fill: mc('light-blue', '800'); + } + } + + i { + margin-right: 10px; + font-size: 16px; + + @include until($tablet) { + margin-right: 0; + font-size: 20px; + } + } + + svg { + margin-right: 10px; + width: auto; + height: 20px; + max-width: 18px; + max-height: 20px; + + path { + fill: #FFF; + } + + @include until($tablet) { + margin-right: 0; + font-size: 20px; + } + } + + span { + font-weight: 600; + + @include until($tablet) { + display: none; + } + } + } + } + + &-frame { + background-image: radial-gradient(circle at top center, rgba(255,255,255,1) 5%,rgba(255,255,255,.6) 100%); + border: 1px solid rgba(255,255,255, .5); + border-radius: 6px; + width: 400px; + padding: 1rem; + color: mc('grey', '700'); + display: flex; + justify-content: center; + flex-direction: column; + text-align: center; + + @include until($tablet) { + width: 350px; + } + + h1 { + font-size: 2rem; + font-weight: 600; + color: mc('light-blue', '700'); + text-shadow: 1px 1px 0 #FFF; + padding: 0; + margin: 0; + } + + h2 { + font-size: 1.5rem; + font-weight: 300; + color: mc('grey', '700'); + text-shadow: 1px 1px 0 #FFF; + padding: 0; + margin: 0 0 25px 0; + } + + form { + display: flex; + flex-direction: column; + } + + input[type=text], input[type=password] { + width: 100%; + border: 1px solid #FFF; + border-radius: 3px; + background-color: rgba(255,255,255,.9); + box-shadow: inset 0 0 0 3px rgba(255,255,255, .25); + padding: 0 15px; + height: 40px; + margin: 0 0 10px 0; + color: mc('grey', '700'); + font-weight: 600; + font-size: .8rem; + transition: all 0.4s ease; + text-align: center; + + &:focus { + outline: none; + border-color: mc('light-blue','500'); + background-color: rgba(255,255,255,1); + box-shadow: inset 0 0 0 3px rgba(mc('light-blue','500'), .25); + color: mc('light-blue', '800'); + } + + } + + } + + &-copyright { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + left: 0; + bottom: 10vh; + width: 100%; + z-index: 2; + color: mc('grey', '500'); + font-weight: 400; + + a { + font-weight: 600; + color: mc('light-blue', '500'); + margin-left: .25rem; + } + + } } diff --git a/config.sample.yml b/config.sample.yml index aa1e7c0a..5421d5a8 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -5,23 +5,9 @@ # https://docs.requarks.io/wiki/install # --------------------------------------------------------------------- -# Title of this site +# Port the main server should listen to # --------------------------------------------------------------------- -title: Wiki - -# --------------------------------------------------------------------- -# Full public path to the site, without the trailing slash -# --------------------------------------------------------------------- -# INCLUDE CLIENT PORT IF NOT 80/443! - -host: http://localhost - -# --------------------------------------------------------------------- -# Port the main server should listen to (80 by default) -# --------------------------------------------------------------------- -# To use process.env.PORT, comment the line below: - port: 80 # --------------------------------------------------------------------- @@ -33,140 +19,38 @@ paths: data: ./data # --------------------------------------------------------------------- -# Upload Limits -# --------------------------------------------------------------------- -# In megabytes (MB) - -uploads: - maxImageFileSize: 3 - maxOtherFileSize: 100 - -# --------------------------------------------------------------------- -# Site Language -# --------------------------------------------------------------------- -# Possible values: en, de, es, fa, fr, ja, ko, nl, pt, ru, sr, tr or zh - -lang: en - -# Enable for right to left languages (e.g. arabic): -langRtl: false - -# --------------------------------------------------------------------- -# Site Authentication +# Database # --------------------------------------------------------------------- -public: false - -auth: - defaultReadAccess: false - local: - enabled: true - google: - enabled: true - clientId: GOOGLE_CLIENT_ID - clientSecret: GOOGLE_CLIENT_SECRET - microsoft: - enabled: true - clientId: MS_APP_ID - clientSecret: MS_APP_SECRET - facebook: - enabled: false - clientId: FACEBOOK_APP_ID - clientSecret: FACEBOOK_APP_SECRET - github: - enabled: false - clientId: GITHUB_CLIENT_ID - clientSecret: GITHUB_CLIENT_SECRET - slack: - enabled: false - clientId: 'SLACK_CLIENT_ID' - clientSecret: 'SLACK_CLIENT_SECRET' - ldap: - enabled: false - url: ldap://serverhost:389 - bindDn: cn='root' - bindCredentials: BIND_PASSWORD - searchBase: o=users,o=example.com - searchFilter: (uid={{username}}) - tlsEnabled: false - tlsCertPath: C:\example\root_ca_cert.crt - azure: - enabled: false - clientId: APP_ID - clientSecret: APP_SECRET_KEY - resource: '00000002-0000-0000-c000-000000000000' - tenant: 'YOUR_TENANT.onmicrosoft.com' +db: + host: localhost + port: 5432 + user: wikijs + pass: wikijsrocks + db: wiki # --------------------------------------------------------------------- -# Secret key to use when encrypting sessions -# --------------------------------------------------------------------- -# Use a long and unique random string (256-bit keys are perfect!) - -sessionSecret: 1234567890abcdefghijklmnopqrstuvxyz - -# --------------------------------------------------------------------- -# Database Connection String -# --------------------------------------------------------------------- -# You can also use an ENV variable by using $ENV_VAR_NAME as the value - -db: mongodb://localhost:27017/wiki - -# --------------------------------------------------------------------- -# Git Connection Info +# Redis # --------------------------------------------------------------------- -git: - url: https://github.com/Organization/Repo - branch: master - auth: - - # Type: basic or ssh - type: ssh - - # Only for Basic authentication: - username: marty - password: MartyMcFly88 - - # Only for SSH authentication: - privateKey: /etc/wiki/keys/git.pem - - sslVerify: true - - # Default email to use as commit author - serverEmail: marty@example.com - - # Whether to use user email as author in commits - showUserEmail: true +redis: + host: localhost + port: 6379 + db: 0 + password: null # --------------------------------------------------------------------- -# Features +# Background Workers # --------------------------------------------------------------------- -# You can enable / disable specific features below +# Leave 0 for auto based on CPU cores -features: - linebreaks: true - mathjax: true +workers: 0 # --------------------------------------------------------------------- -# External Logging +# High Availability # --------------------------------------------------------------------- +# Read the docs BEFORE changing these settings! -externalLogging: - bugsnag: false - loggly: false - papertrail: false - rollbar: false - sentry: false - -# --------------------------------------------------------------------- -# Color Theme -# --------------------------------------------------------------------- - -theme: - primary: indigo - alt: blue-grey - viewSource: all # all | write | false - footer: blue-grey - code: - dark: true - colorize: true +ha: + nodeuid: primary + readonly: false diff --git a/npm/README.md b/npm/README.md deleted file mode 100644 index d482407e..00000000 --- a/npm/README.md +++ /dev/null @@ -1,14 +0,0 @@ -![Wiki.js](https://raw.githubusercontent.com/Requarks/wiki-site/1.0/assets/images/logo.png) - -# Wiki.js - -[![npm](https://img.shields.io/npm/v/wiki.js.svg?style=flat-square)](https://github.com/Requarks) -[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat-square&maxAge=3600)](https://github.com/Requarks/wiki/releases) -[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat-square)](https://github.com/requarks/wiki/blob/master/LICENSE) - -This npm package is an installer for Wiki.js. -For information about Wiki.js, including detailed installation steps, read the following links: - -- [Official Website](https://wiki.js.org/) -- [Installation Guide](https://wiki.js.org/get-started.html) -- [GitHub Repository](https://github.com/Requarks/wiki) diff --git a/npm/install.js b/npm/install.js deleted file mode 100644 index cd0a0511..00000000 --- a/npm/install.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict' - -// ===================================================== -// Wiki.js -// Installation Script -// ===================================================== - -const path = require('path') -const spawn = require('child_process').spawn -const installDir = path.resolve(__dirname, '../..') -const cmd = (process.platform !== 'win32') - ? 'curl -s -S -o- https://wiki.js.org/install.sh | bash' - : `PowerShell.exe -NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://wiki.js.org/install.ps1'))"` - -console.info(`Executing installation script for ${process.platform} platform...`) - -let inst = spawn(cmd, [], { - cwd: installDir, - env: process.env, - shell: true, - stdio: 'inherit', - detached: true -}) - -inst.unref() diff --git a/npm/package.json b/npm/package.json deleted file mode 100644 index 2098e56c..00000000 --- a/npm/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "wiki.js", - "version": "1.0.5-rev.1", - "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown", - "main": "install.js", - "scripts": { - "test": "exit 1", - "postinstall": "node install.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Requarks/wiki.git" - }, - "keywords": [ - "wiki", - "wikis", - "wikijs", - "wiki.js", - "wiki-js", - "docs", - "documentation", - "markdown", - "guides" - ], - "author": "Nicolas Giard", - "license": "AGPL-3.0", - "bugs": { - "url": "https://github.com/Requarks/wiki/issues" - }, - "homepage": "https://github.com/Requarks/wiki#readme", - "dependencies": {} -} diff --git a/package.json b/package.json index ef84b9e1..692165db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wiki", - "version": "1.0.11", + "version": "2.0.0", "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown", "main": "wiki.js", "scripts": { @@ -9,7 +9,6 @@ "restart": "node wiki restart", "build": "node tools/fuse", "dev": "node tools/fuse -d", - "dev-configure": "node tools/fuse -c", "test": "jest", "postinstall": "opencollective postinstall" }, @@ -38,135 +37,143 @@ "node": ">=6.11.1" }, "dependencies": { - "auto-load": "~3.0.0", - "axios": "~0.16.2", - "bcryptjs-then": "~1.0.1", - "bluebird": "~3.5.0", - "body-parser": "~1.17.2", - "bunyan": "~1.8.12", - "cheerio": "~1.0.0-rc.2", - "child-process-promise": "~2.2.1", - "chokidar": "~1.7.0", - "compression": "~1.7.0", - "connect-flash": "~0.1.1", - "connect-mongo": "~1.3.2", - "cookie-parser": "~1.4.3", - "cron": "~1.2.1", - "diff2html": "~2.3.0", - "execa": "~0.8.0", - "express": "~4.15.4", + "apollo-server-express": "1.1.3", + "auto-load": "3.0.0", + "axios": "0.16.2", + "bcryptjs-then": "1.0.1", + "bluebird": "3.5.1", + "body-parser": "1.18.2", + "bull": "3.3.0", + "bunyan": "1.8.12", + "cheerio": "1.0.0-rc.2", + "child-process-promise": "2.2.1", + "chokidar": "1.7.0", + "compression": "1.7.1", + "connect-flash": "0.1.1", + "connect-redis": "3.3.2", + "cookie-parser": "1.4.3", + "diff2html": "2.3.1", + "dotize": "^0.2.0", + "execa": "0.8.0", + "express": "4.16.1", "express-brute": "1.0.1", - "express-brute-mongoose": "~0.0.9", - "express-session": "~1.15.5", - "file-type": "~6.1.0", - "filesize.js": "~1.0.2", - "follow-redirects": "~1.2.4", - "fs-extra": "~4.0.1", - "git-wrapper2-promise": "~0.2.9", - "highlight.js": "~9.12.0", - "i18next": "~9.0.0", - "i18next-express-middleware": "~1.0.5", - "i18next-node-fs-backend": "~1.0.0", - "image-size": "~0.6.0", - "jimp": "~0.2.28", - "js-yaml": "~3.9.1", - "jsonwebtoken": "~7.4.3", - "klaw": "~2.1.0", - "levelup": "~1.3.9", - "lodash": "~4.17.4", - "markdown-it": "~8.4.0", - "markdown-it-abbr": "~1.0.4", - "markdown-it-anchor": "~4.0.0", - "markdown-it-attrs": "~1.1.0", - "markdown-it-emoji": "~1.4.0", - "markdown-it-expand-tabs": "~1.0.12", + "express-brute-redis": "0.0.1", + "express-session": "1.15.6", + "file-type": "6.2.0", + "filesize.js": "1.0.2", + "follow-redirects": "1.2.5", + "fs-extra": "4.0.2", + "git-wrapper2-promise": "0.2.9", + "graphql": "0.10.5", + "graphql-tools": "2.2.1", + "highlight.js": "9.12.0", + "i18next": "9.1.0", + "i18next-express-middleware": "1.0.7", + "i18next-localstorage-cache": "1.1.1", + "i18next-node-fs-backend": "1.0.0", + "image-size": "0.6.1", + "ioredis": "3.1.4", + "jimp": "0.2.28", + "js-yaml": "3.10.0", + "jsonwebtoken": "8.0.1", + "klaw": "2.1.0", + "lodash": "4.17.4", + "markdown-it": "8.4.0", + "markdown-it-abbr": "1.0.4", + "markdown-it-anchor": "4.0.0", + "markdown-it-attrs": "1.2.0", + "markdown-it-emoji": "1.4.0", + "markdown-it-expand-tabs": "1.0.12", "markdown-it-external-links": "0.0.6", - "markdown-it-footnote": "~3.0.1", - "markdown-it-mathjax": "~2.0.0", - "markdown-it-task-lists": "~2.0.1", - "mathjax-node": "~1.2.0", - "memdown": "~1.2.4", - "mime-types": "~2.1.16", - "moment": "~2.18.1", - "moment-timezone": "~0.5.13", - "mongodb": "~2.2.31", - "mongoose": "~4.11.9", - "multer": "~1.3.0", - "node-2fa": "~1.1.2", - "node-graceful": "~0.2.3", - "opencollective": "~1.0.3", - "ora": "~1.3.0", - "passport": "~0.4.0", + "markdown-it-footnote": "3.0.1", + "markdown-it-mathjax": "2.0.0", + "markdown-it-task-lists": "2.0.1", + "mathjax-node": "1.2.1", + "mime-types": "2.1.17", + "moment": "2.18.1", + "moment-timezone": "0.5.13", + "multer": "1.3.0", + "node-2fa": "1.1.2", + "node-graceful": "0.2.3", + "ora": "1.3.0", + "passport": "0.4.0", "passport-azure-ad-oauth2": "0.0.4", - "passport-facebook": "~2.1.1", - "passport-github2": "~0.1.10", - "passport-google-oauth20": "~1.0.0", - "passport-ldapauth": "~2.0.0", - "passport-local": "~1.0.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-slack": "0.0.7", - "passport-windowslive": "~1.0.2", - "passport.socketio": "~3.7.0", - "pm2": "~2.6.1", - "pug": "~2.0.0-rc.3", - "read-chunk": "~2.1.0", - "remove-markdown": "~0.2.2", - "request": "~2.81.0", - "search-index-adder": "~0.3.9", - "search-index-searcher": "~0.2.10", - "semver": "~5.4.1", - "serve-favicon": "~2.4.3", - "simplemde": "~1.11.2", - "socket.io": "~2.0.2", - "stopword": "~0.1.6", - "stream-to-promise": "~2.2.0", - "tar": "~4.0.1", - "through2": "~2.0.3", - "validator": "~8.1.0", - "validator-as-promised": "~1.0.2", - "winston": "~2.3.1", - "yargs": "~8.0.1" + "passport-windowslive": "1.0.2", + "pg": "6.4.2", + "pg-hstore": "2.3.2", + "pg-promise": "6.10.3", + "pm2": "2.7.1", + "pug": "2.0.0-rc.4", + "qr-image": "3.2.0", + "read-chunk": "2.1.0", + "remove-markdown": "0.2.2", + "request": "2.83.0", + "semver": "5.4.1", + "sequelize": "4.13.5", + "serve-favicon": "2.4.5", + "simplemde": "1.11.2", + "stream-to-promise": "2.2.0", + "tar": "4.0.1", + "through2": "2.0.3", + "validator": "9.0.0", + "validator-as-promised": "1.0.2", + "winston": "2.4.0", + "yargs": "9.0.1" }, "devDependencies": { - "@glimpse/glimpse": "~0.22.15", - "@panter/vue-i18next": "~0.5.0", - "babel-cli": "~6.26.0", - "babel-jest": "~20.0.3", - "babel-plugin-transform-object-assign": "~6.22.0", - "babel-preset-es2015": "~6.24.1", - "brace": "~0.10.0", - "colors": "~1.1.2", - "consolidate": "~0.14.5", - "eslint": "~4.5.0", - "eslint-config-standard": "~10.2.1", - "eslint-plugin-import": "~2.7.0", - "eslint-plugin-node": "~5.1.0", - "eslint-plugin-promise": "~3.5.0", - "eslint-plugin-standard": "~3.0.1", - "fuse-box": "~2.2.2", - "i18next-xhr-backend": "~1.4.2", - "jest": "~20.0.4", - "jest-junit": "~3.1.0", - "jquery": "~3.2.1", - "jquery-contextmenu": "~2.5.0", - "jquery-simple-upload": "~1.0.0", - "jquery-smooth-scroll": "~2.2.0", - "jquery-sticky": "~1.0.4", - "lodash-cli": "~4.17.4", - "lodash-es": "~4.17.4", - "node-sass": "~4.5.3", - "nodemon": "~1.11.0", - "pug-lint": "~2.4.0", - "twemoji-awesome": "~1.0.6", - "typescript": "~2.5.2", - "uglify-es": "~3.0.28", - "vee-validate": "~2.0.0-rc.14", - "vue": "~2.4.2", - "vue-clipboards": "~1.1.0", - "vue-lodash": "~1.0.3", - "vue-resource": "~1.3.4", - "vue-template-compiler": "~2.4.2", - "vue-template-es2015-compiler": "~1.5.3", - "vuex": "~2.4.0" + "@glimpse/glimpse": "0.22.15", + "@panter/vue-i18next": "0.6.1", + "apollo-client": "^1.9.3", + "autoprefixer": "7.1.5", + "babel-cli": "6.26.0", + "babel-core": "6.26.0", + "babel-jest": "21.2.0", + "babel-preset-env": "1.6.0", + "babel-preset-es2015": "6.24.1", + "babel-preset-stage-2": "6.24.1", + "brace": "0.10.0", + "colors": "1.1.2", + "consolidate": "0.14.5", + "eslint": "4.8.0", + "eslint-config-requarks": "1.0.7", + "eslint-config-standard": "10.2.1", + "eslint-plugin-import": "2.7.0", + "eslint-plugin-node": "5.2.0", + "eslint-plugin-promise": "3.5.0", + "eslint-plugin-standard": "3.0.1", + "fuse-box": "2.3.3", + "graphql-tag": "^2.4.2", + "i18next-xhr-backend": "1.4.3", + "jest": "21.2.1", + "jquery": "3.2.1", + "jquery-contextmenu": "2.6.2", + "jquery-simple-upload": "1.0.0", + "js-cookie": "2.1.4", + "node-sass": "4.5.3", + "nodemon": "1.12.1", + "postcss-selector-parser": "2.2.3", + "pug-lint": "2.5.0", + "twemoji-awesome": "1.0.6", + "typescript": "2.5.3", + "uglify-es": "3.1.3", + "vee-validate": "2.0.0-rc.18", + "vue": "2.4.4", + "vue-clipboards": "1.1.0", + "vue-hot-reload-api": "2.1.1", + "vue-lodash": "1.0.4", + "vue-material": "^0.7.5", + "vue-resource": "1.3.4", + "vue-simple-breakpoints": "1.0.2", + "vue-template-compiler": "2.4.4", + "vue-template-es2015-compiler": "1.5.3", + "vuex": "2.4.1", + "vuex-persistedstate": "2.0.0" }, "jest": { "testResultsProcessor": "./node_modules/jest-junit", diff --git a/server/agent.js b/server/agent.js deleted file mode 100644 index 38efd466..00000000 --- a/server/agent.js +++ /dev/null @@ -1,222 +0,0 @@ -// =========================================== -// Wiki.js - Background Agent -// 1.0.0 -// Licensed under AGPLv3 -// =========================================== - -const path = require('path') -const ROOTPATH = process.cwd() -const SERVERPATH = path.join(ROOTPATH, 'server') - -global.ROOTPATH = ROOTPATH -global.SERVERPATH = SERVERPATH -const IS_DEBUG = process.env.NODE_ENV === 'development' - -let appconf = require('./libs/config')() -global.appconfig = appconf.config -global.appdata = appconf.data - -// ---------------------------------------- -// Load Winston -// ---------------------------------------- - -global.winston = require('./libs/logger')(IS_DEBUG, 'AGENT') - -// ---------------------------------------- -// Load global modules -// ---------------------------------------- - -global.winston.info('Background Agent is initializing...') - -global.db = require('./libs/db').init() -global.upl = require('./libs/uploads-agent').init() -global.git = require('./libs/git').init() -global.entries = require('./libs/entries').init() -global.lang = require('i18next') -global.mark = require('./libs/markdown') - -// ---------------------------------------- -// Load modules -// ---------------------------------------- - -const moment = require('moment') -const Promise = require('bluebird') -const fs = Promise.promisifyAll(require('fs-extra')) -const klaw = require('klaw') -const Cron = require('cron').CronJob -const i18nBackend = require('i18next-node-fs-backend') - -const entryHelper = require('./helpers/entry') - -// ---------------------------------------- -// Localization Engine -// ---------------------------------------- - -global.lang - .use(i18nBackend) - .init({ - load: 'languageOnly', - ns: ['common', 'admin', 'auth', 'errors', 'git'], - defaultNS: 'common', - saveMissing: false, - preload: [appconfig.lang], - lng: appconfig.lang, - fallbackLng: 'en', - backend: { - loadPath: path.join(SERVERPATH, 'locales/{{lng}}/{{ns}}.json') - } - }) - -// ---------------------------------------- -// Start Cron -// ---------------------------------------- - -let job -let jobIsBusy = false -let jobUplWatchStarted = false - -global.db.onReady.then(() => { - return global.db.Entry.remove({}) -}).then(() => { - job = new Cron({ - cronTime: '0 */5 * * * *', - onTick: () => { - // Make sure we don't start two concurrent jobs - - if (jobIsBusy) { - global.winston.warn('Previous job has not completed gracefully or is still running! Skipping for now. (This is not normal, you should investigate)') - return - } - global.winston.info('Running all jobs...') - jobIsBusy = true - - // Prepare async job collector - - let jobs = [] - let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo) - let dataPath = path.resolve(ROOTPATH, appconfig.paths.data) - let uploadsTempPath = path.join(dataPath, 'temp-upload') - - // ---------------------------------------- - // REGULAR JOBS - // ---------------------------------------- - - //* **************************************** - // -> Sync with Git remote - //* **************************************** - - jobs.push(global.git.resync().then(() => { - // -> Stream all documents - - let cacheJobs = [] - let jobCbStreamDocsResolve = null - let jobCbStreamDocs = new Promise((resolve, reject) => { - jobCbStreamDocsResolve = resolve - }) - - klaw(repoPath).on('data', function (item) { - if (path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') { - let entryPath = entryHelper.parsePath(entryHelper.getEntryPathFromFullPath(item.path)) - let cachePath = entryHelper.getCachePath(entryPath) - - // -> Purge outdated cache - - cacheJobs.push( - fs.statAsync(cachePath).then((st) => { - return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active' - }).catch((err) => { - return (err.code !== 'EEXIST') ? err : 'new' - }).then((fileStatus) => { - // -> Delete expired cache file - - if (fileStatus === 'expired') { - return fs.unlinkAsync(cachePath).return(fileStatus) - } - - return fileStatus - }).then((fileStatus) => { - // -> Update cache and search index - - if (fileStatus !== 'active') { - return global.entries.updateCache(entryPath).then(entry => { - process.send({ - action: 'searchAdd', - content: entry - }) - return true - }) - } - - return true - }) - ) - } - }).on('end', () => { - jobCbStreamDocsResolve(Promise.all(cacheJobs)) - }) - - return jobCbStreamDocs - })) - - //* **************************************** - // -> Clear failed temporary upload files - //* **************************************** - - jobs.push( - fs.readdirAsync(uploadsTempPath).then((ls) => { - let fifteenAgo = moment().subtract(15, 'minutes') - - return Promise.map(ls, (f) => { - return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s } }) - }).filter((s) => { return s.stat.isFile() }).then((arrFiles) => { - return Promise.map(arrFiles, (f) => { - if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) { - return fs.unlinkAsync(path.join(uploadsTempPath, f.filename)) - } else { - return true - } - }) - }) - }) - ) - - // ---------------------------------------- - // Run - // ---------------------------------------- - - Promise.all(jobs).then(() => { - global.winston.info('All jobs completed successfully! Going to sleep for now.') - - if (!jobUplWatchStarted) { - jobUplWatchStarted = true - global.upl.initialScan().then(() => { - job.start() - }) - } - - return true - }).catch((err) => { - global.winston.error('One or more jobs have failed: ', err) - }).finally(() => { - jobIsBusy = false - }) - }, - start: false, - timeZone: 'UTC', - runOnInit: true - }) -}) - -// ---------------------------------------- -// Shutdown gracefully -// ---------------------------------------- - -process.on('disconnect', () => { - global.winston.warn('Lost connection to main server. Exiting...') - job.stop() - process.exit() -}) - -process.on('exit', () => { - job.stop() -}) diff --git a/server/app/data.yml b/server/app/data.yml index 17c1e685..129dc9b9 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -5,67 +5,49 @@ name: Wiki.js defaults: config: - title: Wiki - host: http://localhost port: 80 paths: repo: ./repo data: ./data - uploads: - maxImageFileSize: 3, - maxOtherFileSize: 100 - lang: en - langRtl: false - public: false - auth: - defaultReadAccess: false - local: - enabled: true - microsoft: - enabled: false - google: - enabled: false - facebook: - enabled: false - github: - enabled: false - slack: - enabled: false - ldap: - enabled: false - azure: - enabled: false - db: mongodb://localhost/wiki - sessionSecret: null - admin: null - git: - url: null - branch: master - auth: - type: basic - username: null - password: null - privateKey: null - sslVerify: true - serverEmail: wiki@example.com - showUserEmail: true - features: - linebreaks: true - mathjax: true - externalLogging: - bugsnap: false - loggly: false - papertrail: false - rollbar: false - sentry: false - theme: - primary: indigo - alt: blue-grey - footer: blue-grey - viewSource: false - code: - dark: true - colorize: true + db: + host: localhost + port: 5432 + user: wikijs + pass: wikijsrocks + db: wiki + redis: + host: localhost + port: 6379 + db: 0 + password: null + workers: 0 + ha: + nodeuid: primary + readonly: false + site: + path: '' + lang: en + title: Wiki.js +configNamespaces: + - auth + - features + - git + - logging + - site + - theme + - uploads +queues: + - gitSync + - uplClearTemp +authProviders: + - local + - microsoft + - google + - facebook + - github + - slack + - ldap + - azure colors: - red - pink @@ -108,6 +90,12 @@ langs: - id: ko name: Korean - 한국어 + - + id: fa + name: Persian (Fārsi) - فارسی + - + id: pt + name: Portuguese - Português - id: ru name: Russian - Русский diff --git a/server/authentication/azure.js b/server/authentication/azure.js new file mode 100644 index 00000000..70e23cc4 --- /dev/null +++ b/server/authentication/azure.js @@ -0,0 +1,37 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// Azure AD Account +// ------------------------------------ + +const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy + +module.exports = { + key: 'azure', + title: 'Azure Active Directory', + useForm: false, + props: ['clientId', 'clientSecret', 'callbackURL', 'resource', 'tenant'], + init (passport, conf) { + const jwt = require('jsonwebtoken') + passport.use('azure_ad_oauth2', + new AzureAdOAuth2Strategy({ + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL, + resource: conf.resource, + tenant: conf.tenant + }, (accessToken, refreshToken, params, profile, cb) => { + let waadProfile = jwt.decode(params.id_token) + waadProfile.id = waadProfile.oid + waadProfile.provider = 'azure' + wiki.db.User.processProfile(waadProfile).then((user) => { + return cb(null, user) || true + }).catch((err) => { + return cb(err, null) || true + }) + } + )) + } +} diff --git a/server/authentication/facebook.js b/server/authentication/facebook.js new file mode 100644 index 00000000..24b31416 --- /dev/null +++ b/server/authentication/facebook.js @@ -0,0 +1,32 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// Facebook Account +// ------------------------------------ + +const FacebookStrategy = require('passport-facebook').Strategy + +module.exports = { + key: 'facebook', + title: 'Facebook', + useForm: false, + props: ['clientId', 'clientSecret', 'callbackURL'], + init (passport, conf) { + passport.use('facebook', + new FacebookStrategy({ + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL, + profileFields: ['id', 'displayName', 'email'] + }, function (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/authentication/github.js b/server/authentication/github.js new file mode 100644 index 00000000..2dbca0c4 --- /dev/null +++ b/server/authentication/github.js @@ -0,0 +1,32 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// GitHub Account +// ------------------------------------ + +const GitHubStrategy = require('passport-github2').Strategy + +module.exports = { + key: 'github', + title: 'GitHub', + useForm: false, + props: ['clientId', 'clientSecret', 'callbackURL'], + init (passport, conf) { + passport.use('github', + new GitHubStrategy({ + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL, + scope: ['user:email'] + }, (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/authentication/google.js b/server/authentication/google.js new file mode 100644 index 00000000..d353e6c4 --- /dev/null +++ b/server/authentication/google.js @@ -0,0 +1,31 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// Google ID Account +// ------------------------------------ + +const GoogleStrategy = require('passport-google-oauth20').Strategy + +module.exports = { + key: 'google', + title: 'Google ID', + useForm: false, + props: ['clientId', 'clientSecret', 'callbackURL'], + init (passport, conf) { + passport.use('google', + new GoogleStrategy({ + 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/authentication/ldap.js b/server/authentication/ldap.js new file mode 100644 index 00000000..720b5f4b --- /dev/null +++ b/server/authentication/ldap.js @@ -0,0 +1,46 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// LDAP Account +// ------------------------------------ + +const LdapStrategy = require('passport-ldapauth').Strategy +const fs = require('fs') + +module.exports = { + key: 'ldap', + title: 'LDAP / Active Directory', + useForm: true, + props: ['url', 'bindDn', 'bindCredentials', 'searchBase', 'searchFilter', 'tlsEnabled', 'tlsCertPath'], + init (passport, conf) { + passport.use('ldapauth', + new LdapStrategy({ + server: { + url: conf.url, + bindDn: conf.bindDn, + bindCredentials: conf.bindCredentials, + searchBase: conf.searchBase, + searchFilter: conf.searchFilter, + searchAttributes: ['displayName', 'name', 'cn', 'mail'], + tlsOptions: (conf.tlsEnabled) ? { + ca: [ + fs.readFileSync(conf.tlsCertPath) + ] + } : {} + }, + usernameField: 'email', + passReqToCallback: false + }, (profile, cb) => { + profile.provider = 'ldap' + profile.id = profile.dn + wiki.db.User.processProfile(profile).then((user) => { + return cb(null, user) || true + }).catch((err) => { + return cb(err, null) || true + }) + } + )) + } +} diff --git a/server/authentication/local.js b/server/authentication/local.js new file mode 100644 index 00000000..e2ca7faa --- /dev/null +++ b/server/authentication/local.js @@ -0,0 +1,38 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// Local Account +// ------------------------------------ + +const LocalStrategy = require('passport-local').Strategy + +module.exports = { + key: 'local', + title: 'Local', + useForm: true, + props: [], + init (passport, conf) { + passport.use('local', + new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' + }, (uEmail, uPassword, done) => { + wiki.db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => { + if (user) { + return user.validatePassword(uPassword).then(() => { + return done(null, user) || true + }).catch((err) => { + return done(err, null) + }) + } else { + return done(new Error('INVALID_LOGIN'), null) + } + }).catch((err) => { + done(err, null) + }) + } + )) + } +} diff --git a/server/authentication/microsoft.js b/server/authentication/microsoft.js new file mode 100644 index 00000000..b761aa41 --- /dev/null +++ b/server/authentication/microsoft.js @@ -0,0 +1,31 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// Microsoft Account +// ------------------------------------ + +const WindowsLiveStrategy = require('passport-windowslive').Strategy + +module.exports = { + key: 'microsoft', + title: 'Microsoft Account', + useForm: false, + props: ['clientId', 'clientSecret', 'callbackURL'], + init (passport, conf) { + passport.use('microsoft', + new WindowsLiveStrategy({ + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL + }, function (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/authentication/slack.js b/server/authentication/slack.js new file mode 100644 index 00000000..e4aa6d5f --- /dev/null +++ b/server/authentication/slack.js @@ -0,0 +1,31 @@ +'use strict' + +/* global wiki */ + +// ------------------------------------ +// Slack Account +// ------------------------------------ + +const SlackStrategy = require('passport-slack').Strategy + +module.exports = { + key: 'slack', + title: 'Slack', + useForm: false, + props: ['clientId', 'clientSecret', 'callbackURL'], + init (passport, conf) { + passport.use('slack', + new SlackStrategy({ + 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/configure.js b/server/configure.js index 0766d695..78c00c3c 100644 --- a/server/configure.js +++ b/server/configure.js @@ -1,11 +1,12 @@ -'use strict' +const path = require('path') -module.exports = (port, spinner) => { - const path = require('path') +/* global wiki */ - const ROOTPATH = process.cwd() - const SERVERPATH = path.join(ROOTPATH, 'server') - const IS_DEBUG = process.env.NODE_ENV === 'development' +module.exports = () => { + wiki.config.site = { + path: '', + title: 'Wiki.js' + } // ---------------------------------------- // Load modules @@ -26,28 +27,30 @@ module.exports = (port, spinner) => { // Define Express App // ---------------------------------------- - var app = express() + let app = express() app.use(compression()) - var server + let server // ---------------------------------------- // Public Assets // ---------------------------------------- - app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico'))) - app.use(express.static(path.join(ROOTPATH, 'assets'))) + app.use(favicon(path.join(wiki.ROOTPATH, 'assets', 'favicon.ico'))) + app.use(express.static(path.join(wiki.ROOTPATH, 'assets'))) // ---------------------------------------- // View Engine Setup // ---------------------------------------- - app.set('views', path.join(SERVERPATH, 'views')) + app.set('views', path.join(wiki.SERVERPATH, 'views')) app.set('view engine', 'pug') app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: false })) + app.locals.config = wiki.config + app.locals.data = wiki.data app.locals._ = require('lodash') // ---------------------------------------- @@ -55,21 +58,8 @@ module.exports = (port, spinner) => { // ---------------------------------------- app.get('*', (req, res) => { - let langs = [] - let conf = {} - try { - langs = yaml.safeLoad(fs.readFileSync(path.join(SERVERPATH, 'app/data.yml'), 'utf8')).langs - conf = yaml.safeLoad(fs.readFileSync(path.join(ROOTPATH, 'config.yml'), 'utf8')) - } catch (err) { - console.error(err) - } - res.render('configure/index', { - langs, - conf, - runmode: { - staticPort: (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER), - staticMongo: (!_.isNil(process.env.WIKI_JS_HEROKU)) - } + fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => { + res.render('configure/index', { packageObj }) }) }) @@ -80,15 +70,15 @@ module.exports = (port, spinner) => { Promise.mapSeries([ () => { const semver = require('semver') - if (!semver.satisfies(semver.clean(process.version), '>=6.9.0')) { - throw new Error('Node.js version is too old. Minimum is v6.6.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 v6.9.0.' + return 'Node.js ' + process.version + ' detected. Minimum is 6.11.1.' }, () => { return Promise.try(() => { require('crypto') - }).catch(err => { // eslint-disable-line handle-callback-err + }).catch(err => { throw new Error('Crypto Node.js module is not available.') }).return('Node.js Crypto module is available.') }, @@ -102,24 +92,24 @@ module.exports = (port, spinner) => { } let gitver = _.head(stdout.match(/[\d]+\.[\d]+(\.[\d]+)?/gi)) if (!gitver || !semver.satisfies(semver.clean(gitver), '>=2.7.4')) { - reject(new Error('Git version is too old. Minimum is v2.7.4.')) + reject(new Error('Git version is too old. Minimum is 2.7.4.')) } - resolve('Git v' + gitver + ' detected. Minimum is v2.7.4.') + resolve('Git ' + gitver + ' detected. Minimum is 2.7.4.') }) }) }, () => { const os = require('os') - if (os.totalmem() < 1000 * 1000 * 768) { - throw new Error('Not enough memory. Minimum is 768 MB.') + if (os.totalmem() < 1000 * 1000 * 512) { + throw new Error('Not enough memory. Minimum is 512 MB.') } - return _.round(os.totalmem() / (1024 * 1024)) + ' MB of system memory available. Minimum is 768 MB.' + return _.round(os.totalmem() / (1024 * 1024)) + ' MB of system memory available. Minimum is 512 MB.' }, () => { let fs = require('fs') return Promise.try(() => { - fs.accessSync(path.join(ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK) - }).catch(err => { // eslint-disable-line handle-callback-err + fs.accessSync(path.join(wiki.ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK) + }).catch(err => { throw new Error('config.yml file is not writable by Node.js process or was not created properly.') }).return('config.yml is writable by the setup process.') } @@ -130,43 +120,6 @@ module.exports = (port, spinner) => { }) }) - /** - * 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 */ @@ -174,8 +127,8 @@ module.exports = (port, spinner) => { const exec = require('execa') const url = require('url') - const dataDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData)) - const gitDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo)) + const dataDir = path.resolve(wiki.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData)) + const gitDir = path.resolve(wiki.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo)) let gitRemoteUrl = '' @@ -315,7 +268,7 @@ module.exports = (port, spinner) => { } }) }), - fs.readFileAsync(path.join(ROOTPATH, 'config.yml'), 'utf8').then(confRaw => { + fs.readFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), 'utf8').then(confRaw => { let conf = yaml.safeLoad(confRaw) conf.title = req.body.title conf.host = req.body.host @@ -356,12 +309,12 @@ module.exports = (port, spinner) => { return crypto.randomBytesAsync(32).then(buf => { conf.sessionSecret = buf.toString('hex') confRaw = yaml.safeDump(conf) - return fs.writeFileAsync(path.join(ROOTPATH, 'config.yml'), confRaw) + return fs.writeFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), confRaw) }) }) ).then(() => { if (process.env.IS_HEROKU) { - return fs.outputJsonAsync(path.join(SERVERPATH, 'app/heroku.json'), { configured: true }) + return fs.outputJsonAsync(path.join(wiki.SERVERPATH, 'app/heroku.json'), { configured: true }) } else { return true } @@ -377,7 +330,7 @@ module.exports = (port, spinner) => { */ app.post('/restart', (req, res) => { res.status(204).end() - server.destroy(() => { + /* server.destroy(() => { spinner.text = 'Setup wizard terminated. Restarting in normal mode...' _.delay(() => { const exec = require('execa') @@ -386,7 +339,7 @@ module.exports = (port, spinner) => { process.exit(0) }) }, 1000) - }) + }) */ }) // ---------------------------------------- @@ -403,21 +356,20 @@ module.exports = (port, spinner) => { res.status(err.status || 500) res.send({ message: err.message, - error: IS_DEBUG ? err : {} + error: wiki.IS_DEBUG ? err : {} }) - spinner.fail(err.message) - process.exit(1) + wiki.logger.error(err.message) }) // ---------------------------------------- // Start HTTP server // ---------------------------------------- - spinner.text = 'Starting HTTP server...' + wiki.logger.info(`HTTP Server on port: ${wiki.config.port}`) - app.set('port', port) + app.set('port', wiki.config.port) server = http.createServer(app) - server.listen(port) + server.listen(wiki.config.port) var openConnections = [] @@ -443,10 +395,10 @@ module.exports = (port, spinner) => { switch (error.code) { case 'EACCES': - spinner.fail('Listening on port ' + port + ' requires elevated privileges!') + wiki.logger.error('Listening on port ' + wiki.config.port + ' requires elevated privileges!') return process.exit(1) case 'EADDRINUSE': - spinner.fail('Port ' + port + ' is already in use!') + wiki.logger.error('Port ' + wiki.config.port + ' is already in use!') return process.exit(1) default: throw error @@ -454,6 +406,6 @@ module.exports = (port, spinner) => { }) server.on('listening', () => { - spinner.text = 'Browse to http://localhost:' + port + ' to configure Wiki.js!' + wiki.logger.info('HTTP Server: RUNNING') }) } diff --git a/server/controllers/admin.js b/server/controllers/admin.js index bc7554e0..962c6161 100644 --- a/server/controllers/admin.js +++ b/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, wiki.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, wiki.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, wiki.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' + 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' }) }) diff --git a/server/controllers/auth.js b/server/controllers/auth.js index b513470e..9188102a 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -1,19 +1,19 @@ -'use strict' - -/* global db, lang */ +/* global wiki */ const Promise = require('bluebird') const express = require('express') const router = express.Router() -const passport = require('passport') const ExpressBrute = require('express-brute') -const ExpressBruteMongooseStore = require('express-brute-mongoose') +const ExpressBruteRedisStore = require('express-brute-redis') const moment = require('moment') +const _ = require('lodash') /** * Setup Express-Brute */ -const EBstore = new ExpressBruteMongooseStore(db.Bruteforce) +const EBstore = new ExpressBruteRedisStore({ + client: wiki.redis +}) const bruteforce = new ExpressBrute(EBstore, { freeRetries: 5, minWait: 60 * 1000, @@ -22,8 +22,8 @@ const bruteforce = new ExpressBrute(EBstore, { failCallback (req, res, next, nextValidRequestDate) { req.flash('alert', { class: 'error', - title: lang.t('auth:errors.toomanyattempts'), - message: lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }), + title: wiki.lang.t('auth:errors.toomanyattempts'), + message: wiki.lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }), iconClass: 'fa-times' }) res.redirect('/login') @@ -35,23 +35,24 @@ const bruteforce = new ExpressBrute(EBstore, { */ router.get('/login', function (req, res, next) { res.render('auth/login', { - usr: res.locals.usr + authStrategies: _.reject(wiki.auth.strategies, { key: 'local' }), + hasMultipleStrategies: Object.keys(wiki.config.auth.strategies).length > 1 }) }) router.post('/login', bruteforce.prevent, function (req, res, next) { new Promise((resolve, reject) => { // [1] LOCAL AUTHENTICATION - passport.authenticate('local', function (err, user, info) { + wiki.auth.passport.authenticate('local', function (err, user, info) { if (err) { return reject(err) } if (!user) { return reject(new Error('INVALID_LOGIN')) } resolve(user) })(req, res, next) }).catch({ message: 'INVALID_LOGIN' }, err => { - if (appconfig.auth.ldap && appconfig.auth.ldap.enabled) { + if (_.has(wiki.config.auth.strategy, 'ldap')) { // [2] LDAP AUTHENTICATION return new Promise((resolve, reject) => { - passport.authenticate('ldapauth', function (err, user, info) { + wiki.auth.passport.authenticate('ldapauth', function (err, user, info) { if (err) { return reject(err) } if (info && info.message) { return reject(new Error(info.message)) } if (!user) { return reject(new Error('INVALID_LOGIN')) } @@ -73,13 +74,13 @@ router.post('/login', bruteforce.prevent, function (req, res, next) { // LOGIN FAIL if (err.message === 'INVALID_LOGIN') { req.flash('alert', { - title: lang.t('auth:errors.invalidlogin'), - message: lang.t('auth:errors.invalidloginmsg') + title: wiki.lang.t('auth:errors.invalidlogin'), + message: wiki.lang.t('auth:errors.invalidloginmsg') }) return res.redirect('/login') } else { req.flash('alert', { - title: lang.t('auth:errors.loginerror'), + title: wiki.lang.t('auth:errors.loginerror'), message: err.message }) return res.redirect('/login') @@ -91,19 +92,19 @@ router.post('/login', bruteforce.prevent, function (req, res, next) { * Social Login */ -router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] })) -router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] })) -router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] })) -router.get('/login/github', passport.authenticate('github', { scope: ['user:email'] })) -router.get('/login/slack', passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] })) -router.get('/login/azure', passport.authenticate('azure_ad_oauth2')) +router.get('/login/ms', wiki.auth.passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] })) +router.get('/login/google', wiki.auth.passport.authenticate('google', { scope: ['profile', 'email'] })) +router.get('/login/facebook', wiki.auth.passport.authenticate('facebook', { scope: ['public_profile', 'email'] })) +router.get('/login/github', wiki.auth.passport.authenticate('github', { scope: ['user:email'] })) +router.get('/login/slack', wiki.auth.passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] })) +router.get('/login/azure', wiki.auth.passport.authenticate('azure_ad_oauth2')) -router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' })) -router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' })) -router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' })) -router.get('/login/github/callback', passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' })) -router.get('/login/slack/callback', passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' })) -router.get('/login/azure/callback', passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' })) +router.get('/login/ms/callback', wiki.auth.passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' })) +router.get('/login/google/callback', wiki.auth.passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' })) +router.get('/login/facebook/callback', wiki.auth.passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' })) +router.get('/login/github/callback', wiki.auth.passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' })) +router.get('/login/slack/callback', wiki.auth.passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' })) +router.get('/login/azure/callback', wiki.auth.passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' })) /** * Logout diff --git a/server/controllers/uploads.js b/server/controllers/uploads.js index a2a3f2f2..40b19e68 100644 --- a/server/controllers/uploads.js +++ b/server/controllers/uploads.js @@ -1,6 +1,9 @@ 'use strict' -/* global git, lang, lcdata, upl */ +/* global wiki */ + +module.exports = false +return const express = require('express') const router = express.Router() @@ -12,7 +15,7 @@ const fs = Promise.promisifyAll(require('fs-extra')) const path = require('path') const _ = require('lodash') -const validPathRe = new RegExp('^([a-z0-9/-' + appdata.regex.cjk + appdata.regex.arabic + ']+\\.[a-z0-9]+)$') +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)$') // ========================================== @@ -28,7 +31,7 @@ router.get('/t/*', (req, res, next) => { // todo: Authentication-based access res.sendFile(fileName, { - root: lcdata.getThumbsPath(), + root: wiki.disk.getThumbsPath(), dotfiles: 'deny' }, (err) => { if (err) { @@ -37,12 +40,12 @@ router.get('/t/*', (req, res, next) => { }) }) -router.post('/img', lcdata.uploadImgHandler, (req, res, next) => { +router.post('/img', wiki.disk.uploadImgHandler, (req, res, next) => { let destFolder = _.chain(req.body.folder).trim().toLower().value() - upl.validateUploadsFolder(destFolder).then((destFolderPath) => { + wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => { if (!destFolderPath) { - res.json({ ok: false, msg: lang.t('errors:invalidfolder') }) + res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') }) return true } @@ -50,7 +53,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => { let destFilename = '' let destFilePath = '' - return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => { + return wiki.disk.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => { destFilename = fname destFilePath = path.resolve(destFolderPath, destFilename) @@ -60,7 +63,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => { let mimeInfo = fileType(buf) if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) { - return Promise.reject(new Error(lang.t('errors:invalidfiletype'))) + return Promise.reject(new Error(wiki.lang.t('errors:invalidfiletype'))) } return true }).then(() => { @@ -94,12 +97,12 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => { }) }) -router.post('/file', lcdata.uploadFileHandler, (req, res, next) => { +router.post('/file', wiki.disk.uploadFileHandler, (req, res, next) => { let destFolder = _.chain(req.body.folder).trim().toLower().value() - upl.validateUploadsFolder(destFolder).then((destFolderPath) => { + wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => { if (!destFolderPath) { - res.json({ ok: false, msg: lang.t('errors:invalidfolder') }) + res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') }) return true } @@ -107,7 +110,7 @@ router.post('/file', lcdata.uploadFileHandler, (req, res, next) => { let destFilename = '' let destFilePath = '' - return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => { + return wiki.disk.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => { destFilename = fname destFilePath = path.resolve(destFolderPath, destFilename) @@ -150,7 +153,7 @@ router.get('/*', (req, res, next) => { // todo: Authentication-based access res.sendFile(fileName, { - root: git.getRepoPath() + '/uploads/', + root: wiki.git.getRepoPath() + '/uploads/', dotfiles: 'deny' }, (err) => { if (err) { diff --git a/server/index.js b/server/index.js index 07cc3df8..e74f5c2b 100644 --- a/server/index.js +++ b/server/index.js @@ -1,276 +1,43 @@ -'use strict' - // =========================================== // Wiki.js -// 1.0.0 // Licensed under AGPLv3 // =========================================== const path = require('path') -const ROOTPATH = process.cwd() -const SERVERPATH = path.join(ROOTPATH, 'server') +const cluster = require('cluster') -global.ROOTPATH = ROOTPATH -global.SERVERPATH = SERVERPATH -const IS_DEBUG = process.env.NODE_ENV === 'development' +let wiki = { + IS_DEBUG: process.env.NODE_ENV === 'development', + IS_MASTER: cluster.isMaster, + ROOTPATH: process.cwd(), + SERVERPATH: path.join(process.cwd(), 'server'), + configSvc: require('./modules/config'), + kernel: require('./modules/kernel') +} +global.wiki = wiki process.env.VIPS_WARNING = false -// if (IS_DEBUG) { +// if (wiki.IS_DEBUG) { // require('@glimpse/glimpse').init() // } -let appconf = require('./libs/config')() -global.appconfig = appconf.config -global.appdata = appconf.data +wiki.configSvc.init() // ---------------------------------------- -// Load Winston +// Init Logger // ---------------------------------------- -global.winston = require('./libs/logger')(IS_DEBUG, 'SERVER') -global.winston.info('Wiki.js is initializing...') +wiki.logger = require('./modules/logger').init() // ---------------------------------------- -// Load global modules +// Init DB // ---------------------------------------- -global.lcdata = require('./libs/local').init() -global.db = require('./libs/db').init() -global.entries = require('./libs/entries').init() -global.git = require('./libs/git').init(false) -global.lang = require('i18next') -global.mark = require('./libs/markdown') -global.search = require('./libs/search').init() -global.upl = require('./libs/uploads').init() +wiki.db = require('./modules/db').init() // ---------------------------------------- -// Load modules +// Start Kernel // ---------------------------------------- -const autoload = require('auto-load') -const bodyParser = require('body-parser') -const compression = require('compression') -const cookieParser = require('cookie-parser') -const express = require('express') -const favicon = require('serve-favicon') -const flash = require('connect-flash') -const fork = require('child_process').fork -const http = require('http') -const i18nBackend = require('i18next-node-fs-backend') -const passport = require('passport') -const passportSocketIo = require('passport.socketio') -const session = require('express-session') -const SessionMongoStore = require('connect-mongo')(session) -const graceful = require('node-graceful') -const socketio = require('socket.io') - -var mw = autoload(path.join(SERVERPATH, '/middlewares')) -var ctrl = autoload(path.join(SERVERPATH, '/controllers')) - -// ---------------------------------------- -// Define Express App -// ---------------------------------------- - -const app = express() -global.app = app -app.use(compression()) - -// ---------------------------------------- -// Security -// ---------------------------------------- - -app.use(mw.security) - -// ---------------------------------------- -// Public Assets -// ---------------------------------------- - -app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico'))) -app.use(express.static(path.join(ROOTPATH, 'assets'), { - index: false, - maxAge: '7d' -})) - -// ---------------------------------------- -// Passport Authentication -// ---------------------------------------- - -require('./libs/auth')(passport) -global.rights = require('./libs/rights') -global.rights.init() - -let sessionStore = new SessionMongoStore({ - mongooseConnection: global.db.connection, - touchAfter: 15 -}) - -app.use(cookieParser()) -app.use(session({ - name: 'wikijs.sid', - store: sessionStore, - secret: appconfig.sessionSecret, - resave: false, - saveUninitialized: false -})) -app.use(flash()) -app.use(passport.initialize()) -app.use(passport.session()) - -// ---------------------------------------- -// SEO -// ---------------------------------------- - -app.use(mw.seo) - -// ---------------------------------------- -// Localization Engine -// ---------------------------------------- - -global.lang - .use(i18nBackend) - .init({ - load: 'languageOnly', - ns: ['common', 'admin', 'auth', 'errors', 'git'], - defaultNS: 'common', - saveMissing: false, - preload: [appconfig.lang], - lng: appconfig.lang, - fallbackLng: 'en', - backend: { - loadPath: path.join(SERVERPATH, 'locales/{{lng}}/{{ns}}.json') - } - }) - -// ---------------------------------------- -// View Engine Setup -// ---------------------------------------- - -app.set('views', path.join(SERVERPATH, 'views')) -app.set('view engine', 'pug') - -app.use(bodyParser.json({ limit: '1mb' })) -app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' })) - -// ---------------------------------------- -// View accessible data -// ---------------------------------------- - -app.locals._ = require('lodash') -app.locals.t = global.lang.t.bind(global.lang) -app.locals.moment = require('moment') -app.locals.moment.locale(appconfig.lang) -app.locals.appconfig = appconfig -app.use(mw.flash) - -// ---------------------------------------- -// Controllers -// ---------------------------------------- - -app.use('/', ctrl.auth) - -app.use('/uploads', mw.auth, ctrl.uploads) -app.use('/admin', mw.auth, ctrl.admin) -app.use('/', mw.auth, ctrl.pages) - -// ---------------------------------------- -// Error handling -// ---------------------------------------- - -app.use(function (req, res, next) { - var err = new Error('Not Found') - err.status = 404 - next(err) -}) - -app.use(function (err, req, res, next) { - res.status(err.status || 500) - res.render('error', { - message: err.message, - error: IS_DEBUG ? err : {} - }) -}) - -// ---------------------------------------- -// Start HTTP server -// ---------------------------------------- - -global.winston.info('Starting HTTP/WS server on port ' + appconfig.port + '...') - -app.set('port', appconfig.port) -var server = http.createServer(app) -var io = socketio(server) - -server.listen(appconfig.port) -server.on('error', (error) => { - if (error.syscall !== 'listen') { - throw error - } - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - global.winston.error('Listening on port ' + appconfig.port + ' requires elevated privileges!') - return process.exit(1) - case 'EADDRINUSE': - global.winston.error('Port ' + appconfig.port + ' is already in use!') - return process.exit(1) - default: - throw error - } -}) - -server.on('listening', () => { - global.winston.info('HTTP/WS server started successfully! [RUNNING]') -}) - -// ---------------------------------------- -// WebSocket -// ---------------------------------------- - -io.use(passportSocketIo.authorize({ - key: 'wikijs.sid', - store: sessionStore, - secret: appconfig.sessionSecret, - cookieParser, - success: (data, accept) => { - accept() - }, - fail: (data, message, error, accept) => { - accept() - } -})) - -io.on('connection', ctrl.ws) - -// ---------------------------------------- -// Start child processes -// ---------------------------------------- - -let bgAgent = fork(path.join(SERVERPATH, 'agent.js')) - -bgAgent.on('message', m => { - if (!m.action) { - return - } - - switch (m.action) { - case 'searchAdd': - global.search.add(m.content) - break - } -}) - -// ---------------------------------------- -// Graceful shutdown -// ---------------------------------------- - -graceful.on('exit', () => { - global.winston.info('- SHUTTING DOWN - Terminating Background Agent...') - bgAgent.kill() - global.winston.info('- SHUTTING DOWN - Performing git sync...') - return global.git.resync().then(() => { - global.winston.info('- SHUTTING DOWN - Git sync successful. Now safe to exit.') - process.exit() - }) -}) +wiki.kernel.init() diff --git a/server/init.js b/server/init.js deleted file mode 100644 index 95b3ed52..00000000 --- a/server/init.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict' - -const Promise = require('bluebird') -const fs = Promise.promisifyAll(require('fs-extra')) -const pm2 = Promise.promisifyAll(require('pm2')) -const ora = require('ora') -const path = require('path') - -const ROOTPATH = process.cwd() - -module.exports = { - /** - * Detect the most appropriate start mode - */ - startDetect: function () { - if (process.env.WIKI_JS_HEROKU) { - return this.startInHerokuMode() - } else { - return this.startInBackgroundMode() - } - }, - /** - * Start in background mode - */ - startInBackgroundMode: function () { - let spinner = ora('Initializing...').start() - return fs.emptyDirAsync(path.join(ROOTPATH, './logs')).then(() => { - return pm2.connectAsync().then(() => { - return pm2.startAsync({ - name: 'wiki', - script: 'server', - cwd: ROOTPATH, - output: path.join(ROOTPATH, './logs/wiki-output.log'), - error: path.join(ROOTPATH, './logs/wiki-error.log'), - minUptime: 5000, - maxRestarts: 5 - }).then(() => { - spinner.succeed('Wiki.js has started successfully.') - }).finally(() => { - pm2.disconnect() - }) - }) - }).catch(err => { - spinner.fail(err) - process.exit(1) - }) - }, - /** - * Start in Heroku mode - */ - startInHerokuMode: function () { - console.warn('Incorrect command on Heroku, use instead: node server') - process.exit(1) - }, - /** - * Stop Wiki.js process(es) - */ - stop () { - let spinner = ora('Shutting down Wiki.js...').start() - return pm2.connectAsync().then(() => { - return pm2.stopAsync('wiki').then(() => { - spinner.succeed('Wiki.js has stopped successfully.') - }).finally(() => { - pm2.disconnect() - }) - }).catch(err => { - spinner.fail(err) - process.exit(1) - }) - }, - /** - * Restart Wiki.js process(es) - */ - restart: function () { - let self = this - return self.stop().delay(1000).then(() => { - self.startDetect() - }) - }, - /** - * Start the web-based configuration wizard - * - * @param {Number} port Port to bind the HTTP server on - */ - configure (port) { - port = port || 3000 - let spinner = ora('Initializing interactive setup...').start() - require('./configure')(port, spinner) - } -} diff --git a/server/libs/auth.js b/server/libs/auth.js deleted file mode 100644 index 34271ccc..00000000 --- a/server/libs/auth.js +++ /dev/null @@ -1,261 +0,0 @@ -'use strict' - -/* global appconfig, appdata, db, lang, winston */ - -const fs = require('fs') - -module.exports = function (passport) { - // Serialization user methods - - passport.serializeUser(function (user, done) { - done(null, user._id) - }) - - passport.deserializeUser(function (id, done) { - db.User.findById(id).then((user) => { - if (user) { - done(null, user) - } else { - done(new Error(lang.t('auth:errors:usernotfound')), null) - } - return true - }).catch((err) => { - done(err, null) - }) - }) - - // Local Account - - if (appconfig.auth.local && appconfig.auth.local.enabled) { - const LocalStrategy = require('passport-local').Strategy - passport.use('local', - new LocalStrategy({ - usernameField: 'email', - passwordField: 'password' - }, (uEmail, uPassword, done) => { - db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => { - if (user) { - return user.validatePassword(uPassword).then(() => { - return done(null, user) || true - }).catch((err) => { - return done(err, null) - }) - } else { - return done(new Error('INVALID_LOGIN'), null) - } - }).catch((err) => { - done(err, null) - }) - } - )) - } - - // Google ID - - if (appconfig.auth.google && appconfig.auth.google.enabled) { - const GoogleStrategy = require('passport-google-oauth20').Strategy - passport.use('google', - new GoogleStrategy({ - clientID: appconfig.auth.google.clientId, - clientSecret: appconfig.auth.google.clientSecret, - callbackURL: appconfig.host + '/login/google/callback' - }, (accessToken, refreshToken, profile, cb) => { - db.User.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) - } - )) - } - - // Microsoft Accounts - - if (appconfig.auth.microsoft && appconfig.auth.microsoft.enabled) { - const WindowsLiveStrategy = require('passport-windowslive').Strategy - passport.use('windowslive', - new WindowsLiveStrategy({ - clientID: appconfig.auth.microsoft.clientId, - clientSecret: appconfig.auth.microsoft.clientSecret, - callbackURL: appconfig.host + '/login/ms/callback' - }, function (accessToken, refreshToken, profile, cb) { - db.User.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) - } - )) - } - - // Facebook - - if (appconfig.auth.facebook && appconfig.auth.facebook.enabled) { - const FacebookStrategy = require('passport-facebook').Strategy - passport.use('facebook', - new FacebookStrategy({ - clientID: appconfig.auth.facebook.clientId, - clientSecret: appconfig.auth.facebook.clientSecret, - callbackURL: appconfig.host + '/login/facebook/callback', - profileFields: ['id', 'displayName', 'email'] - }, function (accessToken, refreshToken, profile, cb) { - db.User.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) - } - )) - } - - // GitHub - - if (appconfig.auth.github && appconfig.auth.github.enabled) { - const GitHubStrategy = require('passport-github2').Strategy - passport.use('github', - new GitHubStrategy({ - clientID: appconfig.auth.github.clientId, - clientSecret: appconfig.auth.github.clientSecret, - callbackURL: appconfig.host + '/login/github/callback', - scope: ['user:email'] - }, (accessToken, refreshToken, profile, cb) => { - db.User.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) - } - )) - } - - // Slack - - if (appconfig.auth.slack && appconfig.auth.slack.enabled) { - const SlackStrategy = require('passport-slack').Strategy - passport.use('slack', - new SlackStrategy({ - clientID: appconfig.auth.slack.clientId, - clientSecret: appconfig.auth.slack.clientSecret, - callbackURL: appconfig.host + '/login/slack/callback' - }, (accessToken, refreshToken, profile, cb) => { - db.User.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) - } - )) - } - - // LDAP - - if (appconfig.auth.ldap && appconfig.auth.ldap.enabled) { - const LdapStrategy = require('passport-ldapauth').Strategy - passport.use('ldapauth', - new LdapStrategy({ - server: { - url: appconfig.auth.ldap.url, - bindDn: appconfig.auth.ldap.bindDn, - bindCredentials: appconfig.auth.ldap.bindCredentials, - searchBase: appconfig.auth.ldap.searchBase, - searchFilter: appconfig.auth.ldap.searchFilter, - searchAttributes: ['displayName', 'name', 'cn', 'mail'], - tlsOptions: (appconfig.auth.ldap.tlsEnabled) ? { - ca: [ - fs.readFileSync(appconfig.auth.ldap.tlsCertPath) - ] - } : {} - }, - usernameField: 'email', - passReqToCallback: false - }, (profile, cb) => { - profile.provider = 'ldap' - profile.id = profile.dn - db.User.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) - } - )) - } - - // AZURE AD - - if (appconfig.auth.azure && appconfig.auth.azure.enabled) { - const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy - const jwt = require('jsonwebtoken') - passport.use('azure_ad_oauth2', - new AzureAdOAuth2Strategy({ - clientID: appconfig.auth.azure.clientId, - clientSecret: appconfig.auth.azure.clientSecret, - callbackURL: appconfig.host + '/login/azure/callback', - resource: appconfig.auth.azure.resource, - tenant: appconfig.auth.azure.tenant - }, (accessToken, refreshToken, params, profile, cb) => { - let waadProfile = jwt.decode(params.id_token) - waadProfile.id = waadProfile.oid - waadProfile.provider = 'azure' - db.User.processProfile(waadProfile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) - } - )) - } - - // Create users for first-time - - db.onReady.then(() => { - return db.User.findOne({ provider: 'local', email: 'guest' }).then((c) => { - if (c < 1) { - // Create guest account - - return db.User.create({ - provider: 'local', - email: 'guest', - name: 'Guest', - password: '', - rights: [{ - role: 'read', - path: '/', - exact: false, - deny: !appconfig.public - }] - }).then(() => { - winston.info('[AUTH] Guest account created successfully!') - }).catch((err) => { - winston.error('[AUTH] An error occured while creating guest account:') - winston.error(err) - }) - } - }).then(() => { - if (process.env.WIKI_JS_HEROKU) { - return db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => { - if (c < 1) { - // Create root admin account (HEROKU ONLY) - - return db.User.create({ - provider: 'local', - email: process.env.WIKI_ADMIN_EMAIL, - name: 'Administrator', - password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default) - rights: [{ - role: 'admin', - path: '/', - exact: false, - deny: false - }] - }).then(() => { - winston.info('[AUTH] Root admin account created successfully!') - }).catch((err) => { - winston.error('[AUTH] An error occured while creating root admin account:') - winston.error(err) - }) - } else { return true } - }) - } else { return true } - }) - }) -} diff --git a/server/libs/config.js b/server/libs/config.js deleted file mode 100644 index 810af0d7..00000000 --- a/server/libs/config.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict' - -const fs = require('fs') -const yaml = require('js-yaml') -const _ = require('lodash') -const path = require('path') -const cfgHelper = require('../helpers/config') - -/** - * Load Application Configuration - * - * @param {Object} confPaths Path to the configuration files - * @return {Object} Application Configuration - */ -module.exports = (confPaths) => { - confPaths = _.defaults(confPaths, { - config: path.join(ROOTPATH, 'config.yml'), - data: path.join(SERVERPATH, 'app/data.yml'), - dataRegex: path.join(SERVERPATH, 'app/regex.js') - }) - - let appconfig = {} - let appdata = {} - - try { - appconfig = yaml.safeLoad( - cfgHelper.parseConfigValue( - fs.readFileSync(confPaths.config, 'utf8') - ) - ) - - appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8')) - appdata.regex = require(confPaths.dataRegex) - } catch (ex) { - console.error(ex) - process.exit(1) - } - - // Merge with defaults - - appconfig = _.defaultsDeep(appconfig, appdata.defaults.config) - - // Check port - - if (appconfig.port < 1) { - appconfig.port = process.env.PORT || 80 - } - - // Convert booleans - - appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true') - - // List authentication strategies - - appconfig.authStrategies = { - list: _.filter(appconfig.auth, ['enabled', true]), - socialEnabled: (_.chain(appconfig.auth).omit(['local', 'ldap']).filter(['enabled', true]).value().length > 0) - } - if (appconfig.authStrategies.list.length < 1) { - console.error(new Error('You must enable at least 1 authentication strategy!')) - process.exit(1) - } - - return { - config: appconfig, - data: appdata - } -} diff --git a/server/libs/db.js b/server/libs/db.js deleted file mode 100644 index 733d28a2..00000000 --- a/server/libs/db.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict' - -/* global ROOTPATH, appconfig, winston */ - -const modb = require('mongoose') -const fs = require('fs') -const path = require('path') -const _ = require('lodash') - -/** - * MongoDB module - * - * @return {Object} MongoDB wrapper instance - */ -module.exports = { - - /** - * Initialize DB - * - * @return {Object} DB instance - */ - init() { - let self = this - - let dbModelsPath = path.join(SERVERPATH, 'models') - - modb.Promise = require('bluebird') - - // Event handlers - - modb.connection.on('error', err => { - winston.error('Failed to connect to MongoDB instance.') - return err - }) - modb.connection.once('open', function () { - winston.log('Connected to MongoDB instance.') - }) - - // Store connection handle - - self.connection = modb.connection - self.ObjectId = modb.Types.ObjectId - - // Load DB Models - - fs - .readdirSync(dbModelsPath) - .filter(function (file) { - return (file.indexOf('.') !== 0) - }) - .forEach(function (file) { - let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0])) - self[modelName] = require(path.join(dbModelsPath, file)) - }) - - // Connect - - self.onReady = modb.connect(appconfig.db, { useMongoClient: true }) - - return self - } - -} diff --git a/server/libs/logger.js b/server/libs/logger.js deleted file mode 100644 index 4a6f7884..00000000 --- a/server/libs/logger.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict' - -module.exports = (isDebug, processName) => { - let winston = require('winston') - - if (typeof processName === 'undefined') { - processName = 'SERVER' - } - - // Console - - let logger = new (winston.Logger)({ - level: (isDebug) ? 'debug' : 'info', - transports: [ - new (winston.transports.Console)({ - level: (isDebug) ? 'debug' : 'info', - prettyPrint: true, - colorize: true, - silent: false, - timestamp: true - }) - ] - }) - - logger.filters.push((level, msg) => { - return '[' + processName + '] ' + msg - }) - - // External services - - if (appconfig.externalLogging.bugsnag) { - const bugsnagTransport = require('./winston-transports/bugsnag') - logger.add(bugsnagTransport, { - level: 'warn', - key: appconfig.externalLogging.bugsnag - }) - } - - if (appconfig.externalLogging.loggly) { - require('winston-loggly-bulk') - logger.add(winston.transports.Loggly, { - token: appconfig.externalLogging.loggly.token, - subdomain: appconfig.externalLogging.loggly.subdomain, - tags: ['wiki-js'], - level: 'warn', - json: true - }) - } - - if (appconfig.externalLogging.papertrail) { - require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions - logger.add(winston.transports.Papertrail, { - host: appconfig.externalLogging.papertrail.host, - port: appconfig.externalLogging.papertrail.port, - level: 'warn', - program: 'wiki.js' - }) - } - - if (appconfig.externalLogging.rollbar) { - const rollbarTransport = require('./winston-transports/rollbar') - logger.add(rollbarTransport, { - level: 'warn', - key: appconfig.externalLogging.rollbar - }) - } - - if (appconfig.externalLogging.sentry) { - const sentryTransport = require('./winston-transports/sentry') - logger.add(sentryTransport, { - level: 'warn', - key: appconfig.externalLogging.sentry - }) - } - - return logger -} diff --git a/server/libs/search-index/index.js b/server/libs/search-index/index.js deleted file mode 100644 index 807219d9..00000000 --- a/server/libs/search-index/index.js +++ /dev/null @@ -1,81 +0,0 @@ -const bunyan = require('bunyan') -const level = require('levelup') -const down = require('memdown') -const SearchIndexAdder = require('search-index-adder') -const SearchIndexSearcher = require('search-index-searcher') - -module.exports = function (givenOptions, moduleReady) { - const optionsLoaded = function (err, SearchIndex) { - const siUtil = require('./siUtil.js')(SearchIndex.options) - if (err) return moduleReady(err) - SearchIndex.close = siUtil.close - SearchIndex.countDocs = siUtil.countDocs - getAdder(SearchIndex, adderLoaded) - } - - const adderLoaded = function (err, SearchIndex) { - if (err) return moduleReady(err) - getSearcher(SearchIndex, searcherLoaded) - } - - const searcherLoaded = function (err, SearchIndex) { - if (err) return moduleReady(err) - return moduleReady(err, SearchIndex) - } - - getOptions(givenOptions, optionsLoaded) -} - -const getAdder = function (SearchIndex, done) { - SearchIndexAdder(SearchIndex.options, function (err, searchIndexAdder) { - SearchIndex.add = searchIndexAdder.add - SearchIndex.callbackyAdd = searchIndexAdder.concurrentAdd // deprecated - SearchIndex.concurrentAdd = searchIndexAdder.concurrentAdd - SearchIndex.createWriteStream = searchIndexAdder.createWriteStream - SearchIndex.dbWriteStream = searchIndexAdder.dbWriteStream - SearchIndex.defaultPipeline = searchIndexAdder.defaultPipeline - SearchIndex.del = searchIndexAdder.deleter - SearchIndex.deleteStream = searchIndexAdder.deleteStream - SearchIndex.flush = searchIndexAdder.flush - done(err, SearchIndex) - }) -} - -const getSearcher = function (SearchIndex, done) { - SearchIndexSearcher(SearchIndex.options, function (err, searchIndexSearcher) { - SearchIndex.availableFields = searchIndexSearcher.availableFields - SearchIndex.buckets = searchIndexSearcher.bucketStream - SearchIndex.categorize = searchIndexSearcher.categoryStream - SearchIndex.dbReadStream = searchIndexSearcher.dbReadStream - SearchIndex.get = searchIndexSearcher.get - SearchIndex.match = searchIndexSearcher.match - SearchIndex.scan = searchIndexSearcher.scan - SearchIndex.search = searchIndexSearcher.search - SearchIndex.totalHits = searchIndexSearcher.totalHits - done(err, SearchIndex) - }) -} - -const getOptions = function (options, done) { - var SearchIndex = {} - SearchIndex.options = Object.assign({}, { - indexPath: 'si', - keySeparator: '○', - logLevel: 'error' - }, options) - options.log = bunyan.createLogger({ - name: 'search-index', - level: options.logLevel - }) - if (!options.indexes) { - level(SearchIndex.options.indexPath || 'si', { - valueEncoding: 'json', - db: down - }, function (err, db) { - SearchIndex.options.indexes = db - return done(err, SearchIndex) - }) - } else { - return done(null, SearchIndex) - } -} diff --git a/server/libs/search-index/siUtil.js b/server/libs/search-index/siUtil.js deleted file mode 100644 index bad264a7..00000000 --- a/server/libs/search-index/siUtil.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' - -module.exports = function (siOptions) { - var siUtil = {} - - siUtil.countDocs = function (callback) { - var count = 0 - const gte = 'DOCUMENT' + siOptions.keySeparator - const lte = 'DOCUMENT' + siOptions.keySeparator + siOptions.keySeparator - siOptions.indexes.createReadStream({gte: gte, lte: lte}) - .on('data', function (data) { - count++ - }) - .on('error', function (err) { - return callback(err, null) - }) - .on('end', function () { - return callback(null, count) - }) - } - - siUtil.close = function (callback) { - siOptions.indexes.close(function (err) { - while (!siOptions.indexes.isClosed()) { - // log not always working here- investigate - if (siOptions.log) siOptions.log.info('closing...') - } - if (siOptions.indexes.isClosed()) { - if (siOptions.log) siOptions.log.info('closed...') - callback(err) - } - }) - } - - return siUtil -} diff --git a/server/locales/ja/auth.json b/server/locales/ja/auth.json index eade4c28..e4218ead 100644 --- a/server/locales/ja/auth.json +++ b/server/locales/ja/auth.json @@ -13,7 +13,7 @@ "invalidlogin": "不正なログイン", "invalidloginmsg": "Eメール又はパスワードが無効です。", "invaliduseremail": "無効なユーザーEメール", - "lognerror": "ログインエラー", + "loginerror": "ログインエラー", "notyetauthorized": "まだこのサイトにログインする権限がありません。", "toomanyattempts": "試行回数が多すぎます", "toomanyattemptsmsg": "短期間に失敗した試行回数が多すぎます。{{time}}にもう一度お試しください。", diff --git a/server/master.js b/server/master.js new file mode 100644 index 00000000..4d8a450c --- /dev/null +++ b/server/master.js @@ -0,0 +1,186 @@ +/* global wiki */ + +module.exports = () => { + // ---------------------------------------- + // Load global modules + // ---------------------------------------- + + wiki.auth = require('./modules/auth').init() + wiki.disk = require('./modules/disk').init() + wiki.docs = require('./modules/documents').init() + wiki.git = require('./modules/git').init(false) + wiki.lang = require('./modules/localization').init() + wiki.mark = require('./modules/markdown') + wiki.search = require('./modules/search').init() + wiki.upl = require('./modules/uploads').init() + + // ---------------------------------------- + // Load modules + // ---------------------------------------- + + const autoload = require('auto-load') + const bodyParser = require('body-parser') + const compression = require('compression') + const cookieParser = require('cookie-parser') + const express = require('express') + const favicon = require('serve-favicon') + const flash = require('connect-flash') + const http = require('http') + 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') + + var mw = autoload(path.join(wiki.SERVERPATH, '/middlewares')) + var ctrl = autoload(path.join(wiki.SERVERPATH, '/controllers')) + + // ---------------------------------------- + // Define Express App + // ---------------------------------------- + + const app = express() + wiki.app = app + app.use(compression()) + + // ---------------------------------------- + // Security + // ---------------------------------------- + + app.use(mw.security) + + // ---------------------------------------- + // Public Assets + // ---------------------------------------- + + app.use(favicon(path.join(wiki.ROOTPATH, 'assets', 'favicon.ico'))) + app.use(express.static(path.join(wiki.ROOTPATH, 'assets'), { + index: false, + maxAge: '7d' + })) + + // ---------------------------------------- + // Passport Authentication + // ---------------------------------------- + + let sessionStore = new SessionRedisStore({ + client: wiki.redis + }) + + app.use(cookieParser()) + app.use(session({ + name: 'wikijs.sid', + store: sessionStore, + secret: wiki.config.site.sessionSecret, + resave: false, + saveUninitialized: false + })) + app.use(flash()) + app.use(wiki.auth.passport.initialize()) + app.use(wiki.auth.passport.session()) + + // ---------------------------------------- + // SEO + // ---------------------------------------- + + app.use(mw.seo) + + // ---------------------------------------- + // View Engine Setup + // ---------------------------------------- + + app.set('views', path.join(wiki.SERVERPATH, 'views')) + app.set('view engine', 'pug') + + app.use(bodyParser.json({ limit: '1mb' })) + app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' })) + + // ---------------------------------------- + // View accessible data + // ---------------------------------------- + + app.locals.basedir = wiki.ROOTPATH + app.locals._ = require('lodash') + app.locals.t = wiki.lang.engine.t.bind(wiki.lang) + app.locals.moment = require('moment') + app.locals.moment.locale(wiki.config.site.lang) + app.locals.config = wiki.config + app.use(mw.flash) + + // ---------------------------------------- + // Controllers + // ---------------------------------------- + + app.use('/', ctrl.auth) + + app.use('/graphql', graphqlApollo.graphqlExpress({ schema: graphqlSchema })) + 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) + + // ---------------------------------------- + // Error handling + // ---------------------------------------- + + app.use(function (req, res, next) { + var err = new Error('Not Found') + err.status = 404 + next(err) + }) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.render('error', { + message: err.message, + error: wiki.IS_DEBUG ? err : {} + }) + }) + + // ---------------------------------------- + // Start HTTP server + // ---------------------------------------- + + wiki.logger.info(`HTTP Server on port: ${wiki.config.port}`) + + app.set('port', wiki.config.port) + let server = http.createServer(app) + + server.listen(wiki.config.port) + server.on('error', (error) => { + if (error.syscall !== 'listen') { + throw error + } + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + wiki.logger.error('Listening on port ' + wiki.config.port + ' requires elevated privileges!') + return process.exit(1) + case 'EADDRINUSE': + wiki.logger.error('Port ' + wiki.config.port + ' is already in use!') + return process.exit(1) + default: + throw error + } + }) + + server.on('listening', () => { + 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/middlewares/flash.js b/server/middlewares/flash.js index 98182b47..09e4adf0 100644 --- a/server/middlewares/flash.js +++ b/server/middlewares/flash.js @@ -9,7 +9,7 @@ * @return {any} void */ module.exports = (req, res, next) => { - res.locals.appflash = req.flash('alert') + res.locals.flash = req.flash('alert') next() } diff --git a/server/middlewares/security.js b/server/middlewares/security.js index 3590cff4..0fe2b355 100644 --- a/server/middlewares/security.js +++ b/server/middlewares/security.js @@ -1,7 +1,5 @@ 'use strict' -/* global app */ - /** * Security Middleware * @@ -12,7 +10,7 @@ */ module.exports = function (req, res, next) { // -> Disable X-Powered-By - app.disable('x-powered-by') + req.app.disable('x-powered-by') // -> Disable Frame Embedding res.set('X-Frame-Options', 'deny') diff --git a/server/models/_relations.js b/server/models/_relations.js new file mode 100644 index 00000000..c826a5e0 --- /dev/null +++ b/server/models/_relations.js @@ -0,0 +1,16 @@ +/** + * Associate DB Model relations + */ +module.exports = db => { + db.User.belongsToMany(db.Group, { through: 'userGroups' }) + db.Group.belongsToMany(db.User, { through: 'userGroups' }) + db.Group.hasMany(db.Right) + db.Right.belongsTo(db.Group) + db.Document.belongsToMany(db.Tag, { through: 'documentTags' }) + db.Document.hasMany(db.Comment) + db.Tag.belongsToMany(db.Document, { through: 'documentTags' }) + db.File.belongsTo(db.Folder) + db.Folder.hasMany(db.File) + db.Comment.belongsTo(db.Document) + db.Comment.belongsTo(db.User, { as: 'author' }) +} diff --git a/server/models/bruteforce.js b/server/models/bruteforce.js deleted file mode 100644 index 76981fd7..00000000 --- a/server/models/bruteforce.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const Mongoose = require('mongoose') - -/** - * BruteForce schema - * - * @type {} - */ -var bruteForceSchema = Mongoose.Schema({ - _id: { type: String, index: 1 }, - data: { - count: Number, - lastRequest: Date, - firstRequest: Date - }, - expires: { type: Date, index: { expires: '1d' } } -}) - -module.exports = Mongoose.model('Bruteforce', bruteForceSchema) diff --git a/server/models/comment.js b/server/models/comment.js new file mode 100644 index 00000000..3b35f42c --- /dev/null +++ b/server/models/comment.js @@ -0,0 +1,16 @@ +/** + * Comment schema + */ +module.exports = (sequelize, DataTypes) => { + let commentSchema = sequelize.define('comment', { + content: { + type: DataTypes.STRING, + allowNull: false + } + }, { + timestamps: true, + version: true + }) + + return commentSchema +} diff --git a/server/models/document.js b/server/models/document.js new file mode 100644 index 00000000..957fd91a --- /dev/null +++ b/server/models/document.js @@ -0,0 +1,64 @@ +/** + * Document schema + */ +module.exports = (sequelize, DataTypes) => { + let documentSchema = sequelize.define('setting', { + path: { + type: DataTypes.STRING, + allowNull: false + }, + title: { + type: DataTypes.STRING, + allowNull: false, + validate: { + len: [2, 255] + } + }, + subtitle: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: '' + }, + parentPath: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: '' + }, + parentTitle: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: '' + }, + isDirectory: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + isEntry: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + isDraft: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + searchContent: { + type: DataTypes.TEXT, + allowNull: true, + defaultValue: '' + } + }, { + timestamps: true, + version: true, + indexes: [ + { + unique: true, + fields: ['path'] + } + ] + }) + + return documentSchema +} diff --git a/server/models/entry.js b/server/models/entry.js deleted file mode 100644 index 746d6d28..00000000 --- a/server/models/entry.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' - -const Mongoose = require('mongoose') - -/** - * Entry schema - * - * @type {} - */ -var entrySchema = Mongoose.Schema({ - _id: String, - - title: { - type: String, - required: true, - minlength: 2 - }, - subtitle: { - type: String, - default: '' - }, - parentTitle: { - type: String, - default: '' - }, - parentPath: { - type: String, - default: '' - }, - isDirectory: { - type: Boolean, - default: false - }, - isEntry: { - type: Boolean, - default: false - } -}, { - timestamps: {} -}) - -module.exports = Mongoose.model('Entry', entrySchema) diff --git a/server/models/file.js b/server/models/file.js new file mode 100644 index 00000000..9fca9df4 --- /dev/null +++ b/server/models/file.js @@ -0,0 +1,42 @@ +/** + * File schema + */ +module.exports = (sequelize, DataTypes) => { + let fileSchema = sequelize.define('file', { + category: { + type: DataTypes.ENUM('binary', 'image'), + allowNull: false, + defaultValue: 'binary' + }, + mime: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: 'application/octet-stream' + }, + extra: { + type: DataTypes.JSONB, + allowNull: true + }, + filename: { + type: DataTypes.STRING, + allowNull: false + }, + basename: { + type: DataTypes.STRING, + allowNull: false + }, + filesize: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + isInt: true, + min: 0 + } + } + }, { + timestamps: true, + version: true + }) + + return fileSchema +} diff --git a/server/models/folder.js b/server/models/folder.js new file mode 100644 index 00000000..fdab4f7d --- /dev/null +++ b/server/models/folder.js @@ -0,0 +1,22 @@ +/** + * Folder schema + */ +module.exports = (sequelize, DataTypes) => { + let folderSchema = sequelize.define('folder', { + name: { + type: DataTypes.STRING, + allowNull: false + } + }, { + timestamps: true, + version: true, + indexes: [ + { + unique: true, + fields: ['name'] + } + ] + }) + + return folderSchema +} diff --git a/server/models/group.js b/server/models/group.js new file mode 100644 index 00000000..942ed07e --- /dev/null +++ b/server/models/group.js @@ -0,0 +1,16 @@ +/** + * Group schema + */ +module.exports = (sequelize, DataTypes) => { + let groupSchema = sequelize.define('group', { + name: { + type: DataTypes.STRING, + allowNull: false + } + }, { + timestamps: true, + version: true + }) + + return groupSchema +} diff --git a/server/models/right.js b/server/models/right.js new file mode 100644 index 00000000..eeed3155 --- /dev/null +++ b/server/models/right.js @@ -0,0 +1,36 @@ +/** + * Right schema + */ +module.exports = (sequelize, DataTypes) => { + let rightSchema = sequelize.define('right', { + path: { + type: DataTypes.STRING, + allowNull: false + }, + role: { + type: DataTypes.ENUM('read', 'write', 'manage'), + allowNull: false, + defaultValue: 'read' + }, + exact: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + allow: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + } + }, { + timestamps: true, + version: true, + indexes: [ + { + fields: ['path'] + } + ] + }) + + return rightSchema +} diff --git a/server/models/setting.js b/server/models/setting.js index 73686af2..9759b7c3 100644 --- a/server/models/setting.js +++ b/server/models/setting.js @@ -1,22 +1,26 @@ -'use strict' - -const Mongoose = require('mongoose') - /** * Settings schema - * - * @type {} */ -var settingSchema = Mongoose.Schema({ - key: { - type: String, - required: true, - index: true - }, - value: { - type: String, - required: true - } -}, { timestamps: {} }) +module.exports = (sequelize, DataTypes) => { + let settingSchema = sequelize.define('setting', { + key: { + type: DataTypes.STRING, + allowNull: false + }, + config: { + type: DataTypes.JSONB, + allowNull: false + } + }, { + timestamps: true, + version: true, + indexes: [ + { + unique: true, + fields: ['key'] + } + ] + }) -module.exports = Mongoose.model('Setting', settingSchema) + return settingSchema +} diff --git a/server/models/tag.js b/server/models/tag.js new file mode 100644 index 00000000..6c44859d --- /dev/null +++ b/server/models/tag.js @@ -0,0 +1,22 @@ +/** + * Tags schema + */ +module.exports = (sequelize, DataTypes) => { + let tagSchema = sequelize.define('tag', { + key: { + type: DataTypes.STRING, + allowNull: false + } + }, { + timestamps: true, + version: true, + indexes: [ + { + unique: true, + fields: ['key'] + } + ] + }) + + return tagSchema +} diff --git a/server/models/upl-file.js b/server/models/upl-file.js deleted file mode 100644 index a48602bb..00000000 --- a/server/models/upl-file.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' - -const Mongoose = require('mongoose') - -/** - * Upload File schema - * - * @type {} - */ -var uplFileSchema = Mongoose.Schema({ - - _id: String, - - category: { - type: String, - required: true, - default: 'binary' - }, - mime: { - type: String, - required: true, - default: 'application/octet-stream' - }, - extra: { - type: Object - }, - folder: { - type: String, - ref: 'UplFolder' - }, - filename: { - type: String, - required: true - }, - basename: { - type: String, - required: true - }, - filesize: { - type: Number, - required: true - } - -}, { timestamps: {} }) - -module.exports = Mongoose.model('UplFile', uplFileSchema) diff --git a/server/models/upl-folder.js b/server/models/upl-folder.js deleted file mode 100644 index 80028b6f..00000000 --- a/server/models/upl-folder.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict' - -const Mongoose = require('mongoose') - -/** - * Upload Folder schema - * - * @type {} - */ -var uplFolderSchema = Mongoose.Schema({ - - _id: String, - - name: { - type: String, - index: true - } - -}, { timestamps: {} }) - -module.exports = Mongoose.model('UplFolder', uplFolderSchema) diff --git a/server/models/user.js b/server/models/user.js index 18b2e429..c9102ac7 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,109 +1,120 @@ -'use strict' +/* global wiki */ -/* global db, lang */ - -const Mongoose = require('mongoose') const Promise = require('bluebird') const bcrypt = require('bcryptjs-then') const _ = require('lodash') /** * Users schema - * - * @type {} */ -var userSchema = Mongoose.Schema({ +module.exports = (sequelize, DataTypes) => { + let userSchema = sequelize.define('user', { + email: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isEmail: true + } + }, + provider: { + type: DataTypes.STRING, + allowNull: false + }, + providerId: { + type: DataTypes.STRING, + allowNull: true + }, + password: { + type: DataTypes.STRING, + allowNull: true + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + role: { + type: DataTypes.ENUM('admin', 'user', 'guest'), + allowNull: false + }, + tfaIsActive: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + tfaSecret: { + type: DataTypes.STRING, + allowNull: true + } + }, { + timestamps: true, + version: true, + indexes: [ + { + unique: true, + fields: ['provider', 'email'] + } + ] + }) - email: { - type: String, - required: true, - index: true - }, - - provider: { - type: String, - required: true - }, - - providerId: { - type: String - }, - - password: { - type: String - }, - - name: { - type: String - }, - - rights: [{ - role: String, - path: String, - exact: Boolean, - deny: Boolean - }] - -}, { timestamps: {} }) - -userSchema.statics.processProfile = (profile) => { - let primaryEmail = '' - if (_.isArray(profile.emails)) { - let e = _.find(profile.emails, ['primary', true]) - primaryEmail = (e) ? e.value : _.first(profile.emails).value - } else if (_.isString(profile.email) && profile.email.length > 5) { - primaryEmail = profile.email - } else if (_.isString(profile.mail) && profile.mail.length > 5) { - primaryEmail = profile.mail - } else if (profile.user && profile.user.email && profile.user.email.length > 5) { - primaryEmail = profile.user.email - } else { - return Promise.reject(new Error(lang.t('auth:errors.invaliduseremail'))) + userSchema.prototype.validatePassword = function (rawPwd) { + return bcrypt.compare(rawPwd, this.password).then((isValid) => { + return (isValid) ? true : Promise.reject(new Error(wiki.lang.t('auth:errors:invalidlogin'))) + }) } - profile.provider = _.lowerCase(profile.provider) - primaryEmail = _.toLower(primaryEmail) - - return db.User.findOneAndUpdate({ - email: primaryEmail, - provider: profile.provider - }, { - email: primaryEmail, - provider: profile.provider, - providerId: profile.id, - name: profile.displayName || profile.cn || _.split(primaryEmail, '@')[0] - }, { - new: true - }).then((user) => { - // Handle unregistered accounts - if (!user && profile.provider !== 'local' && (appconfig.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) { - let nUsr = { - email: primaryEmail, - provider: profile.provider, - providerId: profile.id, - password: '', - name: profile.displayName || profile.name || profile.cn, - rights: [{ - role: 'read', - path: '/', - exact: false, - deny: false - }] - } - return db.User.create(nUsr) + userSchema.processProfile = (profile) => { + let primaryEmail = '' + if (_.isArray(profile.emails)) { + let e = _.find(profile.emails, ['primary', true]) + primaryEmail = (e) ? e.value : _.first(profile.emails).value + } else if (_.isString(profile.email) && profile.email.length > 5) { + primaryEmail = profile.email + } else if (_.isString(profile.mail) && profile.mail.length > 5) { + primaryEmail = profile.mail + } else if (profile.user && profile.user.email && profile.user.email.length > 5) { + primaryEmail = profile.user.email + } else { + return Promise.reject(new Error(wiki.lang.t('auth:errors.invaliduseremail'))) } - return user || Promise.reject(new Error(lang.t('auth:errors:notyetauthorized'))) - }) -} -userSchema.statics.hashPassword = (rawPwd) => { - return bcrypt.hash(rawPwd) -} + profile.provider = _.lowerCase(profile.provider) + primaryEmail = _.toLower(primaryEmail) -userSchema.methods.validatePassword = function (rawPwd) { - return bcrypt.compare(rawPwd, this.password).then((isValid) => { - return (isValid) ? true : Promise.reject(new Error(lang.t('auth:errors:invalidlogin'))) - }) -} + return wiki.db.User.findOneAndUpdate({ + email: primaryEmail, + provider: profile.provider + }, { + email: primaryEmail, + provider: profile.provider, + providerId: profile.id, + name: profile.displayName || _.split(primaryEmail, '@')[0] + }, { + new: true + }).then((user) => { + // Handle unregistered accounts + if (!user && profile.provider !== 'local' && (appconfig.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) { + let nUsr = { + email: primaryEmail, + provider: profile.provider, + providerId: profile.id, + password: '', + name: profile.displayName || profile.name || profile.cn, + rights: [{ + role: 'read', + path: '/', + exact: false, + deny: false + }] + } + return wiki.db.User.create(nUsr) + } + return user || Promise.reject(new Error(wiki.lang.t('auth:errors:notyetauthorized'))) + }) + } -module.exports = Mongoose.model('User', userSchema) + userSchema.hashPassword = (rawPwd) => { + return bcrypt.hash(rawPwd) + } + + return userSchema +} diff --git a/server/modules/auth.js b/server/modules/auth.js new file mode 100644 index 00000000..690d0971 --- /dev/null +++ b/server/modules/auth.js @@ -0,0 +1,106 @@ +/* global wiki */ + +const _ = require('lodash') +const passport = require('passport') +const fs = require('fs-extra') +const path = require('path') + +module.exports = { + strategies: {}, + init() { + this.passport = passport + + // Serialization user methods + + passport.serializeUser(function (user, done) { + done(null, user._id) + }) + + passport.deserializeUser(function (id, done) { + wiki.db.User.findById(id).then((user) => { + if (user) { + done(null, user) + } else { + done(new Error(wiki.lang.t('auth:errors:usernotfound')), null) + } + return true + }).catch((err) => { + done(err, null) + }) + }) + + // Load authentication strategies + + wiki.config.auth.strategies.local = {} + + _.forOwn(wiki.config.auth.strategies, (strategyConfig, strategyKey) => { + strategyConfig.callbackURL = `${wiki.config.site.host}${wiki.config.site.path}/login/${strategyKey}/callback` + let strategy = require(`../authentication/${strategyKey}`) + strategy.init(passport, strategyConfig) + fs.readFile(path.join(wiki.ROOTPATH, `assets/svg/auth-icon-${strategyKey}.svg`), 'utf8').then(iconData => { + strategy.icon = iconData + }).catch(err => { + if (err.code === 'ENOENT') { + strategy.icon = '[missing icon]' + } else { + wiki.logger.error(err) + } + }) + this.strategies[strategy.key] = strategy + wiki.logger.info(`Authentication Provider ${strategyKey}: OK`) + }) + + // Create Guest account for first-time + + wiki.db.User.findOne({ + where: { + provider: 'local', + email: 'guest@example.com' + } + }).then((c) => { + if (c < 1) { + return wiki.db.User.create({ + provider: 'local', + email: 'guest@example.com', + name: 'Guest', + password: '', + role: 'guest' + }).then(() => { + wiki.logger.info('[AUTH] Guest account created successfully!') + return true + }).catch((err) => { + wiki.logger.error('[AUTH] An error occured while creating guest account:') + wiki.logger.error(err) + return err + }) + } + }) + + // .then(() => { + // if (process.env.WIKI_JS_HEROKU) { + // return wiki.db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => { + // if (c < 1) { + // // Create root admin account (HEROKU ONLY) + + // return wiki.db.User.create({ + // provider: 'local', + // email: process.env.WIKI_ADMIN_EMAIL, + // name: 'Administrator', + // password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default) + // role: 'admin' + // }).then(() => { + // wiki.logger.info('[AUTH] Root admin account created successfully!') + // return true + // }).catch((err) => { + // wiki.logger.error('[AUTH] An error occured while creating root admin account:') + // wiki.logger.error(err) + // return err + // }) + // } else { return true } + // }) + // } else { return true } + // }) + + return this + } +} diff --git a/server/modules/config.js b/server/modules/config.js new file mode 100644 index 00000000..a8e15378 --- /dev/null +++ b/server/modules/config.js @@ -0,0 +1,85 @@ +/* global wiki */ + +const fs = require('fs') +const yaml = require('js-yaml') +const _ = require('lodash') +const path = require('path') +const cfgHelper = require('../helpers/config') + +module.exports = { + /** + * Load root config from disk + */ + init() { + let confPaths = { + config: path.join(wiki.ROOTPATH, 'config.yml'), + data: path.join(wiki.SERVERPATH, 'app/data.yml'), + dataRegex: path.join(wiki.SERVERPATH, 'app/regex.js') + } + + let appconfig = {} + let appdata = {} + + try { + appconfig = yaml.safeLoad( + cfgHelper.parseConfigValue( + fs.readFileSync(confPaths.config, 'utf8') + ) + ) + appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8')) + appdata.regex = require(confPaths.dataRegex) + } catch (ex) { + console.error(ex) + process.exit(1) + } + + // Merge with defaults + + appconfig = _.defaultsDeep(appconfig, appdata.defaults.config) + + // Check port + + if (appconfig.port < 1) { + appconfig.port = process.env.PORT || 80 + } + + // Convert booleans + + appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true') + + // List authentication strategies + wiki.config = appconfig + wiki.data = appdata + }, + + /** + * Load config from DB + * + * @param {Array} subsets Array of subsets to load + * @returns Promise + */ + loadFromDb(subsets) { + if (!_.isArray(subsets) || subsets.length === 0) { + subsets = wiki.data.configNamespaces + } + + return wiki.db.Setting.findAll({ + attributes: ['key', 'config'], + where: { + key: { + $in: subsets + } + } + }).then(results => { + if (_.isArray(results) && results.length === subsets.length) { + results.forEach(result => { + wiki.config[result.key] = result.config + }) + return true + } else { + wiki.logger.warn('DB Configuration is empty or incomplete.') + return false + } + }) + } +} diff --git a/server/modules/db.js b/server/modules/db.js new file mode 100644 index 00000000..7063f7f1 --- /dev/null +++ b/server/modules/db.js @@ -0,0 +1,137 @@ +'use strict' + +/* global wiki */ + +const fs = require('fs') +const path = require('path') +const _ = require('lodash') +const Promise = require('bluebird') +const Sequelize = require('sequelize') +const Op = Sequelize.Op + +const operatorsAliases = { + $eq: Op.eq, + $ne: Op.ne, + $gte: Op.gte, + $gt: Op.gt, + $lte: Op.lte, + $lt: Op.lt, + $not: Op.not, + $in: Op.in, + $notIn: Op.notIn, + $is: Op.is, + $like: Op.like, + $notLike: Op.notLike, + $iLike: Op.iLike, + $notILike: Op.notILike, + $regexp: Op.regexp, + $notRegexp: Op.notRegexp, + $iRegexp: Op.iRegexp, + $notIRegexp: Op.notIRegexp, + $between: Op.between, + $notBetween: Op.notBetween, + $overlap: Op.overlap, + $contains: Op.contains, + $contained: Op.contained, + $adjacent: Op.adjacent, + $strictLeft: Op.strictLeft, + $strictRight: Op.strictRight, + $noExtendRight: Op.noExtendRight, + $noExtendLeft: Op.noExtendLeft, + $and: Op.and, + $or: Op.or, + $any: Op.any, + $all: Op.all, + $values: Op.values, + $col: Op.col +} + +/** + * PostgreSQL DB module + */ +module.exports = { + + Sequelize, + Op: Sequelize.Op, + + /** + * Initialize DB + * + * @return {Object} DB instance + */ + init() { + let self = this + + let dbModelsPath = path.join(wiki.SERVERPATH, 'models') + + // Define Sequelize instance + + self.inst = new self.Sequelize(wiki.config.db.db, wiki.config.db.user, wiki.config.db.pass, { + host: wiki.config.db.host, + port: wiki.config.db.port, + dialect: 'postgres', + pool: { + max: 10, + min: 0, + idle: 10000 + }, + logging: log => { wiki.logger.log('verbose', log) }, + operatorsAliases + }) + + // Attempt to connect and authenticate to DB + + self.inst.authenticate().then(() => { + wiki.logger.info('Database (PostgreSQL) connection: OK') + }).catch(err => { + wiki.logger.error('Failed to connect to PostgreSQL instance.') + return err + }) + + // Load DB Models + + fs + .readdirSync(dbModelsPath) + .filter(file => { + return (file.indexOf('.') !== 0 && file.indexOf('_') !== 0) + }) + .forEach(file => { + let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0])) + self[modelName] = self.inst.import(path.join(dbModelsPath, file)) + }) + + // Associate DB Models + + require(path.join(dbModelsPath, '_relations.js'))(self) + + // Set init tasks + + let initTasks = { + // -> Sync DB Schemas + syncSchemas() { + return self.inst.sync({ + force: false, + logging: log => { wiki.logger.log('verbose', log) } + }) + }, + // -> Set Connection App Name + setAppName() { + return self.inst.query(`set application_name = 'Wiki.js'`, { raw: true }) + } + } + + let initTasksQueue = (wiki.IS_MASTER) ? [ + initTasks.syncSchemas, + initTasks.setAppName + ] : [ + initTasks.setAppName + ] + + // Perform init tasks + + self.onReady = Promise.each(initTasksQueue, t => t()).return(true) + + return self + } + +} diff --git a/server/libs/local.js b/server/modules/disk.js similarity index 59% rename from server/libs/local.js rename to server/modules/disk.js index 3053e63b..0fafcd68 100644 --- a/server/libs/local.js +++ b/server/modules/disk.js @@ -1,6 +1,6 @@ 'use strict' -/* global lang, winston */ +/* global wiki */ const path = require('path') const Promise = require('bluebird') @@ -10,7 +10,7 @@ const os = require('os') const _ = require('lodash') /** - * Local Data Storage + * Local Disk Storage */ module.exports = { @@ -21,29 +21,26 @@ module.exports = { /** * Initialize Local Data Storage model - * - * @return {Object} Local Data Storage model instance */ init () { - this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads') - this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs') + this._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads') + this._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs') - this.createBaseDirectories(appconfig) - this.initMulter(appconfig) + this.createBaseDirectories() + // this.initMulter() return this }, /** * Init Multer upload handlers - * - * @param {Object} appconfig The application config - * @return {boolean} Void */ - initMulter (appconfig) { + initMulter () { let maxFileSizes = { - img: appconfig.uploads.maxImageFileSize * 1024 * 1024, - file: appconfig.uploads.maxOtherFileSize * 1024 * 1024 + // img: wiki.config.uploads.maxImageFileSize * 1024 * 1024, + // file: wiki.config.uploads.maxOtherFileSize * 1024 * 1024 + img: 3 * 1024 * 1024, + file: 10 * 1024 * 1024 } // -> IMAGES @@ -51,7 +48,7 @@ module.exports = { this.uploadImgHandler = multer({ storage: multer.diskStorage({ destination: (req, f, cb) => { - cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload')) + cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload')) } }), fileFilter: (req, f, cb) => { @@ -76,7 +73,7 @@ module.exports = { this.uploadFileHandler = multer({ storage: multer.diskStorage({ destination: (req, f, cb) => { - cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload')) + cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload')) } }), fileFilter: (req, f, cb) => { @@ -95,35 +92,30 @@ module.exports = { /** * Creates a base directories (Synchronous). - * - * @param {Object} appconfig The application config - * @return {Void} Void */ - createBaseDirectories (appconfig) { - winston.info('Checking data directories...') - + createBaseDirectories () { try { - fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data)) - fs.emptyDirSync(path.resolve(ROOTPATH, appconfig.paths.data)) - fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache')) - fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs')) - fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload')) + fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data)) + fs.emptyDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data)) + fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './cache')) + fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './thumbs')) + fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload')) if (os.type() !== 'Windows_NT') { - fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '755') + fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload'), '755') } - fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo)) - fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads')) + fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo)) + fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads')) if (os.type() !== 'Windows_NT') { - fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'), '755') + fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads'), '755') } } catch (err) { - winston.error(err) + wiki.logger.error(err) } - winston.info('Data and Repository directories are OK.') + wiki.logger.info('Disk Data Paths: OK') }, /** @@ -154,7 +146,7 @@ module.exports = { */ validateUploadsFilename (f, fld, isImage) { let fObj = path.parse(f) - let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(new RegExp('[^a-z0-9-' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g'), '') + let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(new RegExp('[^a-z0-9-' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']', 'g'), '') let fext = _.toLower(fObj.ext) if (isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) { @@ -165,7 +157,7 @@ module.exports = { let fpath = path.resolve(this._uploadsPath, fld, f) return fs.statAsync(fpath).then((s) => { - throw new Error(lang.t('errors:fileexists', { path: f })) + throw new Error(wiki.lang.t('errors:fileexists', { path: f })) }).catch((err) => { if (err.code === 'ENOENT') { return f diff --git a/server/libs/entries.js b/server/modules/documents.js similarity index 97% rename from server/libs/entries.js rename to server/modules/documents.js index af6f1104..8406be0d 100644 --- a/server/libs/entries.js +++ b/server/modules/documents.js @@ -1,6 +1,6 @@ 'use strict' -/* global db, git, lang, mark, rights, search, winston */ +/* global wiki */ const Promise = require('bluebird') const path = require('path') @@ -10,7 +10,7 @@ const _ = require('lodash') const entryHelper = require('../helpers/entry') /** - * Entries Model + * Documents Model */ module.exports = { @@ -25,10 +25,10 @@ module.exports = { init() { let self = this - self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo) - self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache') - appdata.repoPath = self._repoPath - appdata.cachePath = self._cachePath + self._repoPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo) + self._cachePath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'cache') + wiki.data.repoPath = self._repoPath + wiki.data.cachePath = self._cachePath return self }, diff --git a/server/libs/git.js b/server/modules/git.js similarity index 71% rename from server/libs/git.js rename to server/modules/git.js index 7787f37a..68f78e1f 100644 --- a/server/libs/git.js +++ b/server/modules/git.js @@ -1,6 +1,6 @@ 'use strict' -/* global lang, winston */ +/* global wiki */ const Git = require('git-wrapper2-promise') const Promise = require('bluebird') @@ -43,21 +43,19 @@ module.exports = { // -> Build repository path - if (_.isEmpty(appconfig.paths.repo)) { - self._repo.path = path.join(ROOTPATH, 'repo') + if (_.isEmpty(wiki.config.paths.repo)) { + self._repo.path = path.join(wiki.ROOTPATH, 'repo') } else { - self._repo.path = appconfig.paths.repo + self._repo.path = wiki.config.paths.repo } // -> Initialize repository - self.onReady = self._initRepo(appconfig) + self.onReady = (wiki.IS_MASTER) ? self._initRepo() : Promise.resolve() - if (appconfig.git) { - // Set repo branch - self._repo.branch = appconfig.git.branch || 'master' - // Define signature - self._signature.email = appconfig.git.serverEmail || 'wiki@example.com' + if (wiki.config.git) { + self._repo.branch = wiki.config.git.branch || 'master' + self._signature.email = wiki.config.git.serverEmail || 'wiki@example.com' } return self @@ -66,19 +64,17 @@ module.exports = { /** * Initialize Git repository * - * @param {Object} appconfig The application config + * @param {Object} wiki.config The application config * @return {Object} Promise */ - _initRepo(appconfig) { + _initRepo() { let self = this - winston.info('Checking Git repository...') - // -> Check if path is accessible return fs.mkdirAsync(self._repo.path).catch((err) => { if (err.code !== 'EEXIST') { - winston.error('Invalid Git repository path or missing permissions.') + wiki.logger.error('Invalid Git repository path or missing permissions.') } }).then(() => { self._git = new Git({ 'git-dir': self._repo.path }) @@ -92,28 +88,28 @@ module.exports = { self._repo.exists = false }) }).then(() => { - if (appconfig.git === false) { - winston.info('Remote Git syncing is disabled. Not recommended!') + if (wiki.config.git === false) { + wiki.logger.warn('Remote Git syncing is disabled. Not recommended!') return Promise.resolve(true) } // Initialize remote - let urlObj = URL.parse(appconfig.git.url) - if (appconfig.git.auth.type !== 'ssh') { - urlObj.auth = appconfig.git.auth.username + ':' + appconfig.git.auth.password + let urlObj = URL.parse(wiki.config.git.url) + if (wiki.config.git.auth.type !== 'ssh') { + urlObj.auth = wiki.config.git.auth.username + ':' + wiki.config.git.auth.password } self._url = URL.format(urlObj) let gitConfigs = [ () => { return self._git.exec('config', ['--local', 'user.name', 'Wiki']) }, () => { return self._git.exec('config', ['--local', 'user.email', self._signature.email]) }, - () => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(appconfig.git.auth.sslVerify)]) } + () => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(wiki.config.git.auth.sslVerify)]) } ] - if (appconfig.git.auth.type === 'ssh') { + if (wiki.config.git.auth.type === 'ssh') { gitConfigs.push(() => { - return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + appconfig.git.auth.privateKey + '" -o StrictHostKeyChecking=no']) + return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + wiki.config.git.auth.privateKey + '" -o StrictHostKeyChecking=no']) }) } @@ -126,14 +122,14 @@ module.exports = { return self._git.exec('remote', ['set-url', 'origin', self._url]) } }).catch(err => { - winston.error(err) + wiki.logger.error(err) }) }) }).catch((err) => { - winston.error('Git remote error!') + wiki.logger.error('Git remote error!') throw err }).then(() => { - winston.info('Git repository is OK.') + wiki.logger.info('Git Repository: OK') return true }) }, @@ -144,7 +140,7 @@ module.exports = { * @return {String} The repo path. */ getRepoPath() { - return this._repo.path || path.join(ROOTPATH, 'repo') + return this._repo.path || path.join(wiki.ROOTPATH, 'repo') }, /** @@ -157,18 +153,18 @@ module.exports = { // Is git remote disabled? - if (appconfig.git === false) { + if (wiki.config.git === false) { return Promise.resolve(true) } // Fetch - winston.info('Performing pull from remote Git repository...') + wiki.logger.info('Performing pull from remote Git repository...') return self._git.pull('origin', self._repo.branch).then((cProc) => { - winston.info('Git Pull completed.') + wiki.logger.info('Git Pull completed.') }) .catch((err) => { - winston.error('Unable to fetch from git origin!') + wiki.logger.error('Unable to fetch from git origin!') throw err }) .then(() => { @@ -178,19 +174,19 @@ module.exports = { let out = cProc.stdout.toString() if (_.includes(out, 'commit')) { - winston.info('Performing push to remote Git repository...') + wiki.logger.info('Performing push to remote Git repository...') return self._git.push('origin', self._repo.branch).then(() => { - return winston.info('Git Push completed.') + return wiki.logger.info('Git Push completed.') }) } else { - winston.info('Git Push skipped. Repository is already in sync.') + wiki.logger.info('Git Push skipped. Repository is already in sync.') } return true }) }) .catch((err) => { - winston.error('Unable to push changes to remote Git repository!') + wiki.logger.error('Unable to push changes to remote Git repository!') throw err }) }, @@ -210,7 +206,7 @@ module.exports = { let out = cProc.stdout.toString() return _.includes(out, gitFilePath) }).then((isTracked) => { - commitMsg = (isTracked) ? lang.t('git:updated', { path: gitFilePath }) : lang.t('git:added', { path: gitFilePath }) + commitMsg = (isTracked) ? wiki.lang.t('git:updated', { path: gitFilePath }) : wiki.lang.t('git:added', { path: gitFilePath }) return self._git.add(gitFilePath) }).then(() => { let commitUsr = securityHelper.sanitizeCommitUser(author) @@ -245,29 +241,6 @@ module.exports = { }) }, - /** - * Delete a document. - * - * @param {String} entryPath The entry path - * @return {Promise} Resolve on success - */ - deleteDocument(entryPath, author) { - let self = this - let gitFilePath = entryPath + '.md' - - return this._git.exec('rm', [gitFilePath]).then((cProc) => { - let out = cProc.stdout.toString() - if (_.includes(out, 'fatal')) { - let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ','))) - throw new Error(errorMsg) - } - let commitUsr = securityHelper.sanitizeCommitUser(author) - return self._git.exec('commit', ['-m', lang.t('git:deleted', { path: gitFilePath }), '--author="' + commitUsr.name + ' <' + commitUsr.email + '>"']).catch((err) => { - if (_.includes(err.stdout, 'nothing to commit')) { return true } - }) - }) - }, - /** * Commits uploads changes. * diff --git a/server/modules/graphql.js b/server/modules/graphql.js new file mode 100644 index 00000000..aa6a6f7d --- /dev/null +++ b/server/modules/graphql.js @@ -0,0 +1,43 @@ +'use strict' + +/* global wiki */ + +const gqlTools = require('graphql-tools') +const fs = require('fs') +const path = require('path') +const _ = require('lodash') + +const typeDefs = fs.readFileSync(path.join(wiki.SERVERPATH, 'schemas/types.graphql'), 'utf8') + +const DateScalar = require('../schemas/scalar-date') +const AuthenticationResolvers = require('../schemas/resolvers-authentication') +const CommentResolvers = require('../schemas/resolvers-comment') +const DocumentResolvers = require('../schemas/resolvers-document') +const FileResolvers = require('../schemas/resolvers-file') +const FolderResolvers = require('../schemas/resolvers-folder') +const GroupResolvers = require('../schemas/resolvers-group') +const SettingResolvers = require('../schemas/resolvers-setting') +const TagResolvers = require('../schemas/resolvers-tag') +const TranslationResolvers = require('../schemas/resolvers-translation') +const UserResolvers = require('../schemas/resolvers-user') + +const resolvers = _.merge( + AuthenticationResolvers, + CommentResolvers, + DocumentResolvers, + FileResolvers, + FolderResolvers, + GroupResolvers, + SettingResolvers, + TagResolvers, + TranslationResolvers, + UserResolvers, + DateScalar +) + +const Schema = gqlTools.makeExecutableSchema({ + typeDefs, + resolvers +}) + +module.exports = Schema diff --git a/server/modules/kernel.js b/server/modules/kernel.js new file mode 100644 index 00000000..48e1659f --- /dev/null +++ b/server/modules/kernel.js @@ -0,0 +1,91 @@ +const cluster = require('cluster') +const Promise = require('bluebird') +const _ = require('lodash') + +/* global wiki */ + +module.exports = { + numWorkers: 1, + workers: [], + init() { + if (cluster.isMaster) { + wiki.logger.info('=======================================') + wiki.logger.info('= Wiki.js =============================') + wiki.logger.info('=======================================') + + wiki.redis = require('./redis').init() + wiki.queue = require('./queue').init() + + this.setWorkerLimit() + this.bootMaster() + } else { + this.bootWorker() + } + }, + /** + * Pre-Master Boot Sequence + */ + preBootMaster() { + return Promise.mapSeries([ + () => { return wiki.db.onReady }, + () => { return wiki.configSvc.loadFromDb() }, + () => { return wiki.queue.clean() } + ], fn => { return fn() }) + }, + /** + * Boot Master Process + */ + bootMaster() { + this.preBootMaster().then(sequenceResults => { + if (_.every(sequenceResults, rs => rs === true)) { + this.postBootMaster() + } else { + wiki.logger.info('Starting configuration manager...') + require('../configure')() + } + return true + }).catch(err => { + wiki.logger.error(err) + process.exit(1) + }) + }, + /** + * Post-Master Boot Sequence + */ + postBootMaster() { + require('../master')().then(() => { + _.times(this.numWorker, this.spawnWorker) + + wiki.queue.uplClearTemp.add({}, { + repeat: { cron: '*/15 * * * *' } + }) + }) + + cluster.on('exit', (worker, code, signal) => { + wiki.logger.info(`Background Worker #${worker.id} was terminated.`) + }) + }, + /** + * Boot Worker Process + */ + bootWorker() { + wiki.logger.info(`Background Worker #${cluster.worker.id} is initializing...`) + require('../worker') + }, + /** + * Spawn new Worker process + */ + spawnWorker() { + this.workers.push(cluster.fork()) + }, + /** + * Set Worker count based on config + system capabilities + */ + setWorkerLimit() { + const numCPUs = require('os').cpus().length + this.numWorkers = (wiki.config.workers > 0) ? wiki.config.workers : numCPUs + if (this.numWorkers > numCPUs) { + this.numWorkers = numCPUs + } + } +} diff --git a/server/modules/localization.js b/server/modules/localization.js new file mode 100644 index 00000000..d832935b --- /dev/null +++ b/server/modules/localization.js @@ -0,0 +1,52 @@ +const _ = require('lodash') +const dotize = require('dotize') +const i18nBackend = require('i18next-node-fs-backend') +const i18next = require('i18next') +const path = require('path') +const Promise = require('bluebird') + +/* global wiki */ + +module.exports = { + engine: null, + namespaces: ['common', 'admin', 'auth', 'errors', 'git'], + init() { + this.engine = i18next + this.engine.use(i18nBackend).init({ + load: 'languageOnly', + ns: this.namespaces, + defaultNS: 'common', + saveMissing: false, + preload: [wiki.config.site.lang], + lng: wiki.config.site.lang, + fallbackLng: 'en', + backend: { + loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.json') + } + }) + return this + }, + getByNamespace(locale, namespace) { + if (this.engine.hasResourceBundle(locale, namespace)) { + let data = this.engine.getResourceBundle(locale, namespace) + return _.map(dotize.convert(data), (value, key) => { + return { + key, + value + } + }) + } else { + throw new Error('Invalid locale or namespace') + } + }, + loadLocale(locale) { + return Promise.fromCallback(cb => { + return this.engine.loadLanguages(locale, cb) + }) + }, + setCurrentLocale(locale) { + return Promise.fromCallback(cb => { + return this.engine.changeLanguage(locale, cb) + }) + } +} diff --git a/server/modules/logger.js b/server/modules/logger.js new file mode 100644 index 00000000..8b134d05 --- /dev/null +++ b/server/modules/logger.js @@ -0,0 +1,80 @@ +'use strict' + +/* global wiki */ + +const cluster = require('cluster') + +module.exports = { + init() { + let winston = require('winston') + + // Console + + let logger = new (winston.Logger)({ + level: (wiki.IS_DEBUG) ? 'debug' : 'info', + transports: [ + new (winston.transports.Console)({ + level: (wiki.IS_DEBUG) ? 'debug' : 'info', + prettyPrint: true, + colorize: true, + silent: false, + timestamp: true + }) + ] + }) + + logger.filters.push((level, msg) => { + let processName = (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}` + return '[' + processName + '] ' + msg + }) + + // External services + + // if (wiki.config.externalLogging.bugsnag) { + // const bugsnagTransport = require('./winston-transports/bugsnag') + // logger.add(bugsnagTransport, { + // level: 'warn', + // key: wiki.config.externalLogging.bugsnag + // }) + // } + + // if (wiki.config.externalLogging.loggly) { + // require('winston-loggly-bulk') + // logger.add(winston.transports.Loggly, { + // token: wiki.config.externalLogging.loggly.token, + // subdomain: wiki.config.externalLogging.loggly.subdomain, + // tags: ['wiki-js'], + // level: 'warn', + // json: true + // }) + // } + + // if (wiki.config.externalLogging.papertrail) { + // require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions + // logger.add(winston.transports.Papertrail, { + // host: wiki.config.externalLogging.papertrail.host, + // port: wiki.config.externalLogging.papertrail.port, + // level: 'warn', + // program: 'wiki.js' + // }) + // } + + // if (wiki.config.externalLogging.rollbar) { + // const rollbarTransport = require('./winston-transports/rollbar') + // logger.add(rollbarTransport, { + // level: 'warn', + // key: wiki.config.externalLogging.rollbar + // }) + // } + + // if (wiki.config.externalLogging.sentry) { + // const sentryTransport = require('./winston-transports/sentry') + // logger.add(sentryTransport, { + // level: 'warn', + // key: wiki.config.externalLogging.sentry + // }) + // } + + return logger + } +} diff --git a/server/libs/markdown.js b/server/modules/markdown.js similarity index 96% rename from server/libs/markdown.js rename to server/modules/markdown.js index 14b0c887..71e0ea54 100644 --- a/server/libs/markdown.js +++ b/server/modules/markdown.js @@ -1,6 +1,6 @@ 'use strict' -/* global winston */ +/* global wiki */ const Promise = require('bluebird') const md = require('markdown-it') @@ -23,11 +23,12 @@ const mdRemove = require('remove-markdown') var mkdown = md({ html: true, - breaks: appconfig.features.linebreaks, + // breaks: wiki.config.features.linebreaks, + breaks: true, linkify: true, typography: true, highlight(str, lang) { - if (appconfig.theme.code.colorize && lang && hljs.getLanguage(lang)) { + if (wiki.config.theme.code.colorize && lang && hljs.getLanguage(lang)) { try { return '
' + hljs.highlight(lang, str, true).value + '
' } catch (err) { @@ -57,7 +58,8 @@ var mkdown = md({ }) .use(mdAttrs) -if (appconfig.features.mathjax) { +// if (wiki.config.features.mathjax) { +if (true) { mkdown.use(mdMathjax) } @@ -94,7 +96,7 @@ const videoRules = [ // Regex -const textRegex = new RegExp('\\b[a-z0-9-.,' + appdata.regex.cjk + appdata.regex.arabic + ']+\\b', 'g') +const textRegex = new RegExp('\\b[a-z0-9-.,' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']+\\b', 'g') const mathRegex = [ { format: 'TeX', @@ -301,7 +303,7 @@ const parseContent = (content) => { // Mathjax Post-processor - if (appconfig.features.mathjax) { + if (wiki.config.features.mathjax) { return processMathjax(cr.html()) } else { return Promise.resolve(cr.html()) @@ -339,7 +341,7 @@ const processMathjax = (content) => { resolve(result.svg) } else { resolve(currentMatch[0]) - winston.warn(result.errors.join(', ')) + wiki.logger.warn(result.errors.join(', ')) } }) }) diff --git a/server/modules/queue.js b/server/modules/queue.js new file mode 100644 index 00000000..4bbb6dc0 --- /dev/null +++ b/server/modules/queue.js @@ -0,0 +1,37 @@ +'use strict' + +/* global wiki */ + +const Bull = require('bull') +const Promise = require('bluebird') + +module.exports = { + init() { + wiki.data.queues.forEach(queueName => { + this[queueName] = new Bull(queueName, { + prefix: `q-${wiki.config.ha.nodeuid}`, + redis: wiki.config.redis + }) + }) + return this + }, + clean() { + return Promise.each(wiki.data.queues, queueName => { + return new Promise((resolve, reject) => { + let keyStream = wiki.redis.scanStream({ + match: `q-${wiki.config.ha.nodeuid}:${queueName}:*` + }) + keyStream.on('data', resultKeys => { + if (resultKeys.length > 0) { + wiki.redis.del(resultKeys) + } + }) + keyStream.on('end', resolve) + }) + }).then(() => { + wiki.logger.info('Purging old queue jobs: OK') + }).return(true).catch(err => { + wiki.logger.error(err) + }) + } +} diff --git a/server/modules/redis.js b/server/modules/redis.js new file mode 100644 index 00000000..6eb7999b --- /dev/null +++ b/server/modules/redis.js @@ -0,0 +1,33 @@ +'use strict' + +/* global wiki */ + +const Redis = require('ioredis') +const { isPlainObject } = require('lodash') + +/** + * Redis module + * + * @return {Object} Redis client wrapper instance + */ +module.exports = { + + /** + * Initialize Redis client + * + * @return {Object} Redis client instance + */ + init() { + if (isPlainObject(wiki.config.redis)) { + let red = new Redis(wiki.config.redis) + red.on('ready', () => { + wiki.logger.info('Redis connection: OK') + }) + return red + } else { + wiki.logger.error('Invalid Redis configuration!') + process.exit(1) + } + } + +} diff --git a/server/libs/rights.js b/server/modules/rights.js similarity index 94% rename from server/libs/rights.js rename to server/modules/rights.js index ce82882b..d06de758 100644 --- a/server/libs/rights.js +++ b/server/modules/rights.js @@ -1,6 +1,6 @@ 'use strict' -/* global db */ +/* global wiki */ const _ = require('lodash') @@ -32,8 +32,8 @@ module.exports = { init () { let self = this - db.onReady.then(() => { - db.User.findOne({ provider: 'local', email: 'guest' }).then((u) => { + wiki.db.onReady.then(() => { + wiki.db.User.findOne({ provider: 'local', email: 'guest' }).then((u) => { if (u) { self.guest = u } diff --git a/server/libs/search.js b/server/modules/search.js similarity index 87% rename from server/libs/search.js rename to server/modules/search.js index 7c446b17..7801a739 100644 --- a/server/libs/search.js +++ b/server/modules/search.js @@ -1,13 +1,13 @@ 'use strict' -/* global winston */ +/* global wiki */ const Promise = require('bluebird') const _ = require('lodash') -const searchIndex = require('./search-index') +// const searchIndex = require('./search-index') const stopWord = require('stopword') const streamToPromise = require('stream-to-promise') -const searchAllowedChars = new RegExp('[^a-z0-9' + appdata.regex.cjk + appdata.regex.arabic + ' ]', 'g') +const searchAllowedChars = new RegExp('[^a-z0-9' + wiki.data.regex.cjk + wiki.data.regex.arabic + ' ]', 'g') module.exports = { @@ -22,24 +22,24 @@ module.exports = { init () { let self = this self._isReady = new Promise((resolve, reject) => { - searchIndex({ + /*searchIndex({ deletable: true, fieldedSearch: true, indexPath: 'wiki', logLevel: 'error', - stopwords: _.get(stopWord, appconfig.lang, []) + stopwords: _.get(stopWord, wiki.config.lang, []) }, (err, si) => { if (err) { - winston.error('Failed to initialize search index.', err) + wiki.logger.error('Failed to initialize search index.', err) reject(err) } else { self._si = Promise.promisifyAll(si) self._si.flushAsync().then(() => { - winston.info('Search index flushed and ready.') + wiki.logger.info('Search index flushed and ready.') resolve(true) }) } - }) + }) */ }) return self @@ -95,13 +95,13 @@ module.exports = { parent: content.parent || '', content: content.text || '' }]).then(() => { - winston.log('verbose', 'Entry ' + content._id + ' added/updated to search index.') + wiki.logger.log('verbose', 'Entry ' + content._id + ' added/updated to search index.') return true }).catch((err) => { - winston.error(err) + wiki.logger.error(err) }) }).catch((err) => { - winston.error(err) + wiki.logger.error(err) }) }) }, @@ -131,7 +131,7 @@ module.exports = { if (err.type === 'NotFoundError') { return true } else { - winston.error(err) + wiki.logger.error(err) } }) }) @@ -204,7 +204,7 @@ module.exports = { suggest: [] } } else { - winston.error(err) + wiki.logger.error(err) } }) } diff --git a/server/libs/system.js b/server/modules/system.js similarity index 100% rename from server/libs/system.js rename to server/modules/system.js diff --git a/server/libs/uploads-agent.js b/server/modules/uploads-agent.js similarity index 88% rename from server/libs/uploads-agent.js rename to server/modules/uploads-agent.js index a8ae79ec..6b861733 100644 --- a/server/libs/uploads-agent.js +++ b/server/modules/uploads-agent.js @@ -1,6 +1,6 @@ 'use strict' -/* global db, git, lang, upl */ +/* global wiki */ const path = require('path') const Promise = require('bluebird') @@ -32,8 +32,8 @@ module.exports = { init () { let self = this - self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads') - self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs') + self._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads') + self._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs') return self }, @@ -59,16 +59,16 @@ module.exports = { self._watcher.on('add', (p) => { let pInfo = self.parseUploadsRelPath(p) return self.processFile(pInfo.folder, pInfo.filename).then((mData) => { - return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true }) + return wiki.db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true }) }).then(() => { - return git.commitUploads(lang.t('git:uploaded', { path: p })) + return wiki.git.commitUploads(wiki.lang.t('git:uploaded', { path: p })) }) }) // -> Remove upload file self._watcher.on('unlink', (p) => { - return git.commitUploads(lang.t('git:deleted', { path: p })) + return wiki.git.commitUploads(wiki.lang.t('git:deleted', { path: p })) }) }, @@ -91,8 +91,8 @@ module.exports = { // Add folders to DB - return db.UplFolder.remove({}).then(() => { - return db.UplFolder.insertMany(_.map(folderNames, (f) => { + return wiki.db.UplFolder.remove({}).then(() => { + return wiki.db.UplFolder.insertMany(_.map(folderNames, (f) => { return { _id: 'f:' + f, name: f @@ -107,7 +107,7 @@ module.exports = { let fldPath = path.join(self._uploadsPath, fldName) return fs.readdirAsync(fldPath).then((fList) => { return Promise.map(fList, (f) => { - return upl.processFile(fldName, f).then((mData) => { + return wiki.upl.processFile(fldName, f).then((mData) => { if (mData) { allFiles.push(mData) } @@ -118,9 +118,9 @@ module.exports = { }, {concurrency: 1}).finally(() => { // Add files to DB - return db.UplFile.remove({}).then(() => { + return wiki.db.UplFile.remove({}).then(() => { if (_.isArray(allFiles) && allFiles.length > 0) { - return db.UplFile.insertMany(allFiles) + return wiki.db.UplFile.insertMany(allFiles) } else { return true } @@ -131,7 +131,7 @@ module.exports = { }).then(() => { // Watch for new changes - return upl.watch() + return wiki.upl.watch() }) }, diff --git a/server/libs/uploads.js b/server/modules/uploads.js similarity index 81% rename from server/libs/uploads.js rename to server/modules/uploads.js index 2bee6322..f56ecab3 100644 --- a/server/libs/uploads.js +++ b/server/modules/uploads.js @@ -1,6 +1,6 @@ 'use strict' -/* global db, lang, lcdata, upl, winston */ +/* global wiki */ const path = require('path') const Promise = require('bluebird') @@ -27,8 +27,8 @@ module.exports = { * @return {Object} Uploads model instance */ init () { - this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads') - this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs') + this._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads') + this._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs') return this }, @@ -48,7 +48,7 @@ module.exports = { * @return {Array} The uploads folders. */ getUploadsFolders () { - return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => { + return wiki.db.Folder.find({}, 'name').sort('name').exec().then((results) => { return (results) ? _.map(results, 'name') : [{ name: '' }] }) }, @@ -69,7 +69,7 @@ module.exports = { } return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => { - return db.UplFolder.findOneAndUpdate({ + return wiki.db.UplFolder.findOneAndUpdate({ _id: 'f:' + folderName }, { name: folderName @@ -88,7 +88,7 @@ module.exports = { * @return {Boolean} True if valid */ validateUploadsFolder (folderName) { - return db.UplFolder.findOne({ name: folderName }).then((f) => { + return wiki.db.UplFolder.findOne({ name: folderName }).then((f) => { return (f) ? path.resolve(this._uploadsPath, folderName) : false }) }, @@ -101,7 +101,7 @@ module.exports = { */ addUploadsFiles (arrFiles) { if (_.isArray(arrFiles) || _.isPlainObject(arrFiles)) { - // this._uploadsDb.Files.insert(arrFiles); + // this._uploadswiki.Db.Files.insert(arrFiles); } }, @@ -113,7 +113,7 @@ module.exports = { * @return {Array} The files matching the query */ getUploadsFiles (cat, fld) { - return db.UplFile.find({ + return wiki.db.UplFile.find({ category: cat, folder: 'f:' + fld }).sort('filename').exec() @@ -128,7 +128,7 @@ module.exports = { deleteUploadsFile (uid) { let self = this - return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => { + return wiki.db.UplFile.findOneAndRemove({ _id: uid }).then((f) => { if (f) { return self.deleteUploadsFileTry(f, 0) } @@ -150,7 +150,7 @@ module.exports = { return self.deleteUploadsFileTry(f, attempt + 1) }) } else { - winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.') + wiki.logger.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.') return true } }) @@ -168,12 +168,12 @@ module.exports = { let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/')) let destFolder = _.chain(fFolder).trim().toLower().value() - return upl.validateUploadsFolder(destFolder).then((destFolderPath) => { + return wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => { if (!destFolderPath) { - return Promise.reject(new Error(lang.t('errors:invalidfolder'))) + return Promise.reject(new Error(wiki.lang.t('errors:invalidfolder'))) } - return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => { + return wiki.disk.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => { let destFilePath = path.resolve(destFolderPath, destFilename) return new Promise((resolve, reject) => { @@ -194,7 +194,7 @@ module.exports = { rq.abort() destFileStream.destroy() fs.remove(destFilePath) - reject(new Error(lang.t('errors:remotetoolarge'))) + reject(new Error(wiki.lang.t('errors:remotetoolarge'))) } }).on('error', (err) => { destFileStream.destroy() @@ -223,15 +223,15 @@ module.exports = { moveUploadsFile (uid, fld, nFilename) { let self = this - return db.UplFolder.findById('f:' + fld).then((folder) => { + return wiki.db.UplFolder.finwiki.dById('f:' + fld).then((folder) => { if (folder) { - return db.UplFile.findById(uid).then((originFile) => { + return wiki.db.UplFile.finwiki.dById(uid).then((originFile) => { // -> Check if rename is valid let nameCheck = null if (nFilename) { let originFileObj = path.parse(originFile.filename) - nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name) + nameCheck = wiki.disk.validateUploadsFilename(nFilename + originFileObj.ext, folder.name) } else { nameCheck = Promise.resolve(originFile.filename) } @@ -245,12 +245,12 @@ module.exports = { // -> Check for invalid operations if (sourceFilePath === destFilePath) { - return Promise.reject(new Error(lang.t('errors:invalidoperation'))) + return Promise.reject(new Error(wiki.lang.t('errors:invalidoperation'))) } - // -> Delete DB entry + // -> Delete wiki.DB entry - preMoveOps.push(db.UplFile.findByIdAndRemove(uid)) + preMoveOps.push(wiki.db.UplFile.finwiki.dByIdAndRemove(uid)) // -> Move thumbnail ahead to avoid re-generation @@ -273,7 +273,7 @@ module.exports = { }) }) } else { - return Promise.reject(new Error(lang.t('errors:invaliddestfolder'))) + return Promise.reject(new Error(wiki.lang.t('errors:invaliddestfolder'))) } }) } diff --git a/server/libs/winston-transports/bugsnag.js b/server/modules/winston-transports/bugsnag.js similarity index 100% rename from server/libs/winston-transports/bugsnag.js rename to server/modules/winston-transports/bugsnag.js diff --git a/server/libs/winston-transports/rollbar.js b/server/modules/winston-transports/rollbar.js similarity index 100% rename from server/libs/winston-transports/rollbar.js rename to server/modules/winston-transports/rollbar.js diff --git a/server/libs/winston-transports/sentry.js b/server/modules/winston-transports/sentry.js similarity index 100% rename from server/libs/winston-transports/sentry.js rename to server/modules/winston-transports/sentry.js diff --git a/server/queues/git-sync.js b/server/queues/git-sync.js new file mode 100644 index 00000000..6c46e8ef --- /dev/null +++ b/server/queues/git-sync.js @@ -0,0 +1,68 @@ +'use strict' + +/* global wiki */ + +const Promise = require('bluebird') +const fs = Promise.promisifyAll(require('fs-extra')) +const klaw = require('klaw') +const moment = require('moment') +const path = require('path') +const entryHelper = require('../helpers/entry') + +module.exports = (job) => { + return wiki.git.resync().then(() => { + // -> Stream all documents + + let cacheJobs = [] + let jobCbStreamDocsResolve = null + let jobCbStreamDocs = new Promise((resolve, reject) => { + jobCbStreamDocsResolve = resolve + }) + + klaw(wiki.REPOPATH).on('data', function (item) { + if (path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') { + let entryPath = entryHelper.parsePath(entryHelper.getEntryPathFromFullPath(item.path)) + let cachePath = entryHelper.getCachePath(entryPath) + + // -> Purge outdated cache + + cacheJobs.push( + fs.statAsync(cachePath).then((st) => { + return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active' + }).catch((err) => { + return (err.code !== 'EEXIST') ? err : 'new' + }).then((fileStatus) => { + // -> Delete expired cache file + + if (fileStatus === 'expired') { + return fs.unlinkAsync(cachePath).return(fileStatus) + } + + return fileStatus + }).then((fileStatus) => { + // -> Update cache and search index + + if (fileStatus !== 'active') { + return global.entries.updateCache(entryPath).then(entry => { + process.send({ + action: 'searchAdd', + content: entry + }) + return true + }) + } + + return true + }) + ) + } + }).on('end', () => { + jobCbStreamDocsResolve(Promise.all(cacheJobs)) + }) + + return jobCbStreamDocs + }).then(() => { + wiki.logger.info('Git remote repository sync: DONE') + return true + }) +} diff --git a/server/queues/upl-clear-temp.js b/server/queues/upl-clear-temp.js new file mode 100644 index 00000000..9c3a1b5b --- /dev/null +++ b/server/queues/upl-clear-temp.js @@ -0,0 +1,29 @@ +'use strict' + +/* global wiki */ + +const Promise = require('bluebird') +const fs = Promise.promisifyAll(require('fs-extra')) +const moment = require('moment') +const path = require('path') + +module.exports = (job) => { + return fs.readdirAsync(wiki.UPLTEMPPATH).then((ls) => { + let fifteenAgo = moment().subtract(15, 'minutes') + + return Promise.map(ls, (f) => { + return fs.statAsync(path.join(wiki.UPLTEMPPATH, f)).then((s) => { return { filename: f, stat: s } }) + }).filter((s) => { return s.stat.isFile() }).then((arrFiles) => { + return Promise.map(arrFiles, (f) => { + if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) { + return fs.unlinkAsync(path.join(wiki.UPLTEMPPATH, f.filename)) + } else { + return true + } + }) + }) + }).then(() => { + wiki.logger.info('Purging temporary upload files: DONE') + return true + }) +} diff --git a/server/schemas/resolvers-authentication.js b/server/schemas/resolvers-authentication.js new file mode 100644 index 00000000..572c9d37 --- /dev/null +++ b/server/schemas/resolvers-authentication.js @@ -0,0 +1,40 @@ +const _ = require('lodash') +const fs = require('fs-extra') +const path = require('path') + +/* global wiki */ + +module.exports = { + Query: { + authentication(obj, args, context, info) { + switch (args.mode) { + case 'active': + let strategies = _.chain(wiki.auth.strategies).map(str => { + return { + key: str.key, + title: str.title, + useForm: str.useForm + } + }).sortBy(['title']).value() + let localStrategy = _.remove(strategies, str => str.key === 'local') + return _.concat(localStrategy, strategies) + case 'all': + + break + default: + return null + } + } + }, + Mutation: {}, + AuthenticationProvider: { + icon (ap, args) { + return fs.readFileAsync(path.join(wiki.ROOTPATH, `assets/svg/auth-icon-${ap.key}.svg`), 'utf8').catch(err => { + if (err.code === 'ENOENT') { + return null + } + throw err + }) + } + } +} diff --git a/server/schemas/resolvers-comment.js b/server/schemas/resolvers-comment.js new file mode 100644 index 00000000..fec565b3 --- /dev/null +++ b/server/schemas/resolvers-comment.js @@ -0,0 +1,42 @@ + +/* global wiki */ + +module.exports = { + Query: { + comments(obj, args, context, info) { + return wiki.db.Comment.findAll({ where: args }) + } + }, + Mutation: { + createComment(obj, args) { + return wiki.db.Comment.create({ + content: args.content, + author: args.userId, + document: args.documentId + }) + }, + deleteComment(obj, args) { + return wiki.db.Comment.destroy({ + where: { + id: args.id + }, + limit: 1 + }) + }, + modifyComment(obj, args) { + return wiki.db.Comment.update({ + content: args.content + }, { + where: { id: args.id } + }) + } + }, + Comment: { + author(cm) { + return cm.getAuthor() + }, + document(cm) { + return cm.getDocument() + } + } +} diff --git a/server/schemas/resolvers-document.js b/server/schemas/resolvers-document.js new file mode 100644 index 00000000..41dc9160 --- /dev/null +++ b/server/schemas/resolvers-document.js @@ -0,0 +1,46 @@ + +/* global wiki */ + +module.exports = { + Query: { + documents(obj, args, context, info) { + return wiki.db.Document.findAll({ where: args }) + } + }, + Mutation: { + createDocument(obj, args) { + return wiki.db.Document.create(args) + }, + deleteDocument(obj, args) { + return wiki.db.Document.destroy({ + where: { + id: args.id + }, + limit: 1 + }) + }, + modifyDocument(obj, args) { + return wiki.db.Document.update({ + title: args.title, + subtitle: args.subtitle + }, { + where: { id: args.id } + }) + }, + moveDocument(obj, args) { + return wiki.db.Document.update({ + path: args.path + }, { + where: { id: args.id } + }) + } + }, + Document: { + comments(doc) { + return doc.getComments() + }, + tags(doc) { + return doc.getTags() + } + } +} diff --git a/server/schemas/resolvers-file.js b/server/schemas/resolvers-file.js new file mode 100644 index 00000000..c8913413 --- /dev/null +++ b/server/schemas/resolvers-file.js @@ -0,0 +1,51 @@ + +/* global wiki */ + +const gql = require('graphql') + +module.exports = { + Query: { + files(obj, args, context, info) { + return wiki.db.File.findAll({ where: args }) + } + }, + Mutation: { + uploadFile(obj, args) { + // todo + return wiki.db.File.create(args) + }, + deleteFile(obj, args) { + return wiki.db.File.destroy({ + where: { + id: args.id + }, + limit: 1 + }) + }, + renameFile(obj, args) { + return wiki.db.File.update({ + filename: args.filename + }, { + where: { id: args.id } + }) + }, + moveFile(obj, args) { + return wiki.db.File.findById(args.fileId).then(fl => { + if (!fl) { + throw new gql.GraphQLError('Invalid File ID') + } + return wiki.db.Folder.findById(args.folderId).then(fld => { + if (!fld) { + throw new gql.GraphQLError('Invalid Folder ID') + } + return fl.setFolder(fld) + }) + }) + } + }, + File: { + folder(fl) { + return fl.getFolder() + } + } +} diff --git a/server/schemas/resolvers-folder.js b/server/schemas/resolvers-folder.js new file mode 100644 index 00000000..b7fc9a2b --- /dev/null +++ b/server/schemas/resolvers-folder.js @@ -0,0 +1,35 @@ + +/* global wiki */ + +module.exports = { + Query: { + folders(obj, args, context, info) { + return wiki.db.Folder.findAll({ where: args }) + } + }, + Mutation: { + createFolder(obj, args) { + return wiki.db.Folder.create(args) + }, + deleteGroup(obj, args) { + return wiki.db.Folder.destroy({ + where: { + id: args.id + }, + limit: 1 + }) + }, + renameFolder(obj, args) { + return wiki.db.Folder.update({ + name: args.name + }, { + where: { id: args.id } + }) + } + }, + Folder: { + files(grp) { + return grp.getFiles() + } + } +} diff --git a/server/schemas/resolvers-group.js b/server/schemas/resolvers-group.js new file mode 100644 index 00000000..cc9b10b7 --- /dev/null +++ b/server/schemas/resolvers-group.js @@ -0,0 +1,63 @@ + +/* global wiki */ + +const gql = require('graphql') + +module.exports = { + Query: { + groups(obj, args, context, info) { + return wiki.db.Group.findAll({ where: args }) + } + }, + Mutation: { + assignUserToGroup(obj, args) { + return wiki.db.Group.findById(args.groupId).then(grp => { + if (!grp) { + throw new gql.GraphQLError('Invalid Group ID') + } + return wiki.db.User.findById(args.userId).then(usr => { + if (!usr) { + throw new gql.GraphQLError('Invalid User ID') + } + return grp.addUser(usr) + }) + }) + }, + createGroup(obj, args) { + return wiki.db.Group.create(args) + }, + deleteGroup(obj, args) { + return wiki.db.Group.destroy({ + where: { + id: args.id + }, + limit: 1 + }) + }, + removeUserFromGroup(obj, args) { + return wiki.db.Group.findById(args.groupId).then(grp => { + if (!grp) { + throw new gql.GraphQLError('Invalid Group ID') + } + return wiki.db.User.findById(args.userId).then(usr => { + if (!usr) { + throw new gql.GraphQLError('Invalid User ID') + } + return grp.removeUser(usr) + }) + }) + }, + renameGroup(obj, args) { + return wiki.db.Group.update({ + name: args.name + }, { + where: { id: args.id } + }) + } + }, + Group: { + users(grp) { + return grp.getUsers() + } + } +} diff --git a/server/schemas/resolvers-right.js b/server/schemas/resolvers-right.js new file mode 100644 index 00000000..3b286deb --- /dev/null +++ b/server/schemas/resolvers-right.js @@ -0,0 +1,53 @@ + +/* global wiki */ + +const gql = require('graphql') + +module.exports = { + Query: { + rights(obj, args, context, info) { + return wiki.db.Right.findAll({ where: args }) + } + }, + Mutation: { + addRightToGroup(obj, args) { + return wiki.db.Group.findById(args.groupId).then(grp => { + if (!grp) { + throw new gql.GraphQLError('Invalid Group ID') + } + return wiki.db.Right.create({ + path: args.path, + role: args.role, + exact: args.exact, + allow: args.allow, + group: grp + }) + }) + }, + removeRightFromGroup(obj, args) { + return wiki.db.Right.destroy({ + where: { + id: args.rightId + }, + limit: 1 + }) + }, + modifyRight(obj, args) { + return wiki.db.Right.update({ + path: args.path, + role: args.role, + exact: args.exact, + allow: args.allow + }, { + where: { + id: args.id + } + }) + } + }, + Right: { + group(rt) { + return rt.getGroup() + } + } +} diff --git a/server/schemas/resolvers-setting.js b/server/schemas/resolvers-setting.js new file mode 100644 index 00000000..dc0c4f9e --- /dev/null +++ b/server/schemas/resolvers-setting.js @@ -0,0 +1,24 @@ + +/* global wiki */ + +const _ = require('lodash') + +module.exports = { + Query: { + settings(obj, args, context, info) { + return wiki.db.Setting.findAll({ where: args, raw: true }).then(entries => { + return _.map(entries, entry => { + entry.config = JSON.stringify(entry.config) + return entry + }) + }) + } + }, + Mutation: { + setConfigEntry(obj, args) { + return wiki.db.Setting.update({ + value: args.value + }, { where: { key: args.key } }) + } + } +} diff --git a/server/schemas/resolvers-tag.js b/server/schemas/resolvers-tag.js new file mode 100644 index 00000000..0aa3e89e --- /dev/null +++ b/server/schemas/resolvers-tag.js @@ -0,0 +1,63 @@ + +/* global wiki */ + +const gql = require('graphql') + +module.exports = { + Query: { + tags(obj, args, context, info) { + return wiki.db.Tag.findAll({ where: args }) + } + }, + Mutation: { + assignTagToDocument(obj, args) { + return wiki.db.Tag.findById(args.tagId).then(tag => { + if (!tag) { + throw new gql.GraphQLError('Invalid Tag ID') + } + return wiki.db.Document.findById(args.documentId).then(doc => { + if (!doc) { + throw new gql.GraphQLError('Invalid Document ID') + } + return tag.addDocument(doc) + }) + }) + }, + createTag(obj, args) { + return wiki.db.Tag.create(args) + }, + deleteTag(obj, args) { + return wiki.db.Tag.destroy({ + where: { + id: args.id + }, + limit: 1 + }) + }, + removeTagFromDocument(obj, args) { + return wiki.db.Tag.findById(args.tagId).then(tag => { + if (!tag) { + throw new gql.GraphQLError('Invalid Tag ID') + } + return wiki.db.Document.findById(args.documentId).then(doc => { + if (!doc) { + throw new gql.GraphQLError('Invalid Document ID') + } + return tag.removeDocument(doc) + }) + }) + }, + renameTag(obj, args) { + return wiki.db.Group.update({ + key: args.key + }, { + where: { id: args.id } + }) + } + }, + Tag: { + documents(tag) { + return tag.getDocuments() + } + } +} diff --git a/server/schemas/resolvers-translation.js b/server/schemas/resolvers-translation.js new file mode 100644 index 00000000..aed8d57d --- /dev/null +++ b/server/schemas/resolvers-translation.js @@ -0,0 +1,12 @@ + +/* global wiki */ + +module.exports = { + Query: { + translations (obj, args, context, info) { + return wiki.lang.getByNamespace(args.locale, args.namespace) + } + }, + Mutation: {}, + Translation: {} +} diff --git a/server/schemas/resolvers-user.js b/server/schemas/resolvers-user.js new file mode 100644 index 00000000..90133707 --- /dev/null +++ b/server/schemas/resolvers-user.js @@ -0,0 +1,45 @@ + +/* global wiki */ + +module.exports = { + Query: { + users(obj, args, context, info) { + return wiki.db.User.findAll({ where: args }) + } + }, + Mutation: { + createUser(obj, args) { + return wiki.db.User.create(args) + }, + deleteUser(obj, args) { + return wiki.db.User.destroy({ + where: { + id: args.id + }, + limit: 1 + }) + }, + modifyUser(obj, args) { + return wiki.db.User.update({ + email: args.email, + name: args.name, + provider: args.provider, + providerId: args.providerId, + role: args.role + }, { + where: { id: args.id } + }) + }, + resetUserPassword(obj, args) { + return false + }, + setUserPassword(obj, args) { + return false + } + }, + User: { + groups(usr) { + return usr.getGroups() + } + } +} diff --git a/server/schemas/scalar-date.js b/server/schemas/scalar-date.js new file mode 100644 index 00000000..472312ad --- /dev/null +++ b/server/schemas/scalar-date.js @@ -0,0 +1,21 @@ + +const gql = require('graphql') + +module.exports = { + Date: new gql.GraphQLScalarType({ + name: 'Date', + description: 'ISO date-time string at UTC', + parseValue(value) { + return new Date(value) + }, + serialize(value) { + return value.toISOString() + }, + parseLiteral(ast) { + if (ast.kind !== gql.Kind.STRING) { + throw new TypeError('Date value must be an string!') + } + return new Date(ast.value) + } + }) +} diff --git a/server/schemas/types.graphql b/server/schemas/types.graphql new file mode 100644 index 00000000..a47305b4 --- /dev/null +++ b/server/schemas/types.graphql @@ -0,0 +1,342 @@ +# SCALARS + +scalar Date + +# ENUMS + +enum UserRole { + guest + user + admin +} + +enum FileType { + binary + image +} + +enum RightRole { + read + write + manage +} + +# INTERFACES + +interface Base { + id: Int! + createdAt: Date + updatedAt: Date +} + +# TYPES + +type AuthenticationProvider { + key: String! + useForm: Boolean! + title: String! + props: [String] + icon: String + config: String +} + +type Comment implements Base { + id: Int! + createdAt: Date + updatedAt: Date + content: String + document: Document! + author: User! +} + +type Document implements Base { + id: Int! + createdAt: Date + updatedAt: Date + path: String! + title: String! + subtitle: String + parentPath: String + parentTitle: String + isDirectory: Boolean! + isEntry: Boolean! + searchContent: String + comments: [Comment] + tags: [Tag] +} + +type File implements Base { + id: Int! + createdAt: Date + updatedAt: Date + category: FileType! + mime: String! + extra: String + filename: String! + basename: String! + filesize: Int! + folder: Folder +} + +type Folder implements Base { + id: Int! + createdAt: Date + updatedAt: Date + name: String! + files: [File] +} + +type Group implements Base { + id: Int! + createdAt: Date + updatedAt: Date + name: String! + users: [User] + rights: [Right] +} + +type Right implements Base { + id: Int! + createdAt: Date + updatedAt: Date + path: String! + role: RightRole! + exact: Boolean! + allow: Boolean! + group: Group! +} + +type SearchResult { + path: String + title: String + tags: [String] +} + +type Setting implements Base { + id: Int! + createdAt: Date + updatedAt: Date + key: String! + config: String! +} + +# Tags are attached to one or more documents +type Tag implements Base { + id: Int! + createdAt: Date + updatedAt: Date + key: String! + documents: [Document] +} + +type Translation { + key: String! + value: String! +} + +# A User +type User implements Base { + id: Int! + createdAt: Date + updatedAt: Date + email: String! + provider: String! + providerId: String + name: String + role: UserRole! + groups: [Group] +} + +type OperationResult { + succeded: Boolean! + message: String +} + +# Query (Read) +type Query { + authentication(mode: String!): [AuthenticationProvider] + comments(id: Int): [Comment] + documents(id: Int, path: String): [Document] + files(id: Int): [File] + folders(id: Int, name: String): [Folder] + groups(id: Int, name: String): [Group] + rights(id: Int): [Right] + search(q: String, tags: [String]): [SearchResult] + settings(key: String): [Setting] + tags(key: String): [Tag] + translations(locale: String!, namespace: String!): [Translation] + users(id: Int, email: String, provider: String, providerId: String, role: UserRole): [User] +} + +# Mutations (Create, Update, Delete) +type Mutation { + addRightToGroup( + groupId: Int! + path: String! + role: RightRole! + exact: Boolean! + allow: Boolean! + ): Right + + assignTagToDocument( + tagId: Int! + documentId: Int! + ): OperationResult + + assignUserToGroup( + userId: Int! + groupId: Int! + ): OperationResult + + createComment( + userId: Int! + documentId: Int! + content: String! + ): Comment + + createDocument( + path: String! + title: String! + subtitle: String + ): Document + + createFolder( + name: String! + ): Folder + + createGroup( + name: String! + ): Group + + createTag( + name: String! + ): Tag + + createUser( + email: String! + name: String + passwordRaw: String + provider: String! + providerId: String + role: UserRole! + ): User + + deleteComment( + id: Int! + ): OperationResult + + deleteDocument( + id: Int! + ): OperationResult + + deleteFile( + id: Int! + ): OperationResult + + deleteFolder( + id: Int! + ): OperationResult + + deleteGroup( + id: Int! + ): OperationResult + + deleteTag( + id: Int! + ): OperationResult + + deleteUser( + id: Int! + ): OperationResult + + modifyComment( + id: Int! + content: String! + ): Document + + modifyDocument( + id: Int! + title: String + subtitle: String + ): Document + + modifyUser( + id: Int! + email: String + name: String + provider: String + providerId: String + role: UserRole + ): User + + modifyRight( + id: Int! + path: String + role: RightRole + exact: Boolean + allow: Boolean + ): Right + + moveDocument( + id: Int! + path: String! + ): OperationResult + + moveFile( + id: Int! + folderId: Int! + ): OperationResult + + renameFile( + id: Int! + name: String! + ): OperationResult + + renameFolder( + id: Int! + name: String! + ): OperationResult + + renameGroup( + id: Int! + name: String! + ): OperationResult + + renameTag( + id: Int! + key: String! + ): OperationResult + + removeTagFromDocument( + tagId: Int! + documentId: Int! + ): OperationResult + + removeRightFromGroup( + rightId: Int! + ): OperationResult + + removeUserFromGroup( + userId: Int! + groupId: Int! + ): OperationResult + + resetUserPassword( + id: Int! + ): OperationResult + + setConfigEntry( + key: String! + value: String! + ): OperationResult + + setUserPassword( + id: Int! + passwordRaw: String! + ): OperationResult + + uploadFile( + category: FileType! + filename: String! + ): File +} diff --git a/server/views/auth/login.pug b/server/views/auth/login.pug index ad860349..44ebc019 100644 --- a/server/views/auth/login.pug +++ b/server/views/auth/login.pug @@ -1,75 +1,6 @@ -doctype html -html(data-logic='login') - head - 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='msapplication-TileImage', content='/favicons/ms-icon-144x144.png') - title= appconfig.title - - // Favicon - each favsize in [57, 60, 72, 76, 114, 120, 144, 152, 180] - link(rel='apple-touch-icon', sizes=favsize + 'x' + favsize, href='/favicons/apple-icon-' + favsize + 'x' + favsize + '.png') - link(rel='icon', type='image/png', sizes='192x192', href='/favicons/android-icon-192x192.png') - each favsize in [32, 96, 16] - link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href='/favicons/favicon-' + favsize + 'x' + favsize + '.png') - link(rel='manifest', href='/manifest.json') - - // JS / CSS - script(type='text/javascript', src=appconfig.host + '/js/vendor.js') - script(type='text/javascript', src=appconfig.host + '/js/app.js') +extends ../master.pug +block body body - #bg - each bg in _.sampleSize([1, 2, 3],3) - div(style='background-image:url(/images/bg_' + bg + '.jpg);') - #root - h1= appconfig.title - h2= t('auth:loginrequired') - if appflash.length > 0 - h3 - i.icon-warning-outline - = appflash[0].title - h4= appflash[0].message - if appconfig.auth.local.enabled - form(method='post', action='/login') - input#login-user(type='text', name='email', placeholder=t('auth:fields.emailuser')) - input#login-pass(type='password', name='password', placeholder=t('auth:fields.password')) - button(type='submit')= t('auth:actions.login') - if appconfig.authStrategies.socialEnabled - #social - if appconfig.auth.local.enabled - span= t('auth:loginusingalt') - else - span= t('auth:loginusing') - if appconfig.auth.microsoft && appconfig.auth.microsoft.enabled - button.ms(onclick='window.location.assign("/login/ms")') - i.icon-windows2 - span= t('auth:providers.windowslive') - if appconfig.auth.azure && appconfig.auth.azure.enabled - button.ms(onclick='window.location.assign("/login/azure")') - i.icon-windows2 - span= t('auth:providers.azure') - if appconfig.auth.google && appconfig.auth.google.enabled - button.google(onclick='window.location.assign("/login/google")') - i.icon-google - span= t('auth:providers.google') - if appconfig.auth.facebook && appconfig.auth.facebook.enabled - button.facebook(onclick='window.location.assign("/login/facebook")') - i.icon-facebook - span= t('auth:providers.facebook') - if appconfig.auth.github && appconfig.auth.github.enabled - button.github(onclick='window.location.assign("/login/github")') - i.icon-github - span= t('auth:providers.github') - if appconfig.auth.slack && appconfig.auth.slack.enabled - button.slack(onclick='window.location.assign("/login/slack")') - i.icon-slack - span= t('auth:providers.slack') - #copyright - = t('footer.poweredby') + ' ' - a.icon(href='https://github.com/Requarks/wiki') - i.icon-github - a(href='https://wiki.requarks.io/') Wiki.js + #app.is-fullscreen + login diff --git a/server/views/configure/index.pug b/server/views/configure/index.pug index d6c1f62a..aa268c77 100644 --- a/server/views/configure/index.pug +++ b/server/views/configure/index.pug @@ -1,31 +1,9 @@ -doctype html -html(data-logic='configure') - head - meta(http-equiv='X-UA-Compatible', content='IE=edge') - meta(charset='UTF-8') - title Wiki.js | Configure - - // Favicon - each favsize in [32, 96, 16] - link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href='/favicons/favicon-' + favsize + 'x' + favsize + '.png') - - // JS / CSS - script(type='text/javascript'). - var appconfig = !{JSON.stringify(conf)}; - var runmode = !{JSON.stringify(runmode)}; - script(type='text/javascript', src='/js/vendor.js') - script(type='text/javascript', src='/js/configure.js') +extends ../master.pug +block body body - #root - #header-container - nav.nav#header - .nav-left - a.nav-item - h1 - i.icon-layers - | Wiki.js - main + #app.config-manager + config-manager(inline-template) .container transition(name='tst-welcome') .welcome(v-if='state === "welcome" || state === "restart"') @@ -44,10 +22,22 @@ html(data-logic='configure') i(v-if='loading') .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]. + p Detailed information about installation and usage can be found on the #[a(href='https://wiki.requarks.io/docs') 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-telemetry(type='checkbox', v-model='conf.telemetry', name='ipt-telemetry') + label.label(for='ipt-telemetry') Enable telemetry + span.desc Help Wiki.js developers improve this app with anonymized #[a(href='https://wiki.requarks.io/docs/telemetry') telemetry]. + 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-light-blue(v-on:click='proceedToSyscheck', v-bind:disabled='loading') Start + button.button.is-small.is-light-blue(v-on:click='proceedToSyscheck', v-bind:disabled='loading') Start //- ============================================== //- SYSTEM CHECK @@ -69,9 +59,10 @@ html(data-logic='configure') p(v-if='!loading && !syscheck.ok') #[i.icon-square-cross] Error: {{ syscheck.error }} .panel-footer .progress-bar: div(v-bind:style='{width: currentProgress}') - button.button.is-light-blue.is-outlined(v-on:click='proceedToWelcome', v-bind:disabled='loading') Back - button.button.is-teal(v-on:click='proceedToSyscheck', v-if='!loading && !syscheck.ok') Check Again - button.button.is-light-blue(v-on:click='proceedToGeneral', v-if='loading || syscheck.ok', v-bind:disabled='loading') Continue + 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 //- ============================================== //- GENERAL @@ -92,20 +83,24 @@ html(data-logic='configure') 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]. - if !runmode.staticPort - section - p.control - label.label Port - input(type='text', placeholder='e.g. 80', v-model.number='conf.port', data-vv-scope='general', name='ipt-port', v-validate='{ required: true }') - span.desc The port on which Wiki.js will listen to. Usually port 80 if connecting directly, or a random port (e.g. 3000) if using a web server in front of it.
Set $(PORT) to use PORT environment variable. + 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 + input(type='text', placeholder='e.g. 80', v-model.number='conf.port', data-vv-scope='general', name='ipt-port', v-validate='{ required: true }') + span.desc The port on which Wiki.js will listen to. Usually port 80 if connecting directly, or a random port (e.g. 3000) if using a web server in front of it.
Set $(PORT) to use PORT environment variable. section p.control label.label Site UI Language select(v-model='conf.lang') - each lg in langs + 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') @@ -113,8 +108,8 @@ html(data-logic='configure') span.desc Should the site be accessible (read only) without login. .panel-footer .progress-bar: div(v-bind:style='{width: currentProgress}') - button.button.is-light-blue.is-outlined(v-on:click='proceedToSyscheck', v-bind:disabled='loading') Back - button.button.is-light-blue(v-on:click='proceedToConsiderations', v-bind:disabled='loading || errors.any("general")') Continue + button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToSyscheck', v-bind:disabled='loading') Back + button.button.is-small.is-light-blue(v-on:click='proceedToConsiderations', v-bind:disabled='loading || errors.any("general")') Continue //- ============================================== //- CONSIDERATIONS @@ -134,88 +129,18 @@ html(data-logic='configure') 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-light-blue.is-outlined(v-on:click='proceedToGeneral', v-bind:disabled='loading') Back - button.button.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 not 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.
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-light-blue.is-outlined(v-on:click='proceedToConsiderations', v-bind:disabled='loading') Back - button.button.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-light-blue.is-outlined(v-on:click='proceedToDb', v-bind:disabled='loading') Back - button.button.is-teal(v-on:click='proceedToDbcheck', v-if='!loading && !dbcheck.ok') Try Again - button.button.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-light-blue.is-outlined(v-on:click='proceedToDb', v-bind:disabled='loading') Back - button.button.is-light-blue(v-on:click='proceedToGit', v-bind:disabled='loading || errors.any("paths")') Continue + 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='proceedToGit', v-bind:disabled='loading') Continue //- ============================================== //- GIT @@ -279,9 +204,9 @@ html(data-logic='configure') 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-light-blue.is-outlined(v-on:click='proceedToPaths', v-bind:disabled='loading') Back - button.button.is-light-blue.is-outlined(v-on:click='conf.gitUseRemote = false; proceedToGitCheck()', v-bind:disabled='loading') Skip this step - button.button.is-light-blue(v-on:click='conf.gitUseRemote = true; proceedToGitCheck()', v-bind:disabled='loading || errors.any("git")') Continue + 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 //- ============================================== //- GIT CHECK @@ -303,9 +228,9 @@ html(data-logic='configure') p(v-if='!loading && !gitcheck.ok') #[i.icon-square-cross] Error: {{ gitcheck.error }} .panel-footer .progress-bar: div(v-bind:style='{width: currentProgress}') - button.button.is-light-blue.is-outlined(v-on:click='proceedToGit', v-bind:disabled='loading') Back - button.button.is-teal(v-on:click='proceedToGitCheck', v-if='!loading && !gitcheck.ok') Try Again - button.button.is-light-blue(v-on:click='proceedToAdmin', v-if='loading || gitcheck.ok', v-bind:disabled='loading') Continue + button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToGit', v-bind:disabled='loading') Back + button.button.is-small.is-teal(v-on:click='proceedToGitCheck', v-if='!loading && !gitcheck.ok') Try Again + button.button.is-small.is-light-blue(v-on:click='proceedToAdmin', v-if='loading || gitcheck.ok', v-bind:disabled='loading') Continue //- ============================================== //- ADMINISTRATOR ACCOUNT @@ -337,8 +262,8 @@ html(data-logic='configure') span.desc Verify your password again. .panel-footer .progress-bar: div(v-bind:style='{width: currentProgress}') - button.button.is-light-blue.is-outlined(v-on:click='proceedToGit', v-bind:disabled='loading') Back - button.button.is-light-blue(v-on:click='proceedToFinal', v-bind:disabled='loading || errors.any("admin")') Continue + button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToGit', v-bind:disabled='loading') Back + button.button.is-small.is-light-blue(v-on:click='proceedToFinal', v-bind:disabled='loading || errors.any("admin")') Continue //- ============================================== //- FINAL @@ -359,9 +284,9 @@ html(data-logic='configure') p(v-if='!loading && !final.ok') #[i.icon-square-cross] Error: {{ final.error }} .panel-footer .progress-bar: div(v-bind:style='{width: currentProgress}') - button.button.is-light-blue.is-outlined(v-on:click='proceedToAdmin', v-bind:disabled='loading') Back - button.button.is-teal(v-on:click='proceedToFinal', v-if='!loading && !final.ok') Try Again - button.button.is-green(v-on:click='finish', v-if='loading || final.ok', v-bind:disabled='loading') Start + button.button.is-small.is-light-blue.is-outlined(v-on:click='proceedToAdmin', 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 @@ -376,11 +301,4 @@ html(data-logic='configure') 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-green(disabled='disabled') Start - - footer.footer - span - | Powered by - a(href='https://github.com/Requarks/wiki') Wiki.js - | . - block outside + button.button.is-small.is-green(disabled='disabled') Start diff --git a/server/views/error.pug b/server/views/error.pug index 3ef6c43f..51b53efe 100644 --- a/server/views/error.pug +++ b/server/views/error.pug @@ -1,32 +1,12 @@ -doctype html -html(data-logic='error') - head - 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='msapplication-TileImage', content=appconfig.host + '/favicons/ms-icon-144x144.png') - title= appconfig.title - - // Favicon - each favsize in [57, 60, 72, 76, 114, 120, 144, 152, 180] - link(rel='apple-touch-icon', sizes=favsize + 'x' + favsize, href=appconfig.host + '/favicons/apple-icon-' + favsize + 'x' + favsize + '.png') - link(rel='icon', type='image/png', sizes='192x192', href=appconfig.host + '/favicons/android-icon-192x192.png') - each favsize in [32, 96, 16] - link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href=appconfig.host + '/favicons/favicon-' + favsize + 'x' + favsize + '.png') - link(rel='manifest', href=appconfig.host + '/manifest.json') - - // JS / CSS - script(type='text/javascript', src=appconfig.host + '/js/vendor.js') - script(type='text/javascript', src=appconfig.host + '/js/app.js') +extends ./master.pug +block body body(class='is-error') .container - a(href='/'): img(src=appconfig.host + '/images/logo.png') + a(href='/'): img(src=config.site.path + '/images/logo.png') h1= message h2= t('errors:generic') - a.button.is-amber.is-inverted.is-featured(href=appconfig.host + '/')= t('errors:actions.gohome') + a.button.is-amber.is-inverted.is-featured(href=config.site.path+ '/')= t('errors:actions.gohome') if error.stack h3= t('errors:debugmsg') diff --git a/server/views/layout.pug b/server/views/layout.pug index 36c75734..1d704f7e 100644 --- a/server/views/layout.pug +++ b/server/views/layout.pug @@ -1,35 +1,8 @@ -doctype html -html - head - 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='msapplication-TileImage', content=appconfig.host + '/favicons/ms-icon-144x144.png') - title= appconfig.title +extends ./master.pug - //- Favicon - each favsize in [57, 60, 72, 76, 114, 120, 144, 152, 180] - link(rel='apple-touch-icon', sizes=favsize + 'x' + favsize, href=appconfig.host + '/favicons/apple-icon-' + favsize + 'x' + favsize + '.png') - link(rel='icon', type='image/png', sizes='192x192', href=appconfig.host + '/favicons/android-icon-192x192.png') - each favsize in [32, 96, 16] - link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href=appconfig.host + '/favicons/favicon-' + favsize + 'x' + favsize + '.png') - link(rel='manifest', href=appconfig.host + '/manifest.json') - - //- Site Lang - script. - var siteLang = '!{appconfig.lang}'; - var siteRoot = '!{appconfig.host}'; - - //- JS / CSS - script(type='text/javascript', src=appconfig.host + '/js/vendor.js') - script(type='text/javascript', src=appconfig.host + '/js/app.js') - - block head - - body(class={ 'rtl': appconfig.langRtl }) - #root.has-stickynav(class=['is-primary-' + appconfig.theme.primary, 'is-alternate-' + appconfig.theme.alt]) +block body + body + #app.has-stickynav(class=['is-primary-' + appconfig.theme.primary, 'is-alternate-' + appconfig.theme.alt]) include ./common/header.pug alert main diff --git a/server/views/master.pug b/server/views/master.pug new file mode 100644 index 00000000..dc45732a --- /dev/null +++ b/server/views/master.pug @@ -0,0 +1,30 @@ +doctype html +html + head + 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='msapplication-TileImage', content=config.site.path + '/favicons/ms-icon-144x144.png') + title= config.site.title + + //- Favicon + each favsize in [57, 60, 72, 76, 114, 120, 144, 152, 180] + link(rel='apple-touch-icon', sizes=favsize + 'x' + favsize, href=config.site.path + '/favicons/apple-icon-' + favsize + 'x' + favsize + '.png') + link(rel='icon', type='image/png', sizes='192x192', href=config.site.path + '/favicons/android-icon-192x192.png') + each favsize in [32, 96, 16] + link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href=config.site.path + '/favicons/favicon-' + favsize + 'x' + favsize + '.png') + link(rel='manifest', href=config.site.path + '/manifest.json') + + //- Site Lang + script. + var siteConfig = !{JSON.stringify(config.site)} + + //- JS / CSS + script(type='text/javascript', src=config.site.path + '/js/libs.js') + script(type='text/javascript', src=config.site.path + '/js/app.js') + + block head + + block body diff --git a/server/worker.js b/server/worker.js new file mode 100644 index 00000000..6ca90eba --- /dev/null +++ b/server/worker.js @@ -0,0 +1,67 @@ +/* global wiki */ + +const Promise = require('bluebird') + +module.exports = Promise.join( + wiki.db.onReady, + wiki.configSvc.loadFromDb(['features', 'git', 'logging', 'site', 'uploads']) +).then(() => { + const path = require('path') + + wiki.REPOPATH = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo) + wiki.DATAPATH = path.resolve(wiki.ROOTPATH, wiki.config.paths.data) + wiki.UPLTEMPPATH = path.join(wiki.DATAPATH, 'temp-upload') + + // ---------------------------------------- + // Load global modules + // ---------------------------------------- + + // wiki.upl = require('./modules/uploads-agent').init() + // wiki.git = require('./modules/git').init() + // wiki.entries = require('./modules/entries').init() + wiki.lang = require('i18next') + wiki.mark = require('./modules/markdown') + + // ---------------------------------------- + // Localization Engine + // ---------------------------------------- + + const i18nBackend = require('i18next-node-fs-backend') + wiki.lang.use(i18nBackend).init({ + load: 'languageOnly', + ns: ['common', 'admin', 'auth', 'errors', 'git'], + defaultNS: 'common', + saveMissing: false, + preload: [wiki.config.lang], + lng: wiki.config.lang, + fallbackLng: 'en', + backend: { + loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.json') + } + }) + + // ---------------------------------------- + // Start Queues + // ---------------------------------------- + + const Bull = require('bull') + const autoload = require('auto-load') + + let queues = autoload(path.join(wiki.SERVERPATH, 'queues')) + + for (let queueName in queues) { + new Bull(queueName, { + prefix: `q-${wiki.config.ha.nodeuid}`, + redis: wiki.config.redis + }).process(queues[queueName]) + } + + // ---------------------------------------- + // Shutdown gracefully + // ---------------------------------------- + + process.on('disconnect', () => { + wiki.logger.warn('Lost connection to Master. Exiting...') + process.exit() + }) +}) diff --git a/themes/default/theme.yml b/themes/default/theme.yml new file mode 100644 index 00000000..e5c0a6d8 --- /dev/null +++ b/themes/default/theme.yml @@ -0,0 +1,28 @@ +name: Default +author: Nicolas Giard +site: https://wiki.requarks.io/ +version: 1.0.0 +requirements: + minimum: '>= 2.0.0' + maximum: '< 3.0.0' +fields: + primary: + title: Primary Color + description: Used for top navigation bar, headers, links, etc. + type: color + default: indigo + alt: + title: Alternate Color + description: Used for the sidebar (in a darker tone) + type: color + default: blue-grey + codeDark: + title: Code Blocks - Use Dark Theme + description: todo + type: boolean + default: true + codeColorize: + title: Code Blocks - Colorize syntax + description: todo + type: boolean + default: true \ No newline at end of file diff --git a/themes/default/thumbnail.png b/themes/default/thumbnail.png new file mode 100644 index 00000000..489e3f83 Binary files /dev/null and b/themes/default/thumbnail.png differ diff --git a/tools/fuse.js b/tools/fuse.js index 719bf6ef..70aede3c 100644 --- a/tools/fuse.js +++ b/tools/fuse.js @@ -6,16 +6,15 @@ * Client & Server compiler / bundler / watcher */ +const autoprefixer = require('autoprefixer') const colors = require('colors/safe') const fsbx = require('fuse-box') const nodemon = require('nodemon') -const babel = require('babel-core') -const uglify = require('uglify-es') const fs = require('fs-extra') -// ====================================================== +// ------------------------------------------------------- // Parse cmd arguments -// ====================================================== +// ------------------------------------------------------- const args = require('yargs') .option('d', { @@ -23,30 +22,21 @@ const args = require('yargs') describe: 'Start in Developer mode', type: 'boolean' }) - .option('c', { - alias: 'dev-configure', - describe: 'Start in Configure Developer mode', - type: 'boolean' - }) .help('h') .alias('h', 'help') .argv -let mode = 'build' -const dev = args.d || args.c -if (args.d) { +const dev = args.dev + +if (dev) { console.info(colors.bgWhite.black(' Starting Fuse in DEVELOPER mode... ')) - mode = 'dev' -} else if (args.c) { - console.info(colors.bgWhite.black(' Starting Fuse in CONFIGURE DEVELOPER mode... ')) - mode = 'dev-configure' } else { console.info(colors.bgWhite.black(' Starting Fuse in BUILD mode... ')) } -// ====================================================== +// ------------------------------------------------------- // BUILD VARS -// ====================================================== +// ------------------------------------------------------- const ALIASES = { 'brace-ext-modelist': 'brace/ext/modelist.js', @@ -71,16 +61,31 @@ const SHIMS = { } } -// ====================================================== +// ------------------------------------------------------- // Global Tasks -// ====================================================== +// ------------------------------------------------------- console.info(colors.white('└── ') + colors.green('Running global tasks...')) let globalTasks = require('./fuse_tasks') -// ====================================================== -// Fuse Tasks -// ====================================================== +// ------------------------------------------------------- +// FUSEBOX PRODUCER +// ------------------------------------------------------- + +const babelrc = fs.readJsonSync('.babelrc') +const scssChain = [ + fsbx.SassPlugin({ + includePaths: ['node_modules'], + outputStyle: dev ? 'nested' : 'compressed' + }), + fsbx.PostCSS([ + autoprefixer({ + remove: false, + browsers: babelrc.presets[0][1].targets.browsers + }) + ]), + fsbx.CSSPlugin() +] globalTasks.then(() => { let fuse = fsbx.FuseBox.init({ @@ -91,100 +96,60 @@ globalTasks.then(() => { tsConfig: './tsconfig.json', plugins: [ fsbx.EnvPlugin({ NODE_ENV: (dev) ? 'development' : 'production' }), - fsbx.VuePlugin(), - ['.scss', fsbx.SassPlugin({ outputStyle: (dev) ? 'nested' : 'compressed' }), fsbx.CSSPlugin()], - fsbx.BabelPlugin({ comments: false, presets: ['es2015'] }), + fsbx.VueComponentPlugin({ + script: fsbx.BabelPlugin(babelrc), + template: fsbx.ConsolidatePlugin({ + engine: 'pug' + }), + style: scssChain + }), + scssChain, + fsbx.BabelPlugin(babelrc), fsbx.JSONPlugin() - /* !dev && fsbx.QuantumPlugin({ - target: 'browser', - uglify: true, - api: (core) => { - core.solveComputed('default/js/components/editor-codeblock.vue', { - mapping: '/js/ace/ace.js', - fn: (statement, core) => { - statement.setExpression(`'/js/ace/ace.js'`) - } - }) - core.solveComputed('default/js/components/editor.component.js', { - mapping: '/js/simplemde/simplemde.min.js', - fn: (statement, core) => { - statement.setExpression(`'/js/simplemde/simplemde.min.js'`) - } - }) - } - }) */ - // !dev && fsbx.UglifyESPlugin() ], debug: false, log: true }) - const bundleVendor = fuse.bundle('vendor').shim(SHIMS).instructions('~ index.js') // eslint-disable-line no-unused-vars - const bundleApp = fuse.bundle('app').instructions('!> [index.js]') - // const bundleApp = fuse.bundle('app').shim(SHIMS).instructions('> index.js') - const bundleSetup = fuse.bundle('configure').instructions('> configure.js') + // ------------------------------------------------------- + // FUSEBOX DEV + // ------------------------------------------------------- - switch (mode) { - case 'dev': - bundleApp.watch() - break - case 'dev-configure': - bundleSetup.watch() - break + if (dev) { + fuse.dev({ + port: 5555, + httpServer: false + }) } + // ------------------------------------------------------- + // FUSEBOX BUNDLES + // ------------------------------------------------------- + + if (dev) { + fuse.bundle('libs').shim(SHIMS).instructions('~ index.js') + fuse.bundle('app').instructions('!> [index.js]').hmr({ reload: true }).watch() + } else { + fuse.bundle('bundle.min.js').shim(SHIMS).instructions('> index.js') + } + + // ------------------------------------------------------- + // FUSEBOX RUN + // ------------------------------------------------------- + fuse.run().then(() => { console.info(colors.green.bold('\nAssets compilation + bundling completed.')) if (dev) { nodemon({ - exec: (args.d) ? 'node server' : 'node wiki configure', - ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'], - ext: 'js json', - watch: (args.d) ? ['server'] : ['server/configure.js'], + exec: 'node server', + ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/', 'tools/'], + ext: 'js json graphql', + watch: ['server'], env: { 'NODE_ENV': 'development' } }) - } else { - console.info(colors.yellow.bold('\nTranspiling vendor bundle...')) - let appCode = babel.transform(fs.readFileSync('./assets/js/app.js', 'utf8'), { - babelrc: false, - compact: false, - filename: 'app.js', - plugins: ['transform-object-assign'] - }).code - let vendorCode = babel.transform(fs.readFileSync('./assets/js/vendor.js', 'utf8'), { - babelrc: false, - comments: false, - compact: false, - filename: 'vendor.js', - plugins: [ - 'transform-es2015-arrow-functions', - 'transform-es2015-block-scoped-functions', - 'transform-es2015-block-scoping', - 'transform-es2015-classes', - 'transform-es2015-computed-properties', - 'transform-es2015-destructuring', - 'transform-es2015-duplicate-keys', - 'transform-es2015-for-of', - 'transform-es2015-function-name', - 'transform-es2015-literals', - 'transform-es2015-object-super', - 'transform-es2015-parameters', - 'transform-es2015-shorthand-properties', - 'transform-es2015-spread', - 'transform-es2015-sticky-regex', - 'transform-es2015-template-literals', - 'transform-es2015-typeof-symbol', - 'transform-es2015-unicode-regex' - ] - }).code - console.info(colors.yellow.bold('Minifing bundles...')) - fs.writeFileSync('./assets/js/vendor.js', uglify.minify(vendorCode).code, 'utf8') - fs.writeFileSync('./assets/js/app.js', uglify.minify(appCode).code, 'utf8') - fs.writeFileSync('./assets/js/configure.js', uglify.minify(fs.readFileSync('./assets/js/configure.js', 'utf8')).code, 'utf8') - console.info(colors.green.bold('\nBUILD SUCCEEDED.')) - return true } + return true }).catch(err => { console.error(colors.red(' X Bundle compilation failed! ' + err.message)) process.exit(1) diff --git a/tools/fuse_tasks.js b/tools/fuse_tasks.js index 90e7b61b..c9d0ee03 100644 --- a/tools/fuse_tasks.js +++ b/tools/fuse_tasks.js @@ -65,27 +65,6 @@ module.exports = Promise.mapSeries([ } }) }, - /** - * i18n - */ - () => { - console.info(colors.white(' └── ') + colors.green('Copying i18n client files...')) - return fs.ensureDirAsync('./assets/js/i18n').then(() => { - return fs.readJsonAsync('./server/locales/en/browser.json').then(enContent => { - return fs.readdirAsync('./server/locales').then(langs => { - return Promise.map(langs, lang => { - console.info(colors.white(' ' + lang + '.json')) - let outputPath = path.join('./assets/js/i18n', lang + '.json') - return fs.readJsonAsync(path.join('./server/locales', lang, 'browser.json'), 'utf8').then((content) => { - return fs.outputJsonAsync(outputPath, _.defaultsDeep(content, enContent)) - }).catch(err => { // eslint-disable-line handle-callback-err - return fs.outputJsonAsync(outputPath, enContent) - }) - }) - }) - }) - }) - }, /** * Delete Fusebox cache */ diff --git a/wiki.js b/wiki.js index fcfb1358..4f14f1bb 100644 --- a/wiki.js +++ b/wiki.js @@ -1,13 +1,72 @@ #!/usr/bin/env node -'use strict' // =========================================== // Wiki.js -// 1.0.0 +// 2.0 // Licensed under AGPLv3 // =========================================== -const init = require('./server/init') +const Promise = require('bluebird') +const fs = Promise.promisifyAll(require('fs-extra')) +const pm2 = Promise.promisifyAll(require('pm2')) +const ora = require('ora') +const path = require('path') + +const ROOTPATH = process.cwd() + +const init = { + /** + * Start in background mode + */ + start () { + let spinner = ora('Initializing...').start() + return fs.emptyDirAsync(path.join(ROOTPATH, './logs')).then(() => { + return pm2.connectAsync().then(() => { + return pm2.startAsync({ + name: 'wiki', + script: 'server', + cwd: ROOTPATH, + output: path.join(ROOTPATH, './logs/wiki-output.log'), + error: path.join(ROOTPATH, './logs/wiki-error.log'), + minUptime: 5000, + maxRestarts: 5 + }).then(() => { + spinner.succeed('Wiki.js has started successfully.') + }).finally(() => { + pm2.disconnect() + }) + }) + }).catch(err => { + spinner.fail(err) + process.exit(1) + }) + }, + /** + * Stop Wiki.js process(es) + */ + stop () { + let spinner = ora('Shutting down Wiki.js...').start() + return pm2.connectAsync().then(() => { + return pm2.stopAsync('wiki').then(() => { + spinner.succeed('Wiki.js has stopped successfully.') + }).finally(() => { + pm2.disconnect() + }) + }).catch(err => { + spinner.fail(err) + process.exit(1) + }) + }, + /** + * Restart Wiki.js process(es) + */ + restart: function () { + let self = this + return self.stop().delay(1000).then(() => { + self.startDetect() + }) + } +} require('yargs') // eslint-disable-line no-unused-expressions .usage('Usage: node $0 [args]') @@ -16,7 +75,7 @@ require('yargs') // eslint-disable-line no-unused-expressions alias: ['boot', 'init'], desc: 'Start Wiki.js process', handler: argv => { - init.startDetect() + init.start() } }) .command({ @@ -35,18 +94,9 @@ require('yargs') // eslint-disable-line no-unused-expressions init.restart() } }) - .command({ - command: 'configure [port]', - alias: ['config', 'conf', 'cfg', 'setup'], - desc: 'Configure Wiki.js using the web-based setup wizard', - builder: (yargs) => yargs.default('port', 3000), - handler: argv => { - init.configure(argv.port) - } - }) .recommendCommands() .demandCommand(1, 'You must provide one of the accepted commands above.') .help() .version() - .epilogue('Read the docs at https://wiki.requarks.io') + .epilogue('Read the docs at https://docs.requarks.io/wiki') .argv diff --git a/yarn.lock b/yarn.lock index e1e9f9f2..343424e2 100644 Binary files a/yarn.lock and b/yarn.lock differ