Browse Source

refactor: remove config namespaces

NGPixel 7 years ago
parent
commit
416755f17a

+ 1 - 1
client/components/nav-header.vue

@@ -58,7 +58,7 @@
           color='blue'
         )
     v-spacer
-    v-progress-circular.mr-3(indeterminate, color='blue', v-show='isLoading')
+    v-progress-circular.mr-3(indeterminate, color='blue', :size='22', :width='2' v-show='isLoading')
     slot(name='actions')
     transition(name='navHeaderSearch')
       v-btn(icon, @click='searchToggle', v-if='!searchIsShown')

+ 11 - 0
client/components/profile.vue

@@ -41,6 +41,8 @@
 import VueRouter from 'vue-router'
 import { mapState } from 'vuex'
 
+/* global WIKI */
+
 const router = new VueRouter({
   mode: 'history',
   base: '/p',
@@ -53,6 +55,15 @@ const router = new VueRouter({
   ]
 })
 
+router.beforeEach((to, from, next) => {
+  WIKI.$store.commit('loadingStart', 'profile')
+  next()
+})
+
+router.afterEach((to, from) => {
+  WIKI.$store.commit('loadingStop', 'profile')
+})
+
 export default {
   data() {
     return {

+ 22 - 3
client/components/profile/preferences.vue

@@ -12,13 +12,32 @@
                   v-toolbar(color='primary', dark, dense, flat)
                     v-toolbar-title
                       .subheading Display
-                  v-subheader Locale
+                  v-card-text
+                    v-subheader.pl-0 Locale
+                    v-select.grey.lighten-5(solo, flat)
+                    v-divider
+                    v-subheader.pl-0 Timezone
+                    v-select.grey.lighten-5(solo, flat)
+                  v-divider.my-0
+                  v-card-actions.grey.lighten-4
+                    v-spacer
+                    v-btn(color='primary')
+                      v-icon(left) chevron_right
+                      span Save
             v-flex(lg6 xs12)
               v-card
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
-                    .subheading ---
-                v-card-text ---
+                    .subheading Editing
+                v-card-text
+                  v-subheader.pl-0 Default Editor
+                  v-select.grey.lighten-5(solo, flat)
+                v-divider.my-0
+                v-card-actions.grey.lighten-4
+                  v-spacer
+                  v-btn(color='primary')
+                    v-icon(left) chevron_right
+                    span Save
 
 </template>
 

+ 67 - 33
client/components/profile/profile.vue

@@ -1,40 +1,74 @@
 <template lang='pug'>
-  v-container(fluid, fill-height, grid-list-lg)
+  v-container(fluid, grid-list-lg)
     v-layout(row wrap)
-      v-flex(xs12)
+      v-flex(xs6)
         .headline.primary--text Profile
         .subheading.grey--text Personal profile
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(lg6 xs12)
-              v-form
-                v-card
-                  v-toolbar(color='primary', dark, dense, flat)
-                    v-toolbar-title
-                      .subheading User Details
-                  v-card-text
-                    v-text-field(label='Name', :counter='255', v-model='name', prepend-icon='person')
-                    v-divider
-                    v-text-field(label='Email', :counter='255', prepend-icon='email')
-                  v-divider.my-0
-                  v-card-actions.grey.lighten-4
-                    v-spacer
-                    v-btn(color='primary')
-                      v-icon(left) chevron_right
-                      span Save
-            v-flex(lg6 xs12)
-              v-card
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title
-                    .subheading Picture
-                v-card-text ---
-              v-card.mt-3
-                v-toolbar(color='teal', dark, dense, flat)
-                  v-toolbar-title
-                    .subheading Activity
-                v-card-text.grey--text.text--darken-2
-                  .body-1 Joined #[strong January 1st, 2010]
-                  .body-1 Last login on #[strong January 2nd, 2010]
+      v-flex(xs6).text-xs-right
+        v-btn(outline, color='primary').mr-0
+          v-icon(left) public
+          span View Public Profile
+      v-flex(lg6 xs12)
+        v-card
+          v-toolbar(color='primary', dark, dense, flat)
+            v-toolbar-title
+              .subheading User Details
+          v-card-text
+            v-text-field(label='Name', :counter='255', v-model='name', prepend-icon='person')
+            v-text-field(label='Job Title', :counter='255', prepend-icon='accessibility')
+            v-text-field(label='Location / Office', :counter='255', prepend-icon='location_on')
+          v-divider.my-0
+          v-card-actions.grey.lighten-4
+            v-spacer
+            v-btn(color='primary')
+              v-icon(left) chevron_right
+              span Save
+        v-card.mt-3
+          v-toolbar(color='purple darken-4', dark, dense, flat)
+            v-toolbar-title
+              .subheading Authentication
+          v-card-text
+            v-subheader.pl-0 Provider
+            v-toolbar(flat, color='purple lighten-5', dense).purple--text.text--darken-4
+              v-icon(color='purple darken-4') supervised_user_circle
+              .subheading.ml-3 Local
+            v-divider
+            v-subheader.pl-0 Two-Factor Authentication (2FA)
+            .caption.mb-2 2FA adds an extra layer of security by requiring a unique code generated on your smartphone when signing in.
+            v-btn(color='purple darken-4', dark, depressed).ml-0 Enable 2FA
+            v-btn(color='purple darken-4', dark, depressed, disabled).ml-0 Disable 2FA
+            v-divider
+            v-subheader.pl-0 Change Password
+            v-text-field(label='Current Password', prepend-icon='last_page')
+            v-text-field(label='New Password', prepend-icon='last_page')
+            v-text-field(label='Confirm New Password', prepend-icon='last_page')
+            v-btn(color='purple darken-4', dark, depressed).ml-0 Change Password
+      v-flex(lg6 xs12)
+        v-card
+          v-toolbar(color='primary', dark, dense, flat)
+            v-toolbar-title
+              .subheading Picture
+          v-card-title
+            v-avatar(size='64', color='grey')
+              v-icon(size='64', color='grey lighten-2') account_circle
+            v-btn(depressed).ml-4.elevation-1 Upload Picture
+            v-btn(depressed, disabled).elevation-1 Remove Picture
+        v-card.mt-3
+          v-toolbar(color='teal', dark, dense, flat)
+            v-toolbar-title
+              .subheading Activity
+          v-card-text.grey--text.text--darken-2
+            .body-2.grey--text Joined on
+            .body-1: strong January 1st, 2018 at 12:00 AM
+            .body-2.grey--text.mt-3 Profile last updated on
+            .body-1: strong January 1st, 2018 at 12:00 AM
+            .body-2.grey--text.mt-3 Last login on
+            .body-1: strong January 1st, 2018 at 12:00 AM
+            v-divider
+            .body-2.grey--text.mt-3 Pages created
+            .body-1: strong 0
+            .body-2.grey--text.mt-3 Comments posted
+            .body-1: strong 0
 </template>
 
 <script>

+ 11 - 1
client/components/setup.vue

@@ -209,8 +209,12 @@
                   v-flex.pr-3(xs6)
                     v-text-field(
                       ref='adminPassword',
+                      counter='255'
                       v-model='conf.adminPassword',
                       label='Password',
+                      :append-icon="pwdMode ? 'visibility' : 'visibility_off'"
+                      :append-icon-cb="() => (pwdMode = !pwdMode)"
+                      :type="pwdMode ? 'password' : 'text'"
                       hint='At least 8 characters long.',
                       v-validate='{ required: true, min: 8 }',
                       data-vv-name='adminPassword',
@@ -221,8 +225,12 @@
                   v-flex(xs6)
                     v-text-field(
                       ref='adminPasswordConfirm',
+                      counter='255'
                       v-model='conf.adminPasswordConfirm',
                       label='Confirm Password',
+                      :append-icon="pwdConfirmMode ? 'visibility' : 'visibility_off'"
+                      :append-icon-cb="() => (pwdConfirmMode = !pwdConfirmMode)"
+                      :type="pwdConfirmMode ? 'password' : 'text'"
                       hint='Verify your password again.',
                       v-validate='{ required: true, min: 8 }',
                       data-vv-name='adminPasswordConfirm',
@@ -339,7 +347,9 @@ export default {
         title: siteConfig.title || 'Wiki',
         upgrade: false,
         upgMongo: 'mongodb://'
-      }
+      },
+      pwdMode: true,
+      pwdConfirmMode: true
     }
   },
   methods: {

+ 1 - 0
client/scss/app.scss

@@ -15,6 +15,7 @@
 // @import 'node_modules/diff2html/dist/diff2html.min';
 
 @import 'pages/welcome';
+@import 'pages/error';
 
 @import 'layout/_rtl';
 

+ 64 - 0
client/scss/pages/_error.scss

@@ -0,0 +1,64 @@
+.app-error {
+  background: linear-gradient(to bottom, mc('grey', '900') 0%, mc('grey', '800') 100%);
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: mc('grey', '50');
+
+  img {
+    width: 250px;
+    filter: grayscale(50%) brightness(120%);
+    animation: errorlogo 5s linear infinite;
+    margin-bottom: 3rem;
+
+    @include until($tablet) {
+      width: 200px;
+    }
+  }
+
+  @keyframes errorlogo {
+    0% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg);
+    }
+    10% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(100%);
+    }
+    15% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(0%);
+    }
+    30% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg);
+    }
+    32% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(2700deg) invert(100%);
+    }
+    34% {
+      filter: blur(0) grayscale(100%) brightness(50%) hue-rotate(110deg);
+    }
+    50% {
+      filter: blur(0) grayscale(100%) brightness(200%) hue-rotate(110deg) sepia(0%);
+    }
+    55% {
+      filter: blur(0) grayscale(100%) brightness(100%) hue-rotate(110deg) sepia(100%);
+    }
+    60% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) sepia(0%);
+    }
+    90% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg);
+    }
+    95% {
+      filter: blur(5px) grayscale(50%) brightness(200%) hue-rotate(720deg);
+    }
+    100% {
+      filter: blur(0) grayscale(50%) brightness(200%) hue-rotate(110deg) invert(100%);
+    }
+  }
+
+  code {
+    color: mc('grey', '500');
+    font-size: .8rem;
+  }
+}

