Преглед на файлове

feat: guest + user permissions

Nicolas Giard преди 6 години
родител
ревизия
75eb277401
променени са 33 файла, в които са добавени 463 реда и са изтрити 191 реда
  1. 5 5
      client/components/admin/admin-api.vue
  2. 5 0
      client/components/admin/admin-auth.vue
  3. 36 5
      client/components/admin/admin-dev.vue
  4. 7 1
      client/components/admin/admin-general.vue
  5. 4 4
      client/components/admin/admin-groups-edit-permissions.vue
  6. 2 0
      client/components/admin/admin-locale.vue
  7. 3 2
      client/components/admin/admin-logging.vue
  8. 1 1
      client/components/admin/admin-search.vue
  9. 1 1
      client/components/admin/admin-storage.vue
  10. 7 3
      client/components/editor.vue
  11. 1 1
      client/components/editor/editor-markdown.vue
  12. 2 2
      client/graph/admin/auth/auth-mutation-save-strategies.gql
  13. 1 0
      client/scss/app.scss
  14. 81 0
      client/scss/pages/_unauthorized.scss
  15. 14 0
      client/static/svg/icon-delete-shield.svg
  16. 33 0
      client/static/svg/icon-safety-float.svg
  17. 3 0
      client/static/svg/motif-diagonals.svg
  18. 2 4
      client/themes/default/components/page.vue
  19. 0 12
      dev/examples/Dockerfile
  20. 50 16
      dev/examples/docker-compose.yml
  21. 0 9
      server/app/data.yml
  22. 41 0
      server/controllers/common.js
  23. 84 4
      server/core/auth.js
  24. 0 2
      server/core/config.js
  25. 25 22
      server/db/migrations/2.0.0-beta.1.js
  26. 0 11
      server/db/seeds/settings.js
  27. 7 0
      server/graph/resolvers/authentication.js
  28. 8 1
      server/graph/schemas/authentication.graphql
  29. 2 3
      server/master.js
  30. 0 72
      server/middlewares/auth.js
  31. 13 2
      server/models/users.js
  32. 12 8
      server/setup.js
  33. 13 0
      server/views/unauthorized.pug

+ 5 - 5
client/components/admin/admin-api.vue