+ 1 - 25
config.sample.yml

@@ -5,7 +5,7 @@
 # https://docs.requarks.io/wiki/install
 
 # ---------------------------------------------------------------------
-# Port the main server should listen to
+# Port the server should listen to
 # ---------------------------------------------------------------------
 
 port: 80
@@ -55,27 +55,3 @@ redis:
 # Possible values: error, warn, info (default), verbose, debug, silly
 
 logLevel: info
-
-# ---------------------------------------------------------------------
-# Configuration Mode
-# ---------------------------------------------------------------------
-# Possible values: auto (default), file, setup
-
-configMode: auto
-
-# ---------------------------------------------------------------------
-# Background Workers
-# ---------------------------------------------------------------------
-# Leave 0 for auto based on CPU cores
-
-workers: 0
-
-# ---------------------------------------------------------------------
-# High Availability
-# ---------------------------------------------------------------------
-# Read the docs BEFORE changing these settings!
-
-ha:
-  node: primary
-  uid: master
-  readonly: false

+ 1 - 8
server/app/data.yml

@@ -23,12 +23,6 @@ defaults:
       port: 6379
       db: 0
       password: null
-    configMode: auto
-    workers: 0
-    ha:
-      node: primary
-      uid: master
-      readonly: false
     # DB defaults
     auth:
       public: false