@@ -6,14 +6,14 @@
           img(src='/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
           .admin-header-title
             .headline.blue--text.text--darken-2 API
-            .subheading.grey--text Manage keys to access the API
+            .subheading.grey--text Manage keys to access the API #[v-chip(label, color='primary', small).white--text coming soon]
           v-spacer
-          v-btn(outline, color='grey', large, @click='refresh')
+          v-btn(outline, color='grey', large, @click='refresh', disabled)
             v-icon refresh
-          v-btn(color='green', dark, depressed, large, @click='globalSwitch')
+          v-btn(color='green', disabled, depressed, large, @click='globalSwitch')
             v-icon(left) power_settings_new
             | Enable API
-          v-btn(color='primary', depressed, large, @click='newKey')
+          v-btn(color='primary', depressed, large, @click='newKey', disabled)
             v-icon(left) add
             | New API Key
         v-card.mt-3
@@ -58,7 +58,7 @@
                 td {{ props.item.updatedOn }}
                 td: v-btn(icon): v-icon.grey--text.text--darken-1 more_horiz
             template(slot='no-data')
-              v-alert.mt-3(icon='warning', :value='true', outline) No API have been generated yet.
+              v-alert.mt-3(icon='info', :value='true', outline, color='info') No API have been generated yet.
           .text-xs-center.py-2
             v-pagination(v-model='pagination.page', :length='pages')
 </template>

+ 5 - 0
client/components/admin/admin-auth.vue

@@ -207,6 +207,11 @@ export default {
       await this.$apollo.mutate({
         mutation: strategiesSaveMutation,
         variables: {
+          config: {
+            audience: this.jwtAudience,
+            tokenExpiration: this.jwtExpiration,
+            tokenRenewal: this.jwtRenewablePeriod
+          },
           strategies: this.strategies.map(str => _.pick(str, [
             'isEnabled',
             'key',

+ 36 - 5
client/components/admin/admin-dev.vue

@@ -7,8 +7,18 @@
           .admin-header-title
             .headline.primary--text Developer Tools
             .subheading.grey--text ¯\_(ツ)_/¯
+          v-spacer
+          v-card.radius-7
+            v-card-text
+              .caption Enables extra dev options and removes many safeguards.
+              .caption.red--text Do not enable unless you know what you're doing!
+              v-switch.mt-1(
+                color='primary'
+                hide-details
+                label='Dev Mode'
+              )
 
-        v-card.mt-3
+        v-card.mt-3.white.grey--text.text--darken-3
           v-tabs(
             v-model='selectedTab'
             color='grey darken-2'
@@ -92,9 +102,8 @@ export default {
             }, 500)
             return resp
           },
-          query: '',
           response: null,
-          variables: null,
+          variables: '{}',
           operationName: null,
           websocketConnectionParams: null
         }),
@@ -103,6 +112,7 @@ export default {
       graphiQLInstance.queryEditorComponent.editor.refresh()
       graphiQLInstance.variableEditorComponent.editor.refresh()
       graphiQLInstance.state.variableEditorOpen = true
+      graphiQLInstance.state.docExplorerOpen = true
     },
     renderVoyager() {
       ReactDOM.render(
@@ -120,7 +130,7 @@ export default {
 <style lang='scss'>
 
 #graphiql {
-  height: calc(100vh - 230px);
+  height: calc(100vh - 270px);
 
   .topBar {
     background-color: mc('grey', '200');
@@ -136,10 +146,14 @@ export default {
     background-color: initial;
     box-shadow: initial;
   }
+
+  .doc-explorer-title-bar, .history-title-bar {
+    height: auto;
+  }
 }
 
 #voyager {
-  height: calc(100vh - 250px);
+  height: calc(100vh - 270px);
 
   .title-area {
     display: none;
@@ -147,5 +161,22 @@ export default {
   .type-doc {
     margin-top: 5px;
   }
+
+  .doc-navigation {
+    > span {
+      overflow-y: hidden;
+      display: block;
+    }
+    min-height: 40px;
+  }
+
+  .contents {
+    padding-bottom: 0;
+    color: #666;
+  }
+
+  .type-info-popover {
+    display: none;
+  }
 }
 </style>

+ 7 - 1
client/components/admin/admin-general.vue

@@ -38,6 +38,8 @@
                       :counter='50'
                       v-model='config.title'
                       prepend-icon='public'
+                      hint='Displayed in the top bar and appended to all pages meta title.'
+                      persistent-hint
                       )
                   v-divider
                   v-subheader SEO
@@ -48,6 +50,8 @@
                       :counter='255'
                       v-model='config.description'
                       prepend-icon='explore'
+                      hint='Default description when none is provided for a page.'
+                      persistent-hint
                       )
                     v-select.mt-2(
                       outline
@@ -57,7 +61,7 @@
                       v-model='config.robots'
                       prepend-icon='explore'
                       :return-object='false'
-                      hint='Default: Index, Follow'
+                      hint='Default: Index, Follow. Can also be set on a per-page basis.'
                       persistent-hint
                       )
                   v-divider
@@ -69,6 +73,8 @@
                       :items='analyticsServices'
                       v-model='config.analyticsService'
                       prepend-icon='timeline'
+                      persistent-hint
+                      hint='Automatically add tracking code for services like Google Analytics.'
                       )
                     v-text-field.mt-2(
                       v-if='config.analyticsService !== ``'

+ 4 - 4
client/components/admin/admin-groups-edit-permissions.vue

@@ -60,14 +60,14 @@ export default {
             },
             {
               permission: 'write:pages',
-              hint: 'Can view and create new pages, as specified in the Page Rules',
+              hint: 'Can create new pages, as specified in the Page Rules',
               warning: false,
               restrictedForSystem: false,
               disabled: false
             },
             {
               permission: 'manage:pages',
-              hint: 'Can view, create, edit and move existing pages as specified in the Page Rules',
+              hint: 'Can edit and move existing pages as specified in the Page Rules',
               warning: false,
               restrictedForSystem: false,
               disabled: false
@@ -95,7 +95,7 @@ export default {
             },
             {
               permission: 'manage:assets',
-              hint: 'Can edit and delete assets (such as images and files), as specified in the Page Rules',
+              hint: 'Can edit and delete existing assets (such as images and files), as specified in the Page Rules',
               warning: false,
               restrictedForSystem: false,
               disabled: false
@@ -116,7 +116,7 @@ export default {
             },
             {
               permission: 'manage:comments',
-              hint: 'Can edit and delete comments, as specified in the Page Rules',
+              hint: 'Can edit and delete existing comments, as specified in the Page Rules',
               warning: false,
               restrictedForSystem: false,
               disabled: false

+ 2 - 0
client/components/admin/admin-locale.vue

@@ -52,6 +52,8 @@
                 v-toolbar(color='primary', dark, dense, flat)
                   v-toolbar-title
                     .subheading {{ $t('admin:locale.namespacing') }}
+                  v-spacer
+                  v-chip(label, color='white', small).primary--text coming soon
                 v-card-text
                   v-switch(
                     v-model='namespacing'

+ 3 - 2
client/components/admin/admin-logging.vue

@@ -6,11 +6,11 @@
           img(src='/svg/icon-registry-editor.svg', alt='Logging', style='width: 80px;')
           .admin-header-title
             .headline.primary--text Logging
-            .subheading.grey--text Configure the system logger(s)
+            .subheading.grey--text Configure the system logger(s) #[v-chip(label, color='primary', small).white--text coming soon]
           v-spacer
           v-btn(outline, color='grey', @click='refresh', large)
             v-icon refresh
-          v-btn(color='black', dark, depressed, @click='toggleConsole', large)
+          v-btn(color='black', disabled, depressed, @click='toggleConsole', large)
             ConsoleLineIcon.mr-3
             span Live Trail
           v-btn(color='success', @click='save', depressed, large)
@@ -34,6 +34,7 @@
                     :label='logger.title'
                     color='primary'
                     hide-details
+                    disabled
                   )
 
             v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false')

+ 1 - 1
client/components/admin/admin-search.vue

@@ -6,7 +6,7 @@
           img(src='/svg/icon-search.svg', alt='Search Engine', style='width: 80px;')
           .admin-header-title
             .headline.primary--text Search Engine
-            .subheading.grey--text Configure the search capabilities of your wiki
+            .subheading.grey--text Configure the search capabilities of your wiki #[v-chip(label, color='primary', small).white--text coming soon]
           v-spacer
           v-btn(outline, color='grey', @click='refresh', large)
             v-icon refresh

+ 1 - 1
client/components/admin/admin-storage.vue

@@ -6,7 +6,7 @@
           img(src='/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
           .admin-header-title
             .headline.primary--text Storage
-            .subheading.grey--text Set backup and sync targets for your content
+            .subheading.grey--text Set backup and sync targets for your content #[v-chip(label, color='primary', small).white--text coming soon]
           v-spacer
           v-btn(outline, color='grey', @click='refresh', large)
             v-icon refresh

+ 7 - 3
client/components/editor.vue

@@ -14,12 +14,12 @@
           outline
           color='blue'
           @click.native.stop='openPropsModal'
-          :class='{ "is-icon": $vuetify.breakpoint.mdAndDown, "mx-0": mode === `create`, "ml-0": mode !== `create` }'
+          :class='{ "is-icon": $vuetify.breakpoint.mdAndDown, "mx-0": !welcomeMode, "ml-0": !welcomeMode }'
           )
           v-icon(color='blue', :left='$vuetify.breakpoint.lgAndUp') sort_by_alpha
           span.white--text(v-if='$vuetify.breakpoint.lgAndUp') {{ $t('editor:page') }}
         v-btn(
-          v-if='path !== `home`'
+          v-if='!welcomeMode'
           outline
           color='red'
           :class='{ "is-icon": $vuetify.breakpoint.mdAndDown }'
@@ -62,6 +62,7 @@ import editorStore from '@/store/editor'
 WIKI.$store.registerModule('editor', editorStore)
 
 export default {
+  i18nOptions: { namespaces: 'editor' },
   components: {
     AtomSpinner,
     editorCode: () => import(/* webpackChunkName: "editor-code", webpackMode: "lazy" */ './editor/editor-code.vue'),
@@ -127,7 +128,8 @@ export default {
     darkMode: get('site/dark'),
     mode: get('editor/mode'),
     notification: get('notification'),
-    notificationState: sync('notification@isActive')
+    notificationState: sync('notification@isActive'),
+    welcomeMode() { return this.mode === `create` && this.path === `home` }
   },
   watch: {
     currentEditor(newValue, oldValue) {
@@ -242,6 +244,8 @@ export default {
             throw new Error(_.get(resp, 'responseResult.message'))
           }
         }
+
+        this.initContentParsed = this.$store.get('editor/content')
       } catch (err) {
         this.$store.commit('showNotification', {
           message: err.message,

+ 1 - 1
client/components/editor/editor-markdown.vue

@@ -234,7 +234,7 @@ export default {
 
       if (!token.type) { return }
 
-      console.info(token)
+      // console.info(token)
     },
     /**
      * Update scroll sync

+ 2 - 2
client/graph/admin/auth/auth-mutation-save-strategies.gql

@@ -1,6 +1,6 @@
-mutation($strategies: [AuthenticationStrategyInput]) {
+mutation($strategies: [AuthenticationStrategyInput]!, $config: AuthenticationConfigInput) {
   authentication {
-    updateStrategies(strategies: $strategies) {
+    updateStrategies(strategies: $strategies, config: $config) {
       responseResult {
         succeeded
         errorCode

+ 1 - 0
client/scss/app.scss

@@ -23,6 +23,7 @@
 // @import 'node_modules/diff2html/dist/diff2html.min';
 
 @import 'pages/new';
+@import 'pages/unauthorized';
 @import 'pages/welcome';
 @import 'pages/error';
 

+ 81 - 0
client/scss/pages/_unauthorized.scss

@@ -0,0 +1,81 @@
+.unauthorized {
+  background: linear-gradient(to bottom, darken(mc('blue', '900'), 10%) 0%, mc('red', '500') 100%);
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: mc('grey', '50');
+
+  &::before {
+    content: '';
+    display:block;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    background-image: url('../static/svg/motif-diagonals.svg');
+    background-position: center center;
+    background-repeat: repeat;
+    background-size: 50px;
+    z-index: 0;
+    opacity: .75;
+    animation: onboardingBgReveal 50s linear infinite;
+
+    @include keyframes(onboardingBgReveal) {
+      0% {
+        background-position-y: 0;
+      }
+      100% {
+        background-position-y: -2000px;
+      }
+    }
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    background-color: transparent;
+    background-image: url('../static/svg/motif-overlay.svg');
+    background-attachment: fixed;
+    background-size: cover;
+    opacity: .5;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+  }
+
+  &-content {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    z-index: 2;
+  }
+
+  img {
+    height: 250px;
+    margin-bottom: 3rem;
+    z-index: 2;
+    animation-duration: 2s;
+
+    @include until($tablet) {
+      height: 200px;
+    }
+  }
+
+  h1 {
+    font-size: 1.5rem;
+    margin-bottom: 1rem;
+    z-index: 2;
+  }
+  h2 {
+    margin-bottom: 3rem;
+    z-index: 2;
+  }
+  .v-btn {
+    z-index: 2;
+  }
+}

+ 14 - 0
client/static/svg/icon-delete-shield.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+    <g>
+        <g>
+            <path d="M64,116c-0.5,0 -1,-0.1 -1.5,-0.4c-26,-15.4 -41.5,-42.6 -41.5,-72.7l0,-15.1c0,-1.3 0.8,-2.4 2,-2.8l40,-14.8c0.7,-0.2 1.4,-0.2 2.1,0l39.9,14.8c1.2,0.4 2,1.6 2,2.8l0,15.1c0,7.8 -1.1,15.6 -3.2,23.1c-0.4,1.6 -2.1,2.5 -3.7,2.1c-1.6,-0.4 -2.5,-2.1 -2.1,-3.7c2,-6.9 2.9,-14.1 2.9,-21.4l0,-13l-36.9,-13.8l-37,13.7l0,13c0,28 14.4,53.2 38.5,67.5c1.4,0.8 1.9,2.7 1.1,4.1c-0.6,1 -1.6,1.5 -2.6,1.5Z" style="fill:#fff;fill-rule:nonzero;"/>
+            <path d="M64,116c-1,0 -2,-0.5 -2.6,-1.5c-0.8,-1.4 -0.4,-3.3 1.1,-4.1c3.2,-1.9 6.3,-4 9.2,-6.3c1.3,-1 3.2,-0.8 4.2,0.5c1,1.3 0.8,3.2 -0.5,4.2c-3.1,2.5 -6.4,4.8 -9.9,6.8c-0.5,0.3 -1,0.4 -1.5,0.4Z" style="fill:#fff;fill-rule:nonzero;"/>
+            <path d="M64,101.6c-0.6,0 -1.3,-0.2 -1.8,-0.6c-15.9,-11.7 -26,-29 -28.6,-48.5c-0.2,-1.6 0.9,-3.1 2.6,-3.4c1.6,-0.2 3.1,0.9 3.4,2.6c2.3,17.1 10.9,32.3 24.4,43c15.9,-12.6 25,-31.4 25,-51.9l0,-4.6l-25,-9.2l-27,10c-1.6,0.6 -3.3,-0.2 -3.9,-1.8c-0.6,-1.6 0.2,-3.3 1.8,-3.9l28.1,-10.3c0.7,-0.2 1.4,-0.2 2.1,0l28,10.4c1.2,0.4 2,1.6 2,2.8l0,6.7c0,23.2 -10.6,44.4 -29.2,58.1c-0.6,0.4 -1.3,0.6 -1.9,0.6Z" style="fill:#d0d4d8;fill-rule:nonzero;"/>
+        </g>
+        <path d="M64,25.8l0,72.8c17.7,-13.1 28,-33.4 28,-55.7l0,-6.7l-28,-10.4Z" style="fill:#d0d4d8;fill-rule:nonzero;"/>
+        <path d="M89.857,82.858c7.805,-7.805 20.478,-7.805 28.284,0c7.805,7.805 7.805,20.479 0,28.284c-7.806,7.805 -20.479,7.805 -28.284,0c-7.806,-7.805 -7.806,-20.479 0,-28.284Z" style="fill:#ff5576;"/>
+        <path d="M110.9,100l-14,0c-1.7,0 -3,-1.3 -3,-3c0,-1.7 1.3,-3 3,-3l14,0c1.7,0 3,1.3 3,3c0,1.7 -1.3,3 -3,3Z" style="fill:#fff;fill-rule:nonzero;"/>
+    </g>
+</svg>

+ 33 - 0
client/static/svg/icon-safety-float.svg

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve" width="128px" height="128px">
+<path style="fill:#FFFFFF;" d="M64,14c-27.6,0-50,22.4-50,50s22.4,50,50,50s50-22.4,50-50S91.6,14,64,14z M64,89  c-13.8,0-25-11.2-25-25s11.2-25,25-25s25,11.2,25,25S77.8,89,64,89z"/>
+<g>
+	<path style="fill:#FF5576;" d="M14,64c0,6.5,1.2,12.7,3.5,18.4l22.7-10.6C39.4,69.3,39,66.7,39,64c0-9,4.7-16.8,11.8-21.2L40.2,20   C24.6,28.5,14,45,14,64z"/>
+</g>
+<g>
+	<path style="fill:#FF5576;" d="M64,14c-6.5,0-12.7,1.2-18.4,3.5l10.6,22.7c2.4-0.8,5.1-1.2,7.8-1.2c9,0,16.8,4.7,21.2,11.8   L108,40.2C99.5,24.6,83,14,64,14z"/>
+</g>
+<path style="fill:#C3DBEA;" d="M55,37.5L52.4,32c-1.9,0.7-3.7,1.6-5.4,2.6l2.5,5.5C51.2,39,53,38.2,55,37.5z"/>
+<g>
+	<path style="fill:#FF5576;" d="M64,89c-9,0-16.8-4.7-21.2-11.8L20,87.8c8.5,15.6,25,26.2,44,26.2c6.5,0,12.7-1.2,18.4-3.5   L71.8,87.8C69.3,88.6,66.7,89,64,89z"/>
+</g>
+<g>
+	<path style="fill:#FF5576;" d="M114,64c0-6.5-1.2-12.7-3.5-18.4L87.8,56.2c0.8,2.4,1.2,5.1,1.2,7.8c0,9-4.7,16.8-11.8,21.2   L87.8,108C103.4,99.5,114,83,114,64z"/>
+</g>
+<path style="fill:#C3DBEA;" d="M81.1,107.8c1.9-0.7,3.7-1.6,5.4-2.5L84,99.8c-1.7,1-3.6,1.8-5.4,2.5L81.1,107.8z"/>
+<path style="fill:#FFFFFF;" d="M21.5,55.7c-0.3,0-0.5,0-0.8-0.1c-1.6-0.4-2.6-2.1-2.1-3.7c2.5-9.5,7.9-17.9,15.6-24.2  c1.3-1.1,3.2-0.9,4.2,0.4c1.1,1.3,0.9,3.2-0.4,4.2c-6.7,5.5-11.4,12.8-13.6,21.1C24,54.8,22.8,55.7,21.5,55.7z"/>
+<path style="fill:#FFFFFF;" d="M20,67c-0.8,0-1.6-0.3-2.1-0.9c-0.1-0.1-0.3-0.3-0.4-0.4c-0.1-0.2-0.2-0.3-0.3-0.5  c-0.1-0.2-0.1-0.4-0.2-0.6c0-0.2-0.1-0.4-0.1-0.6c0-0.8,0.3-1.6,0.9-2.1c1.1-1.1,3.1-1.1,4.2,0c0.6,0.6,0.9,1.3,0.9,2.1  c0,0.8-0.3,1.6-0.9,2.1C21.6,66.7,20.8,67,20,67z"/>
+<path style="fill:#DB3E64;" d="M33,67c-1.7,0-3-1.3-3-3c0-18.7,15.3-34,34-34c1.7,0,3,1.3,3,3s-1.3,3-3,3c-2.7,0-5.3,2.6-7.8,4.2  c0,0-1.4-1.4-2.9-0.7c-0.9,0.4-1.9,2.9-2.5,3.2l-2.9,1.5C39.8,49.1,36,53.9,36,64C36,65.7,34.7,67,33,67z"/>
+<path style="fill:#FF5576;" d="M56.2,40.2L52.4,32c-1.9,0.7-3.7,1.5-5.4,2.6l3.8,8.2C52.5,41.7,54.3,40.9,56.2,40.2z"/>
+<path style="fill:#DB3E64;" d="M64,111c-1.7,0-3-1.3-3-3s1.3-3,3-3c22.6,0,41-18.4,41-41c0-1.7,1.3-3,3-3s3,1.3,3,3  c0,16.2-8.2,30.5-20.7,39c-1.2,0.8-1.2,4.3-2.4,5c-0.7,0.4-2.7-1.1-3.5-0.8c-1.1,0.5-0.9,2.9-2,3.3c-1.3,0.5-3.8-1.8-5.2-1.4  C73,110.3,68.6,111,64,111z"/>
+<path style="fill:#FF5576;" d="M82.4,110.5c1.9-0.7,3.7-1.6,5.4-2.5L84,99.8c-1.7,1-3.6,1.8-5.4,2.5L82.4,110.5z"/>
+<path style="fill:#444B54;" d="M111,64c0,16.8-8.9,31.6-22.2,39.9c-1.3,0.8-1.8,2.4-1.1,3.8l0,0c0.8,1.6,2.8,2.2,4.3,1.3  c15-9.4,25-26,25-45c0-5.7-0.9-11.3-2.6-16.4c-0.6-1.7-2.5-2.6-4.1-1.8l0,0c-1.4,0.6-2,2.2-1.6,3.7C110.2,54,111,58.9,111,64z"/>
+<path style="fill:#444B54;" d="M64,111c-16.8,0-31.6-8.9-39.9-22.2c-0.8-1.3-2.4-1.8-3.8-1.1l0,0c-1.6,0.8-2.2,2.8-1.3,4.3  c9.4,15,26,25,45,25c5.7,0,11.3-0.9,16.4-2.6c1.7-0.6,2.6-2.5,1.8-4.1l0,0c-0.6-1.4-2.2-2-3.7-1.6C74,110.2,69.1,111,64,111z"/>
+<path style="fill:#444B54;" d="M42,64c0-6.9,3.2-13,8.1-17.1c1.1-0.9,1.5-2.4,0.9-3.6l0,0c-0.8-1.8-3.1-2.3-4.6-1.1  C40.1,47.3,36,55.2,36,64c0,1.9,0.2,3.7,0.5,5.5c0.4,1.9,2.5,2.9,4.2,2.1l0,0c1.2-0.6,1.9-1.9,1.7-3.2C42.1,66.9,42,65.5,42,64z"/>
+<path style="fill:#444B54;" d="M64,42c6.9,0,13,3.2,17.1,8.1c0.9,1.1,2.4,1.5,3.6,0.9l0,0c1.8-0.8,2.3-3.1,1.1-4.6  C80.7,40.1,72.8,36,64,36c-1.9,0-3.7,0.2-5.5,0.5c-1.9,0.4-2.9,2.5-2.1,4.2l0,0c0.6,1.2,1.9,1.9,3.2,1.7C61.1,42.1,62.5,42,64,42z"/>
+<path style="fill:#444B54;" d="M64,86c-6.9,0-13-3.2-17.1-8.1c-0.9-1.1-2.4-1.5-3.6-0.9l0,0c-1.8,0.8-2.3,3.1-1.1,4.6  C47.3,87.9,55.2,92,64,92c1.9,0,3.7-0.2,5.5-0.5c1.9-0.4,2.9-2.5,2.1-4.2l0,0c-0.6-1.2-1.9-1.9-3.2-1.7C66.9,85.9,65.5,86,64,86z"/>
+<path style="fill:#444B54;" d="M86,64c0,6.9-3.2,13-8.1,17.1c-1.1,0.9-1.5,2.4-0.9,3.6l0,0c0.8,1.8,3.1,2.3,4.6,1.1  C87.9,80.7,92,72.8,92,64c0-1.9-0.2-3.7-0.5-5.5c-0.4-1.9-2.5-2.9-4.2-2.1l0,0c-1.2,0.6-1.9,1.9-1.7,3.2C85.9,61.1,86,62.5,86,64z"/>
+<path style="fill:#444B54;" d="M17,64c0-16.8,8.9-31.6,22.2-39.9c1.3-0.8,1.8-2.4,1.1-3.8l0,0c-0.8-1.6-2.8-2.2-4.3-1.3  c-15,9.4-25,26-25,45c0,5.7,0.9,11.3,2.6,16.4c0.6,1.7,2.5,2.6,4.1,1.8l0,0c1.4-0.6,2-2.2,1.6-3.7C17.8,74,17,69.1,17,64z"/>
+<path style="fill:#444B54;" d="M64,17c16.8,0,31.6,8.9,39.9,22.2c0.8,1.3,2.4,1.8,3.8,1.1l0,0c1.6-0.8,2.2-2.8,1.3-4.3  c-9.4-15-26-25-45-25c-5.7,0-11.3,0.9-16.4,2.6c-1.7,0.6-2.6,2.5-1.8,4.1l0,0c0.6,1.4,2.2,2,3.7,1.6C54,17.8,58.9,17,64,17z"/>
+</svg>

+ 3 - 0
client/static/svg/motif-diagonals.svg

@@ -0,0 +1,3 @@
+<svg width="100%" height="100%" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
+    <path d="M40,0l-40,40l0,-20l20,-20l20,0Zm0,40l0,-20l-20,20l20,0Z" fill='#FFFFFF' fill-opacity='0.03' fill-rule='evenodd' />
+</svg>

+ 2 - 4
client/themes/default/components/page.vue

@@ -189,10 +189,8 @@ export default {
       },
       breadcrumbs: [
         { path: '/', name: 'Home' },
-        { path: '/universe', name: 'Universe' },
-        { path: '/universe/galaxy', name: 'Galaxy' },
-        { path: '/universe/galaxy/solar-system', name: 'Solar System' },
-        { path: '/universe/galaxy/solar-system/planet-earth', name: 'Planet Earth' }
+        { path: '/' + this.path, name: 'Breadcrumb' },
+        { path: '/' + this.path, name: 'Coming soon' }
       ],
       scrollStyle: {
         vuescroll: {},

+ 0 - 12
dev/examples/Dockerfile

@@ -1,12 +0,0 @@
-FROM requarks/wiki:latest
-
-# Replace with your email address:
-ENV WIKI_ADMIN_EMAIL admin@example.com
-
-WORKDIR /var/wiki
-
-# Replace your-config.yml with the path to your config file:
-ADD your-config.yml config.yml
-
-EXPOSE 3000
-ENTRYPOINT [ "node", "server" ]

+ 50 - 16
dev/examples/docker-compose.yml

@@ -1,19 +1,53 @@
-version: '3'
+# -- DEV DOCKER-COMPOSE --
+# -- DO NOT USE IN PRODUCTION! --
+
+version: "3"
 services:
-  wikidb:
-    image: mongo
-    expose:
-      - '27017'
-    command: '--smallfiles --logpath=/dev/null'
-    volumes:
-      - ./data/mongo:/data/db
-  wikijs:
-    image: 'requarks/wiki:latest'
-    links:
-      - wikidb
-    ports:
-      - '80:3000'
+
+  redis:
+    image: redis:4-alpine
+    logging:
+      driver: "none"
+    networks:
+      - wikinet
+
+  db:
+    image: postgres:9-alpine
     environment:
-      WIKI_ADMIN_EMAIL: admin@example.com
+      POSTGRES_DB: wiki
+      POSTGRES_PASSWORD: wikijsrocks
+      POSTGRES_USER: wikijs
+    logging:
+      driver: "none"
     volumes:
-      - ./config.yml:/var/wiki/config.yml
+      - db-data:/var/lib/postgresql/data
+    networks:
+      - wikinet
+
+  wiki:
+    image: requarks/wiki:beta
+    depends_on:
+      - db
+      - redis
+    environment:
+      PORT: 3000 # DO NOT CHANGE! Use ports below to specify listening port.
+      DB_TYPE: postgres
+      DB_HOST: db
+      DB_PORT: 5432
+      DB_USER: wikijs
+      DB_PASS: wikijsrocks
+      DB_NAME: wiki
+      REDIS_HOST: redis
+      REDIS_PORT: 6379
+      REDIS_DB: 0
+      REDIS_PASS: ''
+    networks:
+      - wikinet
+    ports:
+      - "3000:3000" # <-- replace with "80:3000" to listen on port 80 instead
+
+networks:
+  wikinet:
+
+volumes:
+  db-data:

+ 0 - 9
server/app/data.yml

@@ -22,14 +22,12 @@ defaults:
       db: 0
       password: null
     # DB defaults
-    defaultEditor: 'markdown'
     graphEndpoint: 'https://graph.requarks.io'
     lang:
       code: en
       autoUpdate: true
       namespaces: []
       namespacing: false
-    public: false
     telemetry:
       clientId: ''
       isEnabled: false
@@ -47,13 +45,6 @@ defaults:
       maxAge: 600
       methods: 'GET,POST'
       origin: true
-configNamespaces:
-  - auth
-  - features
-  - logging
-  - site
-  - theme
-  - uploads
 localeNamespaces:
   - admin
   - auth

+ 41 - 0
server/controllers/common.js

@@ -5,6 +5,18 @@ const _ = require('lodash')
 
 /* global WIKI */
 
+/**
+ * Robots.txt
+ */
+router.get('/robots.txt', (req, res, next) => {
+  res.type('text/plain')
+  if (_.includes(WIKI.config.seo.robots, 'noindex')) {
+    res.send("User-agent: *\nDisallow: /")
+  } else {
+    res.status(200).end()
+  }
+})
+
 /**
  * Create/Edit document
  */
@@ -17,12 +29,20 @@ router.get(['/e', '/e/*'], async (req, res, next) => {
     isPrivate: false
   })
   if (page) {
+    if (!WIKI.auth.checkAccess(req.user, ['manage:pages'], pageArgs)) {
+      return res.render('unauthorized', { action: 'edit'})
+    }
+
     _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
     _.set(res.locals, 'pageMeta.description', page.description)
     page.mode = 'update'
     page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
     page.content = Buffer.from(page.content).toString('base64')
   } else {
+    if (!WIKI.auth.checkAccess(req.user, ['write:pages'], pageArgs)) {
+      return res.render('unauthorized', { action: 'create'})
+    }
+
     _.set(res.locals, 'pageMeta.title', `New Page`)
     page = {
       path: pageArgs.path,
@@ -56,6 +76,11 @@ router.get(['/p', '/p/*'], (req, res, next) => {
  */
 router.get(['/h', '/h/*'], async (req, res, next) => {
   const pageArgs = pageHelper.parsePath(req.path)
+
+  if (!WIKI.auth.checkAccess(req.user, ['read:pages'], pageArgs)) {
+    return res.render('unauthorized', { action: 'history'})
+  }
+
   const page = await WIKI.models.pages.getPageFromDb({
     path: pageArgs.path,
     locale: pageArgs.locale,
@@ -76,6 +101,11 @@ router.get(['/h', '/h/*'], async (req, res, next) => {
  */
 router.get(['/s', '/s/*'], async (req, res, next) => {
   const pageArgs = pageHelper.parsePath(req.path)
+
+  if (!WIKI.auth.checkAccess(req.user, ['read:pages'], pageArgs)) {
+    return res.render('unauthorized', { action: 'source'})
+  }
+
   const page = await WIKI.models.pages.getPageFromDb({
     path: pageArgs.path,
     locale: pageArgs.locale,
@@ -96,6 +126,15 @@ router.get(['/s', '/s/*'], async (req, res, next) => {
  */
 router.get('/*', async (req, res, next) => {
   const pageArgs = pageHelper.parsePath(req.path)
+
+  if (!WIKI.auth.checkAccess(req.user, ['read:pages'], pageArgs)) {
+    if (pageArgs.path === 'home') {
+      return res.redirect('/login')
+    } else {
+      return res.render('unauthorized', { action: 'view'})
+    }
+  }
+
   const page = await WIKI.models.pages.getPage({
     path: pageArgs.path,
     locale: pageArgs.locale,
@@ -108,8 +147,10 @@ router.get('/*', async (req, res, next) => {
     const sidebar = await WIKI.models.navigation.getTree({ cache: true })
     res.render('page', { page, sidebar })
   } else if (pageArgs.path === 'home') {
+    _.set(res.locals, 'pageMeta.title', 'Welcome')
     res.render('welcome')
   } else {
+    _.set(res.locals, 'pageMeta.title', 'Page Not Found')
     res.status(404).render('new', { pagePath: req.path })
   }
 })

+ 84 - 4
server/core/auth.js

@@ -3,6 +3,8 @@ const passportJWT = require('passport-jwt')
 const fs = require('fs-extra')
 const _ = require('lodash')
 const path = require('path')
+const jwt = require('jsonwebtoken')
+const moment = require('moment')
 
 const securityHelper = require('../helpers/security')
 
@@ -10,11 +12,16 @@ const securityHelper = require('../helpers/security')
 
 module.exports = {
   strategies: {},
+  guest: {
+    cacheExpiration: moment.utc().subtract(1, 'd')
+  },
+
+  /**
+   * Initialize the authentication module
+   */
   init() {
     this.passport = passport
 
-    // Serialization user methods
-
     passport.serializeUser(function (user, done) {
       done(null, user.id)
     })
@@ -34,6 +41,10 @@ module.exports = {
 
     return this
   },
+
+  /**
+   * Load authentication strategies
+   */
   async activateStrategies() {
     try {
       // Unload any active strategies
@@ -46,7 +57,7 @@ module.exports = {
       passport.use('jwt', new passportJWT.Strategy({
         jwtFromRequest: securityHelper.extractJWT,
         secretOrKey: WIKI.config.certs.public,
-        audience: 'urn:wiki.js', // TODO: use value from admin
+        audience: WIKI.config.auth.audience,
         issuer: 'urn:wiki.js'
       }, (jwtPayload, cb) => {
         cb(null, jwtPayload)
@@ -60,7 +71,7 @@ module.exports = {
 
         const strategy = require(`../modules/authentication/${stg.key}/authentication.js`)
 
-        stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback` // TODO: config.host
+        stg.config.callbackURL = `${WIKI.config.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 => {
@@ -79,5 +90,74 @@ module.exports = {
       WIKI.logger.error(`Authentication Strategy: [ FAILED ]`)
       WIKI.logger.error(err)
     }
+  },
+
+  /**
+   * Authenticate current request
+   *
+   * @param {Express Request} req
+   * @param {Express Response} res
+   * @param {Express Next Callback} next
+   */
+  authenticate(req, res, next) {
+    WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
+      if (err) { return next() }
+
+      // Expired but still valid within N days, just renew
+      if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(14, 'days').isBefore(info.expiredAt)) {
+        const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
+        try {
+          const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
+          user = newToken.user
+
+          // Try headers, otherwise cookies for response
+          if (req.get('content-type') === 'application/json') {
+            res.set('new-jwt', newToken.token)
+          } else {
+            res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
+          }
+        } catch (err) {
+          return next()
+        }
+      }
+
+      // JWT is NOT valid, set as guest
+      if (!user) {
+        if (WIKI.auth.guest.cacheExpiration ) {
+          WIKI.auth.guest = await WIKI.models.users.getGuestUser()
+          WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
+        }
+        req.user = WIKI.auth.guest
+        return next()
+      }
+
+      // JWT is valid
+      req.logIn(user, { session: false }, (err) => {
+        if (err) { return next(err) }
+        next()
+      })
+    })(req, res, next)
+  },
+
+  /**
+   * Check if user has access to resource
+   *
+   * @param {User} user
+   * @param {Array<String>} permissions
+   * @param {String|Boolean} path
+   */
+  checkAccess(user, permissions = [], path = false) {
+    // System Admin
+    if (_.includes(user.permissions, 'manage:system')) {
+      return true
+    }
+
+    // Check Global Permissions
+    if (_.intersection(user.permissions, permissions).length < 1) {
+      return false
+    }
+
+    // Check Page Rules
+    return false
   }
 }

+ 0 - 2
server/core/config.js

@@ -52,8 +52,6 @@ module.exports = {
       appconfig.port = process.env.PORT || 80
     }
 
-    appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true')
-
     WIKI.config = appconfig
     WIKI.data = appdata
     WIKI.version = require(path.join(WIKI.ROOTPATH, 'package.json')).version

+ 25 - 22
server/db/migrations/2.0.0.js → server/db/migrations/2.0.0-beta.1.js

@@ -1,11 +1,14 @@
 exports.up = knex => {
+  const dbCompat = {
+    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
+  }
   return knex.schema
     // =====================================
     // MODEL TABLES
     // =====================================
     // ASSETS ------------------------------
     .createTable('assets', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('filename').notNullable()
       table.string('basename').notNullable()
@@ -19,7 +22,7 @@ exports.up = knex => {
     })
     // ASSET FOLDERS -----------------------
     .createTable('assetFolders', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('name').notNullable()
       table.string('slug').notNullable()
@@ -27,7 +30,7 @@ exports.up = knex => {
     })
     // AUTHENTICATION ----------------------
     .createTable('authentication', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.boolean('isEnabled').notNullable().defaultTo(false)
       table.json('config').notNullable()
@@ -37,7 +40,7 @@ exports.up = knex => {
     })
     // COMMENTS ----------------------------
     .createTable('comments', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.text('content').notNullable()
       table.string('createdAt').notNullable()
@@ -45,14 +48,14 @@ exports.up = knex => {
     })
     // EDITORS -----------------------------
     .createTable('editors', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.boolean('isEnabled').notNullable().defaultTo(false)
       table.json('config').notNullable()
     })
     // GROUPS ------------------------------
     .createTable('groups', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('name').notNullable()
       table.json('permissions').notNullable()
@@ -63,7 +66,7 @@ exports.up = knex => {
     })
     // LOCALES -----------------------------
     .createTable('locales', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('code', 2).notNullable().primary()
       table.json('strings')
       table.boolean('isRTL').notNullable().defaultTo(false)
@@ -74,7 +77,7 @@ exports.up = knex => {
     })
     // LOGGING ----------------------------
     .createTable('loggers', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.boolean('isEnabled').notNullable().defaultTo(false)
       table.string('level').notNullable().defaultTo('warn')
@@ -82,13 +85,13 @@ exports.up = knex => {
     })
     // NAVIGATION ----------------------------
     .createTable('navigation', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.json('config')
     })
     // PAGE HISTORY ------------------------
     .createTable('pageHistory', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('path').notNullable()
       table.string('hash').notNullable()
@@ -104,7 +107,7 @@ exports.up = knex => {
     })
     // PAGES -------------------------------
     .createTable('pages', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('path').notNullable()
       table.string('hash').notNullable()
@@ -124,7 +127,7 @@ exports.up = knex => {
     })
     // PAGE TREE ---------------------------
     .createTable('pageTree', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('path').notNullable()
       table.integer('depth').unsigned().notNullable()
@@ -135,28 +138,28 @@ exports.up = knex => {
     })
     // RENDERERS ---------------------------
     .createTable('renderers', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.boolean('isEnabled').notNullable().defaultTo(false)
       table.json('config')
     })
     // SEARCH ------------------------------
     .createTable('searchEngines', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.boolean('isEnabled').notNullable().defaultTo(false)
       table.json('config')
     })
     // SETTINGS ----------------------------
     .createTable('settings', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.json('value')
       table.string('updatedAt').notNullable()
     })
     // STORAGE -----------------------------
     .createTable('storage', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.string('key').notNullable().primary()
       table.boolean('isEnabled').notNullable().defaultTo(false)
       table.string('mode', ['sync', 'push', 'pull']).notNullable().defaultTo('push')
@@ -164,7 +167,7 @@ exports.up = knex => {
     })
     // TAGS --------------------------------
     .createTable('tags', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('tag').notNullable().unique()
       table.string('title')
@@ -173,7 +176,7 @@ exports.up = knex => {
     })
     // USER KEYS ---------------------------
     .createTable('userKeys', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('kind').notNullable()
       table.string('token').notNullable()
@@ -182,7 +185,7 @@ exports.up = knex => {
     })
     // USERS -------------------------------
     .createTable('users', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.string('email').notNullable()
       table.string('name').notNullable()
@@ -205,21 +208,21 @@ exports.up = knex => {
     // =====================================
     // PAGE HISTORY TAGS ---------------------------
     .createTable('pageHistoryTags', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
       table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
     })
     // PAGE TAGS ---------------------------
     .createTable('pageTags', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.integer('pageId').unsigned().references('id').inTable('pages').onDelete('CASCADE')
       table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
     })
     // USER GROUPS -------------------------
     .createTable('userGroups', table => {
-      table.charset('utf8mb4')
+      if (dbCompat.charset) { table.charset('utf8mb4') }
       table.increments('id').primary()
       table.integer('userId').unsigned().references('id').inTable('users').onDelete('CASCADE')
       table.integer('groupId').unsigned().references('id').inTable('groups').onDelete('CASCADE')

+ 0 - 11
server/db/seeds/settings.js

@@ -1,11 +0,0 @@
-exports.seed = (knex, Promise) => {
-  return knex('settings')
-    .insert([
-      { key: 'auth', value: {} },
-      { key: 'features', value: {} },
-      { key: 'logging', value: {} },
-      { key: 'site', value: {} },
-      { key: 'theme', value: {} },
-      { key: 'uploads', value: {} }
-    ])
-}

+ 7 - 0
server/graph/resolvers/authentication.js

@@ -70,6 +70,13 @@ module.exports = {
     },
     async updateStrategies(obj, args, context) {
       try {
+        WIKI.config.auth = {
+          audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
+          tokenExpiration: _.get(args, 'config.tokenExpiration', WIKI.config.auth.tokenExpiration),
+          tokenRenewal: _.get(args, 'config.tokenRenewal', WIKI.config.auth.tokenRenewal)
+        }
+        await WIKI.configSvc.saveToDb(['auth'])
+
         for (let str of args.strategies) {
           await WIKI.models.authentication.query().patch({
             isEnabled: str.isEnabled,

+ 8 - 1
server/graph/schemas/authentication.graphql

@@ -43,7 +43,8 @@ type AuthenticationMutation {
   ): AuthenticationRegisterResponse
 
   updateStrategies(
-    strategies: [AuthenticationStrategyInput]
+    strategies: [AuthenticationStrategyInput]!
+    config: AuthenticationConfigInput
   ): DefaultResponse @auth(requires: ["manage:system"])
 }
 
@@ -88,3 +89,9 @@ input AuthenticationStrategyInput {
   domainWhitelist: [String]!
   autoEnrollGroups: [Int]!
 }
+
+input AuthenticationConfigInput {
+  audience: String!
+  tokenExpiration: String!
+  tokenRenewal: String!
+}

+ 2 - 3
server/master.js

@@ -67,7 +67,7 @@ module.exports = async () => {
 
   app.use(cookieParser())
   app.use(WIKI.auth.passport.initialize())
-  app.use(mw.auth.jwt)
+  app.use(WIKI.auth.authenticate)
 
   // ----------------------------------------
   // SEO
@@ -138,8 +138,7 @@ module.exports = async () => {
   // ----------------------------------------
 
   app.use('/', ctrl.auth)
-
-  app.use('/', mw.auth.checkPath, ctrl.common)
+  app.use('/', ctrl.common)
 
   // ----------------------------------------
   // Error handling

+ 0 - 72
server/middlewares/auth.js

@@ -1,72 +0,0 @@
-const jwt = require('jsonwebtoken')
-const moment = require('moment')
-
-const securityHelper = require('../helpers/security')
-
-/* global WIKI */
-
-/**
- * Authentication middleware
- */
-module.exports = {
-  jwt(req, res, next) {
-    WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
-      if (err) { return next() }
-
-      // Expired but still valid within 7 days, just renew
-      if (info instanceof Error && info.name === 'TokenExpiredError' && moment().subtract(14, 'days').isBefore(info.expiredAt)) {
-        const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
-        try {
-          const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
-          user = newToken.user
-
-          // Try headers, otherwise cookies for response
-          if (req.get('content-type') === 'application/json') {
-            res.set('new-jwt', newToken.token)
-          } else {
-            res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
-          }
-        } catch (err) {
-          return next()
-        }
-      }
-
-      // JWT is NOT valid
-      if (!user) { return next() }
-
-      // JWT is valid
-      req.logIn(user, { session: false }, (err) => {
-        if (err) { return next(err) }
-        next()
-      })
-    })(req, res, next)
-  },
-  checkPath(req, res, next) {
-    // Is user authenticated ?
-
-    if (!req.isAuthenticated()) {
-      if (WIKI.config.public !== true) {
-        return res.redirect('/login')
-      } else {
-        // req.user = rights.guest
-        res.locals.isGuest = true
-      }
-    } else {
-      res.locals.isGuest = false
-    }
-
-    // Check permissions
-
-    // res.locals.rights = rights.check(req)
-
-    // if (!res.locals.rights.read) {
-    //   return res.render('error-forbidden')
-    // }
-
-    // Expose user data
-
-    res.locals.user = req.user
-
-    return next()
-  }
-}

+ 13 - 2
server/models/users.js

@@ -138,6 +138,11 @@ module.exports = class User extends Model {
     return (result && _.has(result, 'delta') && result.delta === 0)
   }
 
+  async getPermissions() {
+    const permissions = await this.$relatedQuery('groups').select('permissions').pluck('permissions')
+    this.permissions = _.uniq(_.flatten(permissions))
+  }
+
   static async processProfile(profile) {
     let primaryEmail = ''
     if (_.isArray(profile.emails)) {
@@ -262,8 +267,8 @@ module.exports = class User extends Model {
         passphrase: WIKI.config.sessionSecret
       }, {
         algorithm: 'RS256',
-        expiresIn: '30m',
-        audience: 'urn:wiki.js', // TODO: use value from admin
+        expiresIn: WIKI.config.auth.tokenExpiration,
+        audience: WIKI.config.auth.audience,
         issuer: 'urn:wiki.js'
       }),
       user
@@ -391,4 +396,10 @@ module.exports = class User extends Model {
       throw new WIKI.Error.AuthRegistrationDisabled()
     }
   }
+
+  static async getGuestUser () {
+    let user = await WIKI.models.users.query().findById(2)
+    user.getPermissions()
+    return user
+  }
 }

+ 12 - 8
server/setup.js

@@ -104,8 +104,12 @@ module.exports = () => {
       await fs.ensureDir(path.join(dataPath, 'uploads'))
 
       // Set config
+      _.set(WIKI.config, 'auth', {
+        audience: 'urn:wiki.js',
+        tokenExpiration: '30m',
+        tokenRenewal: '14d'
+      })
       _.set(WIKI.config, 'company', '')
-      _.set(WIKI.config, 'defaultEditor', 'markdown')
       _.set(WIKI.config, 'features', {
         featurePageRatings: true,
         featurePageComments: true,
@@ -136,7 +140,6 @@ module.exports = () => {
         dkimKeySelector: '',
         dkimPrivateKey: ''
       })
-      _.set(WIKI.config, 'public', false)
       _.set(WIKI.config, 'seo', {
         description: '',
         robots: ['index', 'follow'],
@@ -145,7 +148,7 @@ module.exports = () => {
       })
       _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
       _.set(WIKI.config, 'telemetry', {
-        isEnabled: req.body.telemetry === 'true',
+        isEnabled: req.body.telemetry === true,
         clientId: WIKI.telemetry.cid
       })
       _.set(WIKI.config, 'theming', {
@@ -179,16 +182,15 @@ module.exports = () => {
       // Save config to DB
       WIKI.logger.info('Persisting config to DB...')
       await WIKI.configSvc.saveToDb([
+        'auth',
         'certs',
         'company',
-        'defaultEditor',
         'features',
         'graphEndpoint',
         'host',
         'lang',
         'logo',
         'mail',
-        'public',
         'seo',
         'sessionSecret',
         'telemetry',
@@ -389,8 +391,10 @@ module.exports = () => {
 
   WIKI.server.on('listening', () => {
     WIKI.logger.info('HTTP Server: [ RUNNING ]')
-    WIKI.logger.info('========================================')
-    WIKI.logger.info(`Browse to http://localhost:${WIKI.config.port}/`)
-    WIKI.logger.info('========================================')
+    WIKI.logger.info('🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻')
+    WIKI.logger.info('')
+    WIKI.logger.info(`Browse to http://localhost:${WIKI.config.port}/ to complete setup!`)
+    WIKI.logger.info('')
+    WIKI.logger.info('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
   })
 }

+ 13 - 0
server/views/unauthorized.pug

@@ -0,0 +1,13 @@
+extends master.pug
+
+block body
+  #root.is-fullscreen
+    v-app
+      .unauthorized
+        .unauthorized-content
+          img.animated.fadeIn(src='/svg/icon-delete-shield.svg', alt='Unauthorized')
+          .headline= t('unauthorized.title')
+          .subheading.mt-3= t('unauthorized.action.' + action)
+          v-btn.mt-5(color='red lighten-4', href='javascript:window.history.go(-1);', large, outline)
+            v-icon(left) arrow_back
+            span= t('unauthorized.goback')