@@ -36,8 +30,6 @@ defaults:
         local:
           isEnabled: true
           allowSelfRegister: false
-    git:
-      enabled: false
     logging:
       telemetry: false
       loggers:
@@ -48,6 +40,7 @@ defaults:
       rtl: false
       title: Wiki.js
     # System defaults
+    setup: false
     cors:
       credentials: true
       maxAge: 600

+ 36 - 32
server/core/auth.js

@@ -1,10 +1,9 @@
-/* global WIKI */
-
-const _ = require('lodash')
 const passport = require('passport')
 const fs = require('fs-extra')
+const _ = require('lodash')
 const path = require('path')
-const autoload = require('auto-load')
+
+/* global WIKI */
 
 module.exports = {
   strategies: {},
@@ -30,34 +29,39 @@ module.exports = {
       })
     })
 
-    // Load authentication strategies
-
-    const modules = _.values(autoload(path.join(WIKI.SERVERPATH, 'modules/authentication')))
-    _.forEach(modules, (strategy) => {
-      const strategyConfig = _.get(WIKI.config.auth.strategies, strategy.key, { isEnabled: false })
-      strategyConfig.callbackURL = `${WIKI.config.site.host}${WIKI.config.site.path}login/${strategy.key}/callback`
-      strategy.config = strategyConfig
-      if (strategyConfig.isEnabled) {
-        try {
-          strategy.init(passport, strategyConfig)
-        } catch (err) {
-          WIKI.logger.error(`Authentication Provider ${strategy.title}: [ FAILED ]`)
-          WIKI.logger.error(err)
-        }
-      }
-      fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${strategy.key}.svg`), 'utf8').then(iconData => {
-        strategy.icon = iconData
-      }).catch(err => {
-        if (err.code === 'ENOENT') {
-          strategy.icon = '[missing icon]'
-        } else {
-          WIKI.logger.warn(err)
-        }
-      })
-      this.strategies[strategy.key] = strategy
-      WIKI.logger.info(`Authentication Provider ${strategy.title}: [ OK ]`)
-    })
-
     return this
+  },
+  async activateStrategies() {
+    try {
+      // Unload any active strategies
+      WIKI.auth.strategies = []
+      const currentStrategies = _.keys(passport._strategies)
+      _.pull(currentStrategies, 'session')
+      _.forEach(currentStrategies, stg => { passport.unuse(stg) })
+
+      // Load enable strategies
+      const enabledStrategies = await WIKI.db.authentication.getEnabledStrategies()
+      for (let idx in enabledStrategies) {
+        const stg = enabledStrategies[idx]
+        const strategy = require(`../modules/authentication/${stg.key}`)
+        stg.config.callbackURL = `${WIKI.config.site.host}/login/${stg.key}/callback`
+        strategy.init(passport, stg.config)
+
+        fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${strategy.key}.svg`), 'utf8').then(iconData => {
+          strategy.icon = iconData
+        }).catch(err => {
+          if (err.code === 'ENOENT') {
+            strategy.icon = '[missing icon]'
+          } else {
+            WIKI.logger.warn(err)
+          }
+        })
+        WIKI.auth.strategies[stg.key] = strategy
+        WIKI.logger.info(`Authentication Strategy ${stg.title}: [ OK ]`)
+      }
+    } catch (err) {
+      WIKI.logger.error(`Authentication Strategy: [ FAILED ]`)
+      WIKI.logger.error(err)
+    }
   }
 }

+ 14 - 27
server/core/config.js

@@ -50,45 +50,32 @@ module.exports = {
 
   /**
    * Load config from DB
-   *
-   * @param {Array} subsets Array of subsets to load
-   * @returns Promise
    */
-  async loadFromDb(subsets) {
-    if (!_.isArray(subsets) || subsets.length === 0) {
-      subsets = WIKI.data.configNamespaces
-    }
-
-    let results = await WIKI.db.settings.query().select(['key', 'value']).whereIn('key', subsets)
-    if (_.isArray(results) && results.length === subsets.length) {
-      results.forEach(result => {
-        WIKI.config[result.key] = result.value
-      })
-      return true
+  async loadFromDb() {
+    let conf = await WIKI.db.settings.getConfig()
+    if (conf) {
+      WIKI.config = _.defaultsDeep(conf, WIKI.config)
     } else {
-      WIKI.logger.warn('DB Configuration is empty or incomplete.')
-      return false
+      WIKI.logger.warn('DB Configuration is empty or incomplete. Switching to Setup mode...')
+      WIKI.config.setup = true
     }
   },
   /**
    * Save config to DB
    *
-   * @param {Array} subsets Array of subsets to save
+   * @param {Array} keys Array of keys to save
    * @returns Promise
    */
-  async saveToDb(subsets) {
-    if (!_.isArray(subsets) || subsets.length === 0) {
-      subsets = WIKI.data.configNamespaces
-    }
-
+  async saveToDb(keys) {
     let trx = await WIKI.db.Objection.transaction.start(WIKI.db.knex)
 
     try {
-      for (let set of subsets) {
-        console.info(set)
-        await WIKI.db.settings.query(trx).patch({
-          value: _.get(WIKI.config, set, {})
-        }).where('key', set)
+      for (let key of keys) {
+        const value = _.get(WIKI.config, key, null)
+        let affectedRows = await WIKI.db.settings.query(trx).patch({ value }).where('key', key)
+        if (affectedRows === 0 && value) {
+          await WIKI.db.settings.query(trx).insert({ key, value })
+        }
       }
       await trx.commit()
     } catch (err) {

+ 16 - 10
server/core/db.js

@@ -53,6 +53,20 @@ module.exports = {
       client: dbClient,
       useNullAsDefault: true,
       connection: dbConfig,
+      pool: {
+        async afterCreate(conn, done) {
+          // -> Set Connection App Name
+          switch (WIKI.config.db.type) {
+            case 'postgres':
+              await conn.query(`set application_name = 'Wiki.js'`)
+              done()
+              break
+            default:
+              done()
+              break
+          }
+        }
+      },
       debug: WIKI.IS_DEBUG
     })
 
@@ -71,21 +85,13 @@ module.exports = {
           directory: path.join(WIKI.SERVERPATH, 'db/migrations'),
           tableName: 'migrations'
         })
-      },
-      // -> Set Connection App Name
-      async setAppName() {
-        switch (WIKI.config.db.type) {
-          case 'postgres':
-            return self.knex.raw(`set application_name = 'Wiki.js'`)
-        }
       }
     }
 
     let initTasksQueue = (WIKI.IS_MASTER) ? [
-      initTasks.syncSchemas,
-      initTasks.setAppName
+      initTasks.syncSchemas
     ] : [
-      initTasks.setAppName
+      () => { return Promise.resolve() }
     ]
 
     // Perform init tasks

+ 29 - 65
server/core/kernel.js

@@ -1,89 +1,53 @@
-const _ = require('lodash')
-const cluster = require('cluster')
-const Promise = require('bluebird')
-
 /* global WIKI */
 
 module.exports = {
-  numWorkers: 1,
-  workers: [],
-  init() {
-    if (cluster.isMaster) {
-      WIKI.logger.info('=======================================')
-      WIKI.logger.info('= Wiki.js =============================')
-      WIKI.logger.info('=======================================')
+  async init() {
+    WIKI.logger.info('=======================================')
+    WIKI.logger.info('= Wiki.js =============================')
+    WIKI.logger.info('=======================================')
 
-      WIKI.redis = require('./redis').init()
-      WIKI.queue = require('./queue').init()
+    WIKI.db = require('./db').init()
+    WIKI.redis = require('./redis').init()
+    WIKI.queue = require('./queue').init()
 
-      this.setWorkerLimit()
-      this.bootMaster()
-    } else {
-      this.bootWorker()
-    }
+    await this.preBootMaster()
+    this.bootMaster()
   },
   /**
    * Pre-Master Boot Sequence
    */
-  preBootMaster() {
-    return Promise.mapSeries([
-      () => { return WIKI.db.onReady },
-      () => { return WIKI.configSvc.loadFromDb() },
-      () => { return WIKI.queue.clean() }
-    ], fn => { return fn() })
+  async preBootMaster() {
+    try {
+      await WIKI.db.onReady
+      await WIKI.configSvc.loadFromDb()
+      await WIKI.queue.clean()
+    } catch (err) {
+      WIKI.logger.error(err)
+      process.exit(1)
+    }
   },
   /**
    * Boot Master Process
    */
-  bootMaster() {
-    this.preBootMaster().then(sequenceResults => {
-      if (_.every(sequenceResults, rs => rs === true) && WIKI.config.configMode !== 'setup') {
-        this.postBootMaster()
-      } else {
-        WIKI.logger.info('Starting configuration manager...')
+  async bootMaster() {
+    try {
+      if (WIKI.config.setup) {
+        WIKI.logger.info('Starting setup wizard...')
         require('../setup')()
+      } else {
+        await require('../master')()
+        this.postBootMaster()
       }
-      return true
-    }).catch(err => {
+    } catch (err) {
       WIKI.logger.error(err)
       process.exit(1)
-    })
+    }
   },
   /**
    * Post-Master Boot Sequence
    */
   async postBootMaster() {
-    await require('../master')()
-
-    WIKI.queue.start()
-
-    cluster.on('exit', (worker, code, signal) => {
-      if (!global.DEV) {
-        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
-    }
+    await WIKI.auth.activateStrategies()
+    await WIKI.queue.start()
   }
 }

+ 3 - 2
server/core/localization.js

@@ -17,7 +17,7 @@ module.exports = {
       ns: this.namespaces,
       defaultNS: 'common',
       saveMissing: false,
-      lng: WIKI.config.site.lang,
+      lng: WIKI.config.lang,
       fallbackLng: 'en'
     })
 
@@ -31,7 +31,7 @@ module.exports = {
     }
 
     // Load current language
-    this.loadLocale(WIKI.config.site.lang, { silent: true })
+    this.loadLocale(WIKI.config.lang, { silent: true })
 
     return this
   },
@@ -55,6 +55,7 @@ module.exports = {
     const res = await WIKI.db.locales.query().findOne('code', locale)
     if (res) {
       if (_.isPlainObject(res.strings)) {
+        console.info(res.strings)
         _.forOwn(res.strings, (data, ns) => {
           this.namespaces.push(ns)
           this.engine.addResourceBundle(locale, ns, data, true, true)

+ 36 - 5
server/db/migrations/2.0.0.js

@@ -23,6 +23,15 @@ exports.up = knex => {
       table.string('slug').notNullable()
       table.integer('parentId').unsigned().references('id').inTable('assetFolders')
     })
+    // AUTHENTICATION ----------------------
+    .createTable('authentication', table => {
+      table.increments('id').primary()
+      table.string('key').notNullable().unique()
+      table.string('title').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.boolean('useForm').notNullable().defaultTo(false)
+      table.jsonb('config').notNullable()
+    })
     // COMMENTS ----------------------------
     .createTable('comments', table => {
       table.increments('id').primary()
@@ -30,6 +39,14 @@ exports.up = knex => {
       table.string('createdAt').notNullable()
       table.string('updatedAt').notNullable()
     })
+    // EDITORS -----------------------------
+    .createTable('editors', table => {
+      table.increments('id').primary()
+      table.string('key').notNullable().unique()
+      table.string('title').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.jsonb('config')
+    })
     // GROUPS ------------------------------
     .createTable('groups', table => {
       table.increments('id').primary()
@@ -54,6 +71,7 @@ exports.up = knex => {
       table.string('path').notNullable()
       table.string('title').notNullable()
       table.string('description')
+      table.boolean('isPrivate').notNullable().defaultTo(false)
       table.boolean('isPublished').notNullable().defaultTo(false)
       table.string('publishStartDate')
       table.string('publishEndDate')
@@ -66,9 +84,16 @@ exports.up = knex => {
       table.increments('id').primary()
       table.string('key').notNullable().unique()
       table.jsonb('value')
-      table.string('createdAt').notNullable()
       table.string('updatedAt').notNullable()
     })
+    // STORAGE -----------------------------
+    .createTable('storage', table => {
+      table.increments('id').primary()
+      table.string('key').notNullable().unique()
+      table.string('title').notNullable()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.jsonb('config')
+    })
     // TAGS --------------------------------
     .createTable('tags', table => {
       table.increments('id').primary()
@@ -82,16 +107,17 @@ exports.up = knex => {
       table.increments('id').primary()
       table.string('email').notNullable()
       table.string('name').notNullable()
-      table.string('provider').notNullable().defaultTo('local')
       table.string('providerId')
       table.string('password')
       table.boolean('tfaIsActive').notNullable().defaultTo(false)
       table.string('tfaSecret')
       table.enum('role', ['admin', 'guest', 'user']).notNullable().defaultTo('guest')
+      table.string('jobTitle').defaultTo('')
+      table.string('location').defaultTo('')
+      table.string('pictureUrl')
+      table.string('timezone').notNullable().defaultTo('America/New_York')
       table.string('createdAt').notNullable()
       table.string('updatedAt').notNullable()
-
-      table.unique(['provider', 'email'])
     })
     // =====================================
     // RELATION TABLES
@@ -120,11 +146,16 @@ exports.up = knex => {
       table.integer('authorId').unsigned().references('id').inTable('users')
     })
     .table('pages', table => {
+      table.string('editor').references('key').inTable('editors')
       table.string('locale', 2).references('code').inTable('locales')
       table.integer('authorId').unsigned().references('id').inTable('users')
     })
     .table('users', table => {
-      table.string('locale', 2).references('code').inTable('locales')
+      table.string('provider').references('key').inTable('authentication').notNullable().defaultTo('local')
+      table.string('locale', 2).references('code').inTable('locales').notNullable().defaultTo('en')
+      table.string('defaultEditor').references('key').inTable('editors').notNullable().defaultTo('markdown')
+
+      table.unique(['provider', 'email'])
     })
 }
 

+ 64 - 0
server/db/models/authentication.js

@@ -0,0 +1,64 @@
+const Model = require('objection').Model
+const autoload = require('auto-load')
+const path = require('path')
+const _ = require('lodash')
+
+/* global WIKI */
+
+/**
+ * Authentication model
+ */
+module.exports = class Authentication extends Model {
+  static get tableName() { return 'authentication' }
+
+  static get jsonSchema () {
+    return {
+      type: 'object',
+      required: ['key', 'title', 'isEnabled', 'useForm'],
+
+      properties: {
+        id: {type: 'integer'},
+        key: {type: 'string'},
+        title: {type: 'string'},
+        isEnabled: {type: 'boolean'},
+        useForm: {type: 'boolean'},
+        config: {type: 'object'}
+      }
+    }
+  }
+
+  static async getEnabledStrategies() {
+    return WIKI.db.authentication.query().where({ isEnabled: true })
+  }
+
+  static async refreshStrategiesFromDisk() {
+    try {
+      const dbStrategies = await WIKI.db.authentication.query()
+      const diskStrategies = autoload(path.join(WIKI.SERVERPATH, 'modules/authentication'))
+      let newStrategies = []
+      _.forOwn(diskStrategies, (strategy, strategyKey) => {
+        if (!_.some(dbStrategies, ['key', strategy.key])) {
+          newStrategies.push({
+            key: strategy.key,
+            title: strategy.title,
+            isEnabled: false,
+            useForm: strategy.useForm,
+            config: _.reduce(strategy.props, (result, value, key) => {
+              _.set(result, value, '')
+              return result
+            }, {})
+          })
+        }
+      })
+      if (newStrategies.length > 0) {
+        await WIKI.db.authentication.query().insert(newStrategies)
+        WIKI.logger.info(`Loaded ${newStrategies.length} new authentication strategies: [ OK ]`)
+      } else {
+        WIKI.logger.info(`No new authentication strategies found: [ SKIPPED ]`)
+      }
+    } catch (err) {
+      WIKI.logger.error(`Failed to scan or load new authentication providers: [ FAILED ]`)
+      WIKI.logger.error(err)
+    }
+  }
+}

+ 62 - 0
server/db/models/editors.js

@@ -0,0 +1,62 @@
+const Model = require('objection').Model
+const autoload = require('auto-load')
+const path = require('path')
+const _ = require('lodash')
+
+/* global WIKI */
+
+/**
+ * Editor model
+ */
+module.exports = class Editor extends Model {
+  static get tableName() { return 'editors' }
+
+  static get jsonSchema () {
+    return {
+      type: 'object',
+      required: ['key', 'title', 'isEnabled'],
+
+      properties: {
+        id: {type: 'integer'},
+        key: {type: 'string'},
+        title: {type: 'string'},
+        isEnabled: {type: 'boolean'},
+        config: {type: 'object'}
+      }
+    }
+  }
+
+  static async getEnabledEditors() {
+    return WIKI.db.editors.query().where({ isEnabled: true })
+  }
+
+  static async refreshEditorsFromDisk() {
+    try {
+      const dbEditors = await WIKI.db.editors.query()
+      const diskEditors = autoload(path.join(WIKI.SERVERPATH, 'modules/editor'))
+      let newEditors = []
+      _.forOwn(diskEditors, (strategy, strategyKey) => {
+        if (!_.some(dbEditors, ['key', strategy.key])) {
+          newEditors.push({
+            key: strategy.key,
+            title: strategy.title,
+            isEnabled: false,
+            config: _.reduce(strategy.props, (result, value, key) => {
+              _.set(result, value, '')
+              return result
+            }, {})
+          })
+        }
+      })
+      if (newEditors.length > 0) {
+        await WIKI.db.editors.query().insert(newEditors)
+        WIKI.logger.info(`Loaded ${newEditors.length} new editors: [ OK ]`)
+      } else {
+        WIKI.logger.info(`No new editors found: [ SKIPPED ]`)
+      }
+    } catch (err) {
+      WIKI.logger.error(`Failed to scan or load new editors: [ FAILED ]`)
+      WIKI.logger.error(err)
+    }
+  }
+}

+ 16 - 2
server/db/models/settings.js

@@ -1,9 +1,12 @@
 const Model = require('objection').Model
+const _ = require('lodash')
+
+/* global WIKI */
 
 /**
  * Settings model
  */
-module.exports = class User extends Model {
+module.exports = class Setting extends Model {
   static get tableName() { return 'settings' }
 
   static get jsonSchema () {
@@ -25,7 +28,18 @@ module.exports = class User extends Model {
     this.updatedAt = new Date().toISOString()
   }
   $beforeInsert() {
-    this.createdAt = new Date().toISOString()
     this.updatedAt = new Date().toISOString()
   }
+
+  static async getConfig() {
+    const settings = await WIKI.db.settings.query()
+    if (settings.length > 0) {
+      return _.reduce(settings, (res, val, key) => {
+        _.set(res, val.key, (val.value.v) ? val.value.v : val.value)
+        return res
+      }, {})
+    } else {
+      return false
+    }
+  }
 }

+ 3 - 0
server/db/models/users.js

@@ -30,6 +30,9 @@ module.exports = class User extends Model {
         tfaIsActive: {type: 'boolean', default: false},
         tfaSecret: {type: 'string'},
         locale: {type: 'string'},
+        jobTitle: {type: 'string'},
+        location: {type: 'string'},
+        pictureUrl: {type: 'string'},
         createdAt: {type: 'string'},
         updatedAt: {type: 'string'}
       }

+ 3 - 8
server/index.js

@@ -4,11 +4,10 @@
 // ===========================================
 
 const path = require('path')
-const cluster = require('cluster')
 
 let WIKI = {
   IS_DEBUG: process.env.NODE_ENV === 'development',
-  IS_MASTER: cluster.isMaster,
+  IS_MASTER: true,
   ROOTPATH: process.cwd(),
   SERVERPATH: path.join(process.cwd(), 'server'),
   Error: require('./helpers/error'),
@@ -32,18 +31,14 @@ WIKI.logger = require('./core/logger').init('MASTER')
 WIKI.telemetry = require('./core/telemetry').init()
 
 process.on('unhandledRejection', (err) => {
+  WIKI.logger.warn(err)
   WIKI.telemetry.sendError(err)
 })
 process.on('uncaughtException', (err) => {
+  WIKI.logger.warn(err)
   WIKI.telemetry.sendError(err)
 })
 
-// ----------------------------------------
-// Init DB
-// ----------------------------------------
-
-WIKI.db = require('./core/db').init()
-
 // ----------------------------------------
 // Start Kernel
 // ----------------------------------------

+ 5 - 4
server/jobs/fetch-graph-locale.js

@@ -7,14 +7,15 @@ const { createApolloFetch } = require('apollo-fetch')
 WIKI.redis = require('../core/redis').init()
 WIKI.db = require('../core/db').init()
 
-const apollo = createApolloFetch({
-  uri: 'https://graph.requarks.io'
-})
-
 module.exports = async (job) => {
   WIKI.logger.info(`Fetching locale ${job.data.locale} from Graph endpoint...`)
 
   try {
+    await WIKI.configSvc.loadFromDb()
+    const apollo = createApolloFetch({
+      uri: WIKI.config.graphEndpoint
+    })
+
     const respStrings = await apollo({
       query: `query ($code: String!) {
         localization {

+ 4 - 5
server/jobs/sync-graph-locales.js

@@ -7,15 +7,14 @@ const { createApolloFetch } = require('apollo-fetch')
 WIKI.redis = require('../core/redis').init()
 WIKI.db = require('../core/db').init()
 
-const apollo = createApolloFetch({
-  uri: 'https://graph.requarks.io'
-})
-
 module.exports = async (job) => {
   WIKI.logger.info('Syncing locales with Graph endpoint...')
 
   try {
-    await WIKI.configSvc.loadFromDb(['site'])
+    await WIKI.configSvc.loadFromDb()
+    const apollo = createApolloFetch({
+      uri: WIKI.config.graphEndpoint
+    })
 
     // -> Fetch locales list
 

+ 1 - 1
server/master.js

@@ -75,7 +75,7 @@ module.exports = async () => {
   app.use(session({
     name: 'wikijs.sid',
     store: sessionStore,
-    secret: WIKI.config.site.sessionSecret,
+    secret: WIKI.config.sessionSecret,
     resave: false,
     saveUninitialized: false
   }))

+ 10 - 0
server/modules/editor/markdown.js

@@ -0,0 +1,10 @@
+// ------------------------------------
+// Markdown Editor (default)
+// ------------------------------------
+
+module.exports = {
+  key: 'markdown',
+  title: 'Markdown (default)',
+  props: [],
+  init (conf) {}
+}

+ 39 - 35
server/setup.js

@@ -250,8 +250,6 @@ module.exports = () => {
   app.post('/finalize', async (req, res) => {
     WIKI.telemetry.sendEvent('setup', 'finalize')
 
-    console.error('DUDE')
-
     try {
       // Upgrade from WIKI.js 1.x?
       if (req.body.upgrade) {
@@ -272,41 +270,31 @@ module.exports = () => {
       confRaw = yaml.safeDump(conf)
       await fs.writeFileAsync(path.join(WIKI.ROOTPATH, 'config.yml'), confRaw)
 
-      _.set(WIKI.config, 'port', req.body.port)
+      // Set config
+      _.set(WIKI.config, 'defaultEditor', true)
+      _.set(WIKI.config, 'graphEndpoint', 'https://graph.requarks.io')
+      _.set(WIKI.config, 'lang', 'en')
+      _.set(WIKI.config, 'langAutoUpdate', true)
+      _.set(WIKI.config, 'langRTL', false)
       _.set(WIKI.config, 'paths.content', req.body.pathContent)
-
-      // Populate config namespaces
-      WIKI.config.auth = WIKI.config.auth || {}
-      WIKI.config.features = WIKI.config.features || {}
-      WIKI.config.logging = WIKI.config.logging || {}
-      WIKI.config.site = WIKI.config.site || {}
-      WIKI.config.theme = WIKI.config.theme || {}
-      WIKI.config.uploads = WIKI.config.uploads || {}
-
-      // Site namespace
-      _.set(WIKI.config.site, 'title', req.body.title)
-      _.set(WIKI.config.site, 'lang', 'en')
-      _.set(WIKI.config.site, 'langAutoUpdate', true)
-      _.set(WIKI.config.site, 'rtl', false)
-      _.set(WIKI.config.site, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
-
-      // Auth namespace
-      _.set(WIKI.config.auth, 'public', req.body.public === 'true')
-      _.set(WIKI.config.auth, 'strategies.local.isEnabled', true)
-      _.set(WIKI.config.auth, 'strategies.local.allowSelfRegister', req.body.selfRegister === 'true')
-
-      // Logging namespace
-      WIKI.config.logging.telemetry = (req.body.telemetry === 'true')
+      _.set(WIKI.config, 'port', req.body.port)
+      _.set(WIKI.config, 'public', req.body.public === 'true')
+      _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
+      _.set(WIKI.config, 'telemetry', req.body.telemetry === 'true')
+      _.set(WIKI.config, 'title', req.body.title)
 
       // Save config to DB
       WIKI.logger.info('Persisting config to DB...')
       await WIKI.db.settings.query().insert([
-        { key: 'auth', value: WIKI.config.auth },
-        { key: 'features', value: WIKI.config.features },
-        { key: 'logging', value: WIKI.config.logging },
-        { key: 'site', value: WIKI.config.site },
-        { key: 'theme', value: WIKI.config.theme },
-        { key: 'uploads', value: WIKI.config.uploads }
+        { key: 'defaultEditor', value: { v: WIKI.config.defaultEditor } },
+        { key: 'graphEndpoint', value: { v: WIKI.config.graphEndpoint } },
+        { key: 'lang', value: { v: WIKI.config.lang } },
+        { key: 'langAutoUpdate', value: { v: WIKI.config.langAutoUpdate } },
+        { key: 'langRTL', value: { v: WIKI.config.langRTL } },
+        { key: 'public', value: { v: WIKI.config.public } },
+        { key: 'sessionSecret', value: { v: WIKI.config.sessionSecret } },
+        { key: 'telemetry', value: { v: WIKI.config.telemetry } },
+        { key: 'title', value: { v: WIKI.config.title } }
       ])
 
       // Create default locale
@@ -319,8 +307,20 @@ module.exports = () => {
         nativeName: 'English'
       })
 
+      // Load authentication strategies + enable local
+      await WIKI.db.authentication.refreshStrategiesFromDisk()
+      await WIKI.db.authentication.query().patch({ isEnabled: true }).where('key', 'local')
+
+      // Load editors + enable default
+      await WIKI.db.editors.refreshEditorsFromDisk()
+      await WIKI.db.editors.query().patch({ isEnabled: true }).where('key', 'markdown')
+
       // Create root administrator
       WIKI.logger.info('Creating root administrator...')
+      await WIKI.db.users.query().delete().where({
+        provider: 'local',
+        email: req.body.adminEmail
+      })
       await WIKI.db.users.query().insert({
         email: req.body.adminEmail,
         provider: 'local',
@@ -328,11 +328,12 @@ module.exports = () => {
         name: 'Administrator',
         role: 'admin',
         locale: 'en',
+        defaultEditor: 'markdown',
         tfaIsActive: false
       })
 
       // Create Guest account
-      WIKI.logger.info('Creating root administrator...')
+      WIKI.logger.info('Creating guest account...')
       const guestUsr = await WIKI.db.users.query().findOne({
         provider: 'local',
         email: 'guest@example.com'
@@ -345,6 +346,7 @@ module.exports = () => {
           password: '',
           role: 'guest',
           locale: 'en',
+          defaultEditor: 'markdown',
           tfaIsActive: false
         })
       }
@@ -356,6 +358,8 @@ module.exports = () => {
         redirectPort: WIKI.config.port
       }).end()
 
+      WIKI.config.setup = false
+
       WIKI.logger.info('Stopping Setup...')
       WIKI.server.destroy(() => {
         WIKI.logger.info('Setup stopped. Starting Wiki.js...')
@@ -392,7 +396,7 @@ module.exports = () => {
   // Start HTTP server
   // ----------------------------------------
 
-  WIKI.logger.info(`HTTP Server on port: ${WIKI.config.port}`)
+  WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
 
   app.set('port', WIKI.config.port)
   WIKI.server = http.createServer(app)
@@ -433,6 +437,6 @@ module.exports = () => {
   })
 
   WIKI.server.on('listening', () => {
-    WIKI.logger.info('HTTP Server: RUNNING')
+    WIKI.logger.info('HTTP Server: [ RUNNING ]')
   })
 }

+ 23 - 10
server/views/error.pug

@@ -1,13 +1,26 @@
 extends ./master.pug
 
 block body
-  body(class='is-error')
-    .container
-      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=config.site.path+ '/')= t('errors:actions.gohome')
-
-      if error.stack
-        h3= t('errors:debugmsg')
-        pre: code #{error.stack}
+  #app.is-fullscreen
+    v-app(dark)
+      .app-error
+        v-container
+          .pt-5
+            v-layout(row)
+              v-flex(xs10)
+                a(href='/'): img(src='/svg/logo-wikijs.svg')
+              v-flex(xs2).text-xs-right
+                v-btn(href='/', depressed, color='red darken-3')
+                  v-icon(left) home
+                  span Home
+            v-alert(color='grey', outline, :value='true', icon='error')
+              strong.red--text.text--lighten-3 Oops, something went wrong...
+              .body-1.red--text.text--lighten-2= message
+            
+            if error.stack
+              v-expansion-panel.mt-5
+                v-expansion-panel-content.red.darken-3(:value='true')
+                  div(slot='header') View Debug Trace
+                  v-card(color='grey darken-4')
+                    v-card-text
+                      pre: code #{error.stack}

+ 13 - 6
wiki.js

@@ -87,19 +87,26 @@ const init = {
         const devWatcher = chokidar.watch([
           './server',
           '!./server/views/master.pug'
-        ])
+        ], {
+          ignoreInitial: true,
+          atomic: 400
+        })
         devWatcher.on('ready', () => {
-          devWatcher.on('all', () => {
+          devWatcher.on('all', async () => {
             console.warn('--- >>>>>>>>>>>>>>>>>>>>>>>>>>>> ---')
             console.warn('--- Changes detected: Restarting ---')
             console.warn('--- <<<<<<<<<<<<<<<<<<<<<<<<<<<< ---')
+            console.warn('--- Closing DB connections...')
+            await global.WIKI.db.knex.destroy()
+            console.warn('--- Closing Redis connections...')
+            await global.WIKI.redis.quit()
+            console.warn('--- Closing Server connections...')
             global.WIKI.server.destroy(() => {
               global.WIKI = {}
-              for (const workerId in cluster.workers) {
-                cluster.workers[workerId].kill()
-              }
               Object.keys(require.cache).forEach(function(id) {
-                if (/[/\\]server[/\\]/.test(id)) delete require.cache[id]
+                if (/[/\\]server[/\\]/.test(id)) {
+                  delete require.cache[id]
+                }
               })
               require('./server')
             })