瀏覽代碼

feat: mandatory password change on login + UI fixes

Nick 5 年之前
父節點
當前提交
d3e693ab46
共有 40 個文件被更改,包括 975 次插入575 次删除
  1. 1 1
      client/components/admin/admin-contribute.vue
  2. 1 1
      client/components/admin/admin-dev-flags.vue
  3. 108 26
      client/components/admin/admin-general.vue
  4. 17 22
      client/components/admin/admin-groups-edit-users.vue
  5. 1 1
      client/components/admin/admin-groups.vue
  6. 3 3
      client/components/admin/admin-pages-edit.vue
  7. 1 1
      client/components/admin/admin-pages.vue
  8. 2 2
      client/components/admin/admin-search.vue
  9. 10 4
      client/components/admin/admin-storage.vue
  10. 10 10
      client/components/admin/admin-system.vue
  11. 2 1
      client/components/admin/admin-users-create.vue
  12. 59 38
      client/components/admin/admin-users-edit.vue
  13. 1 1
      client/components/common/page-delete.vue
  14. 3 3
      client/components/common/user-search.vue
  15. 126 46
      client/components/login.vue
  16. 13 1
      client/graph/admin/site/site-mutation-save-config.gql
  17. 6 0
      client/graph/admin/site/site-query-config.gql
  18. 2 0
      client/graph/admin/users/users-query-single.gql
  19. 13 0
      client/graph/login/login-mutation-changepassword.gql
  20. 3 2
      client/graph/login/login-mutation-login.gql
  21. 3 2
      client/graph/login/login-mutation-tfa.gql
  22. 16 3
      client/scss/pages/_error.scss
  23. 20 19
      client/themes/default/components/page.vue
  24. 1 1
      dev/templates/legacy.pug
  25. 1 1
      dev/templates/master.pug
  26. 56 53
      package.json
  27. 7 0
      server/app/data.yml
  28. 23 9
      server/graph/resolvers/authentication.js
  29. 11 2
      server/graph/resolvers/site.js
  30. 10 4
      server/graph/schemas/authentication.graphql
  31. 12 0
      server/graph/schemas/site.graphql
  32. 2 0
      server/graph/schemas/user.graphql
  33. 12 3
      server/middlewares/security.js
  34. 1 1
      server/models/userKeys.js
  35. 61 16
      server/models/users.js
  36. 1 1
      server/modules/authentication/local/definition.yml
  37. 7 21
      server/views/error.pug
  38. 1 1
      server/views/legacy/master.pug
  39. 1 1
      server/views/master.pug
  40. 347 274
      yarn.lock

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

@@ -66,7 +66,7 @@
               v-tab-item(:transition='false', :reverse-transition='false')
               v-tab-item(:transition='false', :reverse-transition='false')
                 .body-1.pa-3 {{ $t('admin:contribute.tshirts') }}
                 .body-1.pa-3 {{ $t('admin:contribute.tshirts') }}
                 v-card-actions.ml-2
                 v-card-actions.ml-2
-                  v-btn(outline, :color='darkMode ? `blue lighten-1` : `primary`', href='https://wikijs.threadless.com', large)
+                  v-btn(outlined, :color='darkMode ? `blue lighten-1` : `primary`', href='https://wikijs.threadless.com', large)
                     v-icon(left) mdi-tshirt-crew
                     v-icon(left) mdi-tshirt-crew
                     span {{ $t('admin:contribute.shop') }}
                     span {{ $t('admin:contribute.shop') }}
             v-divider.mt-3
             v-divider.mt-3

+ 1 - 1
client/components/admin/admin-dev-flags.vue

@@ -13,7 +13,7 @@
             span {{$t('common:actions.apply')}}
             span {{$t('common:actions.apply')}}
 
 
         v-card.mt-3.white.grey--text.text--darken-3
         v-card.mt-3.white.grey--text.text--darken-3
-          v-alert(color='red', value='true', icon='mdi-alert', dark, prominent)
+          v-alert(color='red', :value='true', icon='mdi-alert', dark, prominent)
             span Do NOT enable these flags unless you know what you're doing!
             span Do NOT enable these flags unless you know what you're doing!
             .caption Doing so may result in data loss or broken installation!
             .caption Doing so may result in data loss or broken installation!
           v-card-text
           v-card-text

+ 108 - 26
client/components/admin/admin-general.vue

@@ -92,14 +92,14 @@
 
 
             v-flex(lg6 xs12)
             v-flex(lg6 xs12)
               v-card.animated.fadeInUp.wait-p4s
               v-card.animated.fadeInUp.wait-p4s
-                v-toolbar(color='primary', dark, dense, flat)
+                v-toolbar(color='indigo', dark, dense, flat)
                   v-toolbar-title.subtitle-1 Features
                   v-toolbar-title.subtitle-1 Features
                   v-spacer
                   v-spacer
-                  v-chip(label, color='white', small).primary--text coming soon
+                  v-chip(label, color='white', small).indigo--text coming soon
                 v-card-text
                 v-card-text
                   v-switch(
                   v-switch(
                     label='Asset Image Optimization'
                     label='Asset Image Optimization'
-                    color='primary'
+                    color='indigo'
                     v-model='config.featureTinyPNG'
                     v-model='config.featureTinyPNG'
                     persistent-hint
                     persistent-hint
                     hint='Image optimization tool to reduce filesize and bandwidth costs.'
                     hint='Image optimization tool to reduce filesize and bandwidth costs.'
@@ -119,7 +119,7 @@
                   v-divider.mt-3
                   v-divider.mt-3
                   v-switch(
                   v-switch(
                     label='Page Ratings'
                     label='Page Ratings'
-                    color='primary'
+                    color='indigo'
                     v-model='config.featurePageRatings'
                     v-model='config.featurePageRatings'
                     persistent-hint
                     persistent-hint
                     hint='Allow users to rate pages.'
                     hint='Allow users to rate pages.'
@@ -129,7 +129,7 @@
                   v-divider.mt-3
                   v-divider.mt-3
                   v-switch(
                   v-switch(
                     label='Page Comments'
                     label='Page Comments'
-                    color='primary'
+                    color='indigo'
                     v-model='config.featurePageComments'
                     v-model='config.featurePageComments'
                     persistent-hint
                     persistent-hint
                     hint='Allow users to leave comments on pages.'
                     hint='Allow users to leave comments on pages.'
@@ -139,13 +139,75 @@
                   v-divider.mt-3
                   v-divider.mt-3
                   v-switch(
                   v-switch(
                     label='Personal Wikis'
                     label='Personal Wikis'
-                    color='primary'
+                    color='indigo'
                     v-model='config.featurePersonalWikis'
                     v-model='config.featurePersonalWikis'
                     persistent-hint
                     persistent-hint
                     hint='Allow users to have their own personal wiki.'
                     hint='Allow users to have their own personal wiki.'
                     disabled
                     disabled
                     )
                     )
 
 
+              v-card.mt-5.animated.fadeInUp.wait-p5s
+                v-toolbar(color='red darken-2', dark, dense, flat)
+                  v-toolbar-title.subtitle-1 Security
+                v-card-text
+                  v-alert(outlined, color='red darken-2', icon='mdi-information-outline').body-2 Make sure to understand the implications before turning on / off a security feature.
+                  v-switch.mt-3(
+                    label='Block IFrame Embedding'
+                    color='red darken-2'
+                    v-model='config.securityIframe'
+                    persistent-hint
+                    hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'
+                    )
+                  v-divider.mt-3
+                  v-switch(
+                    label='Same Origin Referrer Policy'
+                    color='red darken-2'
+                    v-model='config.securityReferrerPolicy'
+                    persistent-hint
+                    hint='Limits the referrer header to same origin.'
+                    )
+
+                  v-divider.mt-3
+                  v-switch(
+                    label='Enforce HSTS'
+                    color='red darken-2'
+                    v-model='config.securityHSTS'
+                    persistent-hint
+                    hint='This ensures the connection cannot be established through an insecure HTTP connection.'
+                    )
+                  v-select.mt-5(
+                    outlined
+                    label='HSTS Max Age'
+                    :items='hstsDurations'
+                    v-model='config.securityHSTSDuration'
+                    prepend-icon='mdi-subdirectory-arrow-right'
+                    :disabled='!config.securityHSTS'
+                    hide-details
+                    style='max-width: 450px;'
+                    )
+                  .pl-11.mt-3
+                    .caption Defines the duration for which the server should only deliver content through HTTPS.
+                    .caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values.
+
+                  v-divider.mt-3
+                  v-switch(
+                    label='Enforce CSP'
+                    color='red darken-2'
+                    v-model='config.securityCSP'
+                    persistent-hint
+                    hint='Restricts scripts to pre-approved content sources.'
+                    disabled
+                    )
+                  v-textarea.mt-5(
+                    label='CSP Directives'
+                    outlined
+                    v-model='config.securityCSPDirectives'
+                    prepend-icon='mdi-subdirectory-arrow-right'
+                    persistent-hint
+                    hint='One directive per line.'
+                    disabled
+                  )
+
 </template>
 </template>
 
 
 <script>
 <script>
@@ -163,12 +225,6 @@ export default {
         { text: 'Google Analytics', value: 'ga' },
         { text: 'Google Analytics', value: 'ga' },
         { text: 'Google Tag Manager', value: 'gtm' }
         { text: 'Google Tag Manager', value: 'gtm' }
       ],
       ],
-      metaRobots: [
-        { text: 'Index', value: 'index' },
-        { text: 'Follow', value: 'follow' },
-        { text: 'No Index', value: 'noindex' },
-        { text: 'No Follow', value: 'nofollow' }
-      ],
       config: {
       config: {
         host: '',
         host: '',
         title: '',
         title: '',
@@ -183,8 +239,28 @@ export default {
         featurePageRatings: false,
         featurePageRatings: false,
         featurePageComments: false,
         featurePageComments: false,
         featurePersonalWikis: false,
         featurePersonalWikis: false,
-        featureTinyPNG: false
-      }
+        featureTinyPNG: false,
+        securityIframe: true,
+        securityReferrerPolicy: true,
+        securityHSTS: false,
+        securityHSTSDuration: 0,
+        securityCSP: false,
+        securityCSPDirectives: ''
+      },
+      hstsDurations: [
+        { value: 300, text: '5 minutes' },
+        { value: 86400, text: '1 day' },
+        { value: 604800, text: '1 week' },
+        { value: 2592000, text: '1 month' },
+        { value: 31536000, text: '1 year' },
+        { value: 63072000, text: '2 years' }
+      ],
+      metaRobots: [
+        { text: 'Index', value: 'index' },
+        { text: 'Follow', value: 'follow' },
+        { text: 'No Index', value: 'noindex' },
+        { text: 'No Follow', value: 'nofollow' }
+      ]
     }
     }
   },
   },
   computed: {
   computed: {
@@ -198,18 +274,24 @@ export default {
         await this.$apollo.mutate({
         await this.$apollo.mutate({
           mutation: siteUpdateConfigMutation,
           mutation: siteUpdateConfigMutation,
           variables: {
           variables: {
-            host: this.config.host || '',
-            title: this.config.title || '',
-            description: this.config.description || '',
-            robots: this.config.robots || [],
-            analyticsService: this.config.analyticsService || '',
-            analyticsId: this.config.analyticsId || '',
-            company: this.config.company || '',
-            hasLogo: this.config.hasLogo || false,
-            logoIsSquare: this.config.logoIsSquare || false,
-            featurePageRatings: this.config.featurePageRatings || false,
-            featurePageComments: this.config.featurePageComments || false,
-            featurePersonalWikis: this.config.featurePersonalWikis || false
+            host: _.get(this.config, 'host', ''),
+            title: _.get(this.config, 'title', ''),
+            description: _.get(this.config, 'description', ''),
+            robots: _.get(this.config, 'robots', []),
+            analyticsService: _.get(this.config, 'analyticsService', ''),
+            analyticsId: _.get(this.config, 'analyticsId', ''),
+            company: _.get(this.config, 'company', ''),
+            hasLogo: _.get(this.config, 'hasLogo', false),
+            logoIsSquare: _.get(this.config, 'logoIsSquare', false),
+            featurePageRatings: _.get(this.config, 'featurePageRatings', false),
+            featurePageComments: _.get(this.config, 'featurePageComments', false),
+            featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false),
+            securityIframe: _.get(this.config, 'securityIframe', false),
+            securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
+            securityHSTS: _.get(this.config, 'securityHSTS', false),
+            securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),
+            securityCSP: _.get(this.config, 'securityCSP', false),
+            securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')
           },
           },
           watchLoading (isLoading) {
           watchLoading (isLoading) {
             this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
             this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')

+ 17 - 22
client/components/admin/admin-groups-edit-users.vue

@@ -23,26 +23,21 @@
       must-sort,
       must-sort,
       hide-default-footer
       hide-default-footer
     )
     )
-      template(slot='item', slot-scope='props')
-        tr(:active='props.selected')
-          td.text-xs-right {{ props.item.id }}
-          td {{ props.item.name }}
-          td {{ props.item.email }}
-          td
-            v-menu(bottom, right, min-width='200')
-              template(v-slot:activator='{ on }')
-                v-btn(icon, v-on='on', small)
-                  v-icon.grey--text.text--darken-1 mdi-dots-horizontal
-              v-list(dense, nav)
-                v-list-item(:to='`/users/` + props.item.id')
-                  v-list-item-action: v-icon(color='primary') mdi-account-outline
-                  v-list-item-content
-                    v-list-item-title View User Profile
-                template(v-if='props.item.id !== 2')
-                  v-list-item(@click='unassignUser(props.item.id)')
-                    v-list-item-action: v-icon(color='orange') mdi-account-remove-outline
-                    v-list-item-content
-                      v-list-item-title Unassign
+      template(v-slot:item.actions='{ item }')
+        v-menu(bottom, right, min-width='200')
+          template(v-slot:activator='{ on }')
+            v-btn(icon, v-on='on', small)
+              v-icon.grey--text.text--darken-1 mdi-dots-horizontal
+          v-list(dense, nav)
+            v-list-item(:to='`/users/` + item.id')
+              v-list-item-action: v-icon(color='primary') mdi-account-outline
+              v-list-item-content
+                v-list-item-title View User Profile
+            template(v-if='item.id !== 2')
+              v-list-item(@click='unassignUser(item.id)')
+                v-list-item-action: v-icon(color='orange') mdi-account-remove-outline
+                v-list-item-content
+                  v-list-item-title Unassign
       template(slot='no-data')
       template(slot='no-data')
         v-alert.ma-3(icon='warning', outlined) No users to display.
         v-alert.ma-3(icon='warning', outlined) No users to display.
     .text-center.py-2(v-if='group.users.length > 15')
     .text-center.py-2(v-if='group.users.length > 15')
@@ -70,10 +65,10 @@ export default {
   data() {
   data() {
     return {
     return {
       headers: [
       headers: [
-        { text: 'ID', value: 'id', width: 50, align: 'right' },
+        { text: 'ID', value: 'id', width: 50 },
         { text: 'Name', value: 'name' },
         { text: 'Name', value: 'name' },
         { text: 'Email', value: 'email' },
         { text: 'Email', value: 'email' },
-        { text: '', value: 'actions', sortable: false, width: 50 }
+        { text: 'Actions', value: 'actions', sortable: false, width: 50 }
       ],
       ],
       searchUserDialog: false,
       searchUserDialog: false,
       pagination: 1,
       pagination: 1,

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

@@ -17,7 +17,7 @@
                 span New Group
                 span New Group
             v-card
             v-card
               .dialog-header.is-short New Group
               .dialog-header.is-short New Group
-              v-card-text
+              v-card-text.pt-5
                 v-text-field.md2(
                 v-text-field.md2(
                   outlined
                   outlined
                   prepend-icon='mdi-account-group'
                   prepend-icon='mdi-account-group'

+ 3 - 3
client/components/admin/admin-pages-edit.vue

@@ -30,11 +30,11 @@
             template(v-slot:activator='{ on }')
             template(v-slot:activator='{ on }')
               v-btn.mx-1.animated.fadeInDown.wait-p1s(color='red', large, outlined, v-on='on')
               v-btn.mx-1.animated.fadeInDown.wait-p1s(color='red', large, outlined, v-on='on')
                 v-icon(color='red') mdi-trash-can-outline
                 v-icon(color='red') mdi-trash-can-outline
-            v-card.wiki-form
+            v-card
               .dialog-header.is-short.is-red
               .dialog-header.is-short.is-red
                 v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
                 v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
                 span {{$t('common:page.delete')}}
                 span {{$t('common:page.delete')}}
-              v-card-text
+              v-card-text.pt-5
                 i18next.body-2(path='common:page.deleteTitle', tag='div')
                 i18next.body-2(path='common:page.deleteTitle', tag='div')
                   span.red--text.text--darken-2(place='title') {{page.title}}
                   span.red--text.text--darken-2(place='title') {{page.title}}
                 .caption {{$t('common:page.deleteSubtitle')}}
                 .caption {{$t('common:page.deleteSubtitle')}}
@@ -44,7 +44,7 @@
                   span.red--text.text--darken-2 /{{page.path}}
                   span.red--text.text--darken-2 /{{page.path}}
               v-card-chin
               v-card-chin
                 v-spacer
                 v-spacer
-                v-btn(flat, @click='deletePageDialog = false', :disabled='loading') {{$t('common:actions.cancel')}}
+                v-btn(text, @click='deletePageDialog = false', :disabled='loading') {{$t('common:actions.cancel')}}
                 v-btn(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}}
                 v-btn(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}}
           v-btn.ml-1.animated.fadeInDown(color='teal', large, outlined, @click='rerenderPage')
           v-btn.ml-1.animated.fadeInDown(color='teal', large, outlined, @click='rerenderPage')
             v-icon(left) mdi-cube-scan
             v-icon(left) mdi-cube-scan

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

@@ -64,7 +64,7 @@
                 td {{ props.item.createdAt | moment('calendar') }}
                 td {{ props.item.createdAt | moment('calendar') }}
                 td {{ props.item.updatedAt | moment('calendar') }}
                 td {{ props.item.updatedAt | moment('calendar') }}
             template(slot='no-data')
             template(slot='no-data')
-              v-alert.ma-3(icon='warning', :value='true', outline) No pages to display.
+              v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display.
           .text-xs-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
           .text-xs-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
             v-pagination(v-model='pagination', :length='pageTotal')
             v-pagination(v-model='pagination', :length='pageTotal')
 </template>
 </template>

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

@@ -26,8 +26,8 @@
               v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
               v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
                 v-list-item-avatar(size='24')
                 v-list-item-avatar(size='24')
                   v-icon(color='grey', v-if='!eng.isAvailable') mdi-minus-box-outline
                   v-icon(color='grey', v-if='!eng.isAvailable') mdi-minus-box-outline
-                  v-icon(color='primary', v-else-if='eng.key === selectedEngine') mdi-checkbox-marked-outline
-                  v-icon(color='grey', v-else) mdi-checkbox-blank-outline
+                  v-icon(color='primary', v-else-if='eng.key === selectedEngine') mdi-checkbox-marked-circle-outline
+                  v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline
                 v-list-item-content
                 v-list-item-content
                   v-list-item-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }}
                   v-list-item-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }}
                   v-list-item-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}
                   v-list-item-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}

+ 10 - 4
client/components/admin/admin-storage.vue

@@ -49,7 +49,7 @@
                     v-icon(color='white') mdi-clock-outline
                     v-icon(color='white') mdi-clock-outline
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title.body-2 {{tgt.title}}
                     v-list-item-title.body-2 {{tgt.title}}
-                    v-list-item-sub-title.purple--text.caption {{tgt.status}}
+                    v-list-item-subtitle.purple--text.caption {{tgt.status}}
                   v-list-item-action
                   v-list-item-action
                     v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
                     v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
                 template(v-else-if='tgt.status === `operational`')
                 template(v-else-if='tgt.status === `operational`')
@@ -57,13 +57,13 @@
                     v-icon(color='white') mdi-check-circle
                     v-icon(color='white') mdi-check-circle
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title.body-2 {{tgt.title}}
                     v-list-item-title.body-2 {{tgt.title}}
-                    v-list-item-sub-title.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
+                    v-list-item-subtitle.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
                 template(v-else)
                 template(v-else)
                   v-list-item-avatar(color='red')
                   v-list-item-avatar(color='red')
                     v-icon(color='white') mdi-close-circle-outline
                     v-icon(color='white') mdi-close-circle-outline
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title.body-2 {{tgt.title}}
                     v-list-item-title.body-2 {{tgt.title}}
-                    v-list-item-sub-title.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
+                    v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
                   v-list-item-action
                   v-list-item-action
                     v-menu
                     v-menu
                       v-btn(slot='activator', icon)
                       v-btn(slot='activator', icon)
@@ -86,6 +86,10 @@
                 img(:src='target.logo', :alt='target.title')
                 img(:src='target.logo', :alt='target.title')
               .body-2.pt-3 {{target.description}}
               .body-2.pt-3 {{target.description}}
               .body-2.pt-3.pb-5: a(:href='target.website') {{target.website}}
               .body-2.pt-3.pb-5: a(:href='target.website') {{target.website}}
+              i18next.body-2(path='admin:storage.targetState', tag='div', v-if='target.isEnabled')
+                v-chip(color='green', small, dark, label, place='state') {{$t('admin:storage.targetStateActive')}}
+              i18next.body-2(path='admin:storage.targetState', tag='div', v-else)
+                v-chip(color='red', small, dark, label, place='state') {{$t('admin:storage.targetStateInactive')}}
               v-divider.mt-3
               v-divider.mt-3
               .overline.my-5 {{$t('admin:storage.targetConfig')}}
               .overline.my-5 {{$t('admin:storage.targetConfig')}}
               .body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}}
               .body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}}
@@ -179,6 +183,8 @@
               template(v-if='target.actions && target.actions.length > 0')
               template(v-if='target.actions && target.actions.length > 0')
                 v-divider.mt-3
                 v-divider.mt-3
                 .overline.my-5 {{$t('admin:storage.actions')}}
                 .overline.my-5 {{$t('admin:storage.actions')}}
+                v-alert(outlined, :value='!target.isEnabled', color='red', icon='mdi-alert')
+                  .body-2 {{$t('admin:storage.actionsInactiveWarn')}}
                 v-container.pt-0(grid-list-xl, fluid)
                 v-container.pt-0(grid-list-xl, fluid)
                   v-layout(row, wrap, fill-height)
                   v-layout(row, wrap, fill-height)
                     v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
                     v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
@@ -190,7 +196,7 @@
                             @click='executeAction(target.key, act.handler)'
                             @click='executeAction(target.key, act.handler)'
                             outlined
                             outlined
                             :color='$vuetify.theme.dark ? `blue` : `primary`'
                             :color='$vuetify.theme.dark ? `blue` : `primary`'
-                            :disabled='runningAction'
+                            :disabled='runningAction || !target.isEnabled'
                             :loading='runningActionHandler === act.handler'
                             :loading='runningActionHandler === act.handler'
                             ) {{$t('admin:storage.actionRun')}}
                             ) {{$t('admin:storage.actionRun')}}
 
 

+ 10 - 10
client/components/admin/admin-system.vue

@@ -13,13 +13,13 @@
               v-btn.animated.fadeInLeft.wait-p2s.btn-animate-rotate(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, @click='refresh'): v-icon(color='grey') mdi-refresh
               v-btn.animated.fadeInLeft.wait-p2s.btn-animate-rotate(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, @click='refresh'): v-icon(color='grey') mdi-refresh
               v-subheader Wiki.js
               v-subheader Wiki.js
               v-list(two-line, dense)
               v-list(two-line, dense)
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-icon.blue.white--text mdi-application-export
                     v-icon.blue.white--text mdi-application-export
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title {{ $t('admin:system.currentVersion') }}
                     v-list-item-title {{ $t('admin:system.currentVersion') }}
                     v-list-item-subtitle {{ info.currentVersion }}
                     v-list-item-subtitle {{ info.currentVersion }}
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-icon.blue.white--text mdi-inbox-arrow-up
                     v-icon.blue.white--text mdi-inbox-arrow-up
                   v-list-item-content
                   v-list-item-content
@@ -31,38 +31,38 @@
               v-divider.mt-3
               v-divider.mt-3
               v-subheader {{ $t('admin:system.hostInfo') }}
               v-subheader {{ $t('admin:system.hostInfo') }}
               v-list(two-line, dense)
               v-list(two-line, dense)
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-avatar.blue-grey(size='40')
                     v-avatar.blue-grey(size='40')
                       v-icon(color='white') {{platformLogo}}
                       v-icon(color='white') {{platformLogo}}
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title {{ $t('admin:system.os') }}
                     v-list-item-title {{ $t('admin:system.os') }}
                     v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}
                     v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-icon.blue-grey.white--text mdi-desktop-classic
                     v-icon.blue-grey.white--text mdi-desktop-classic
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title {{ $t('admin:system.hostname') }}
                     v-list-item-title {{ $t('admin:system.hostname') }}
                     v-list-item-subtitle {{ info.hostname }}
                     v-list-item-subtitle {{ info.hostname }}
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-icon.blue-grey.white--text mdi-cpu-64-bit
                     v-icon.blue-grey.white--text mdi-cpu-64-bit
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title {{ $t('admin:system.cpuCores') }}
                     v-list-item-title {{ $t('admin:system.cpuCores') }}
                     v-list-item-subtitle {{ info.cpuCores }}
                     v-list-item-subtitle {{ info.cpuCores }}
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-icon.blue-grey.white--text mdi-memory
                     v-icon.blue-grey.white--text mdi-memory
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title {{ $t('admin:system.totalRAM') }}
                     v-list-item-title {{ $t('admin:system.totalRAM') }}
                     v-list-item-subtitle {{ info.ramTotal }}
                     v-list-item-subtitle {{ info.ramTotal }}
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-icon.blue-grey.white--text mdi-iframe-outline
                     v-icon.blue-grey.white--text mdi-iframe-outline
                   v-list-item-content
                   v-list-item-content
                     v-list-item-title {{ $t('admin:system.workingDirectory') }}
                     v-list-item-title {{ $t('admin:system.workingDirectory') }}
                     v-list-item-subtitle {{ info.workingDirectory }}
                     v-list-item-subtitle {{ info.workingDirectory }}
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline
                     v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline
                   v-list-item-content
                   v-list-item-content
@@ -73,7 +73,7 @@
             v-card.pb-3.animated.fadeInUp.wait-p4s
             v-card.pb-3.animated.fadeInUp.wait-p4s
               v-subheader Node.js
               v-subheader Node.js
               v-list(dense)
               v-list(dense)
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-avatar.light-green(size='40')
                     v-avatar.light-green(size='40')
                       v-icon(color='white') mdi-nodejs
                       v-icon(color='white') mdi-nodejs
@@ -83,7 +83,7 @@
               v-divider.mt-3
               v-divider.mt-3
               v-subheader {{ info.dbType }}
               v-subheader {{ info.dbType }}
               v-list(dense)
               v-list(dense)
-                v-list-item(avatar)
+                v-list-item
                   v-list-item-avatar
                   v-list-item-avatar
                     v-avatar.indigo.darken-1(size='40')
                     v-avatar.indigo.darken-1(size='40')
                       v-icon(color='white') mdi-database
                       v-icon(color='white') mdi-database

+ 2 - 1
client/components/admin/admin-users-create.vue

@@ -8,7 +8,7 @@
         v-btn.mx-0(color='white', outlined, disabled, dark)
         v-btn.mx-0(color='white', outlined, disabled, dark)
           v-icon(left) mdi-database-import
           v-icon(left) mdi-database-import
           span Bulk Import
           span Bulk Import
-      v-card-text
+      v-card-text.pt-5
         v-select(
         v-select(
           :items='providers'
           :items='providers'
           item-text='title'
           item-text='title'
@@ -89,6 +89,7 @@
           label='Send a welcome email'
           label='Send a welcome email'
           hide-details
           hide-details
           v-model='sendWelcomeEmail'
           v-model='sendWelcomeEmail'
+          disabled
         )
         )
       v-card-chin
       v-card-chin
         v-spacer
         v-spacer

+ 59 - 38
client/components/admin/admin-users-edit.vue

@@ -3,12 +3,26 @@
     v-layout(row, wrap)
     v-layout(row, wrap)
       v-flex(xs12)
       v-flex(xs12)
         .admin-header
         .admin-header
-          img.animated.fadeInUp(src='/svg/icon-male-user.svg', alt='Edit User', style='width: 80px;')
+          img.animated.fadeInUp(src='/svg/icon-male-user.svg', :alt='$t(`admin:users.edit`)', style='width: 80px;')
           .admin-header-title
           .admin-header-title
-            .headline.blue--text.text--darken-2.animated.fadeInLeft Edit User
+            .headline.blue--text.text--darken-2.animated.fadeInLeft {{$t('admin:users.edit')}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{user.name}}
             .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{user.name}}
           v-spacer
           v-spacer
-          .caption.grey--text.animated.fadeInRight.wait-p5s ID #[strong {{user.id}}]
+          template(v-if='user.isActive')
+            status-indicator.mr-3(positive, pulse)
+            .caption.green--text {{$t('admin:users.active')}}
+          template(v-else)
+            status-indicator.mr-3(negative, pulse)
+            .caption.red--text {{$t('admin:users.inactive')}}
+          template(v-if='user.isVerified')
+            status-indicator.mr-3.ml-4(active, pulse)
+            .caption.blue--text {{$t('admin:users.verified')}}
+          template(v-else)
+            status-indicator.mr-3.ml-4(intermediary, pulse)
+            .caption.deep-orange--text {{$t('admin:users.unverified')}}
+          v-spacer
+          i18next.caption.grey--text.animated.fadeInRight.wait-p5s(path='admin:users.id', tag='div')
+            strong(place='id') {{user.id}}
           v-divider.animated.fadeInRight.wait-p3s.ml-3(vertical)
           v-divider.animated.fadeInRight.wait-p3s.ml-3(vertical)
           v-btn.ml-3.animated.fadeInDown.wait-p2s(color='grey', large, outlined, to='/users')
           v-btn.ml-3.animated.fadeInDown.wait-p2s(color='grey', large, outlined, to='/users')
             v-icon mdi-arrow-left
             v-icon mdi-arrow-left
@@ -30,15 +44,15 @@
         v-card.animated.fadeInUp
         v-card.animated.fadeInUp
           v-toolbar(color='primary', dense, dark, flat)
           v-toolbar(color='primary', dense, dark, flat)
             v-icon.mr-2 mdi-information-variant
             v-icon.mr-2 mdi-information-variant
-            span Basic Info
+            span {{$t('admin:users.basicInfo')}}
           v-list.py-0(two-line, dense)
           v-list.py-0(two-line, dense)
             v-list-item
             v-list-item
               v-list-item-avatar(size='32')
               v-list-item-avatar(size='32')
                 v-icon mdi-email-variant
                 v-icon mdi-email-variant
               v-list-item-content
               v-list-item-content
-                v-list-item-title Email
+                v-list-item-title {{$t('admin:users.email')}}
                 v-list-item-subtitle {{ user.email }}
                 v-list-item-subtitle {{ user.email }}
-              v-list-item-action(v-if='!user.isSystem')
+              v-list-item-action(v-if='!user.isSystem && user.providerKey === `local`')
                 v-menu(
                 v-menu(
                   v-model='editPop.email'
                   v-model='editPop.email'
                   :close-on-content-click='false'
                   :close-on-content-click='false'
@@ -52,7 +66,7 @@
                     v-text-field(
                     v-text-field(
                       ref='iptEmail'
                       ref='iptEmail'
                       v-model='user.email'
                       v-model='user.email'
-                      label='Email'
+                      :label='$t(`admin:users.email`)'
                       solo
                       solo
                       hide-details
                       hide-details
                       append-icon='mdi-check'
                       append-icon='mdi-check'
@@ -66,7 +80,7 @@
               v-list-item-avatar(size='32')
               v-list-item-avatar(size='32')
                 v-icon mdi-account
                 v-icon mdi-account
               v-list-item-content
               v-list-item-content
-                v-list-item-title Display Name
+                v-list-item-title {{$t('admin:users.displayName')}}
                 v-list-item-subtitle {{ user.name }}
                 v-list-item-subtitle {{ user.name }}
               v-list-item-action
               v-list-item-action
                 v-menu(
                 v-menu(
@@ -82,7 +96,7 @@
                     v-text-field(
                     v-text-field(
                       ref='iptDisplayName'
                       ref='iptDisplayName'
                       v-model='user.name'
                       v-model='user.name'
-                      label='Display Name'
+                      :label='$t(`admin:users.displayName`)'
                       solo
                       solo
                       hide-details
                       hide-details
                       append-icon='mdi-check'
                       append-icon='mdi-check'
@@ -94,13 +108,13 @@
         v-card.mt-3.animated.fadeInUp.wait-p2s(v-if='!user.isSystem')
         v-card.mt-3.animated.fadeInUp.wait-p2s(v-if='!user.isSystem')
           v-toolbar(color='primary', dense, dark, flat)
           v-toolbar(color='primary', dense, dark, flat)
             v-icon.mr-2 mdi-lock-outline
             v-icon.mr-2 mdi-lock-outline
-            span Authentication
+            span {{$t('admin:users.authentication')}}
           v-list.py-0(two-line, dense)
           v-list.py-0(two-line, dense)
             v-list-item
             v-list-item
               v-list-item-avatar(size='32')
               v-list-item-avatar(size='32')
                 v-icon mdi-domain
                 v-icon mdi-domain
               v-list-item-content
               v-list-item-content
-                v-list-item-title Provider
+                v-list-item-title {{$t('admin:users.authProvider')}}
                 v-list-item-subtitle {{ user.providerKey }}
                 v-list-item-subtitle {{ user.providerKey }}
               //- v-list-item-action
               //- v-list-item-action
               //-   v-img(src='https://static.requarks.io/logo/wikijs.svg', alt='', contain, max-height='32', position='center right')
               //-   v-img(src='https://static.requarks.io/logo/wikijs.svg', alt='', contain, max-height='32', position='center right')
@@ -110,7 +124,7 @@
                 v-list-item-avatar(size='32')
                 v-list-item-avatar(size='32')
                   v-icon mdi-textbox-password
                   v-icon mdi-textbox-password
                 v-list-item-content
                 v-list-item-content
-                  v-list-item-title Password
+                  v-list-item-title {{$t('admin:users.password')}}
                   v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
                   v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
                 v-list-item-action
                 v-list-item-action
                   v-menu(
                   v-menu(
@@ -124,12 +138,12 @@
                         template(v-slot:activator='{ on: tooltip }')
                         template(v-slot:activator='{ on: tooltip }')
                           v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)')
                           v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)')
                             v-icon mdi-cached
                             v-icon mdi-cached
-                        span Change Password
+                        span {{$t('admin:users.changePassword')}}
                     v-card
                     v-card
                       v-text-field(
                       v-text-field(
                         ref='iptNewPassword'
                         ref='iptNewPassword'
                         v-model='newPassword'
                         v-model='newPassword'
-                        label='New Password'
+                        :label='$t(`admin:users.newPassword`)'
                         solo
                         solo
                         hide-details
                         hide-details
                         append-icon='mdi-check'
                         append-icon='mdi-check'
@@ -149,26 +163,26 @@
                 v-list-item-avatar(size='32')
                 v-list-item-avatar(size='32')
                   v-icon mdi-two-factor-authentication
                   v-icon mdi-two-factor-authentication
                 v-list-item-content
                 v-list-item-content
-                  v-list-item-title Two Factor Authentication (2FA)
+                  v-list-item-title {{$t('admin:users.tfa')}}
                   v-list-item-subtitle.red--text Inactive
                   v-list-item-subtitle.red--text Inactive
                 v-list-item-action
                 v-list-item-action
                   v-tooltip(top)
                   v-tooltip(top)
                     template(v-slot:activator='{ on }')
                     template(v-slot:activator='{ on }')
                       v-btn(icon, color='grey', x-small, v-on='on', disabled)
                       v-btn(icon, color='grey', x-small, v-on='on', disabled)
                         v-icon mdi-power
                         v-icon mdi-power
-                    span Toggle 2FA
-              template(v-if='user.providerId')
-                v-divider
-                v-list-item
-                  v-list-item-avatar(size='32')
-                    v-icon mdi-account
-                  v-list-item-content
-                    v-list-item-title Provider Id
-                    v-list-item-subtitle {{ user.providerId }}
+                    span {{$t('admin:users.toggle2FA')}}
+            template(v-if='user.providerId')
+              v-divider
+              v-list-item
+                v-list-item-avatar(size='32')
+                  v-icon mdi-music-accidental-sharp
+                v-list-item-content
+                  v-list-item-title {{$t('admin:users.authProviderId')}}
+                  v-list-item-subtitle {{ user.providerId }}
         v-card.mt-3.animated.fadeInUp.wait-p4s
         v-card.mt-3.animated.fadeInUp.wait-p4s
           v-toolbar(color='primary', dense, dark, flat)
           v-toolbar(color='primary', dense, dark, flat)
             v-icon.mr-2 mdi-account-group
             v-icon.mr-2 mdi-account-group
-            span User Groups
+            span {{$t('admin:users.groups')}}
           v-list(dense)
           v-list(dense)
             template(v-for='(group, idx) in user.groups')
             template(v-for='(group, idx) in user.groups')
               v-list-item(:key='`group-` + group.id')
               v-list-item(:key='`group-` + group.id')
@@ -181,14 +195,14 @@
                     v-icon mdi-close
                     v-icon mdi-close
               v-divider(v-if='idx < user.groups.length - 1')
               v-divider(v-if='idx < user.groups.length - 1')
           v-alert.mx-3(v-if='user.groups.length < 1', outlined, color='grey darken-1', icon='mdi-alert')
           v-alert.mx-3(v-if='user.groups.length < 1', outlined, color='grey darken-1', icon='mdi-alert')
-            .caption This user is not assigned to any group yet. You must assign at least 1 group to a user.
+            .caption {{$t('admin:users.noGroupAssigned')}}
           v-card-chin(v-if='!user.isSystem')
           v-card-chin(v-if='!user.isSystem')
             v-spacer
             v-spacer
             v-select(
             v-select(
               ref='iptAssignGroup'
               ref='iptAssignGroup'
               :items='groups'
               :items='groups'
               v-model='newGroup'
               v-model='newGroup'
-              label='Select Group...'
+              :label='$t(`admin:users.selectGroup`)'
               item-value='id'
               item-value='id'
               item-text='name'
               item-text='name'
               item-disabled='isSystem'
               item-disabled='isSystem'
@@ -201,18 +215,18 @@
             )
             )
             v-btn.ml-2.px-4(depressed, color='primary', height='48', @click='assignGroup', :disabled='newGroup === 0')
             v-btn.ml-2.px-4(depressed, color='primary', height='48', @click='assignGroup', :disabled='newGroup === 0')
               v-icon(left) mdi-clipboard-account-outline
               v-icon(left) mdi-clipboard-account-outline
-              span Assign
+              span {{$t('admin:users.groupAssign')}}
       v-flex(xs6)
       v-flex(xs6)
         v-card.animated.fadeInUp.wait-p2s
         v-card.animated.fadeInUp.wait-p2s
           v-toolbar(color='primary', dense, dark, flat)
           v-toolbar(color='primary', dense, dark, flat)
             v-icon.mr-2 mdi-account-badge-outline
             v-icon.mr-2 mdi-account-badge-outline
-            span Extended Metadata
+            span {{$t('admin:users.extendedMetadata')}}
           v-list.py-0(two-line, dense)
           v-list.py-0(two-line, dense)
             v-list-item
             v-list-item
               v-list-item-avatar(size='32')
               v-list-item-avatar(size='32')
                 v-icon mdi-map-marker
                 v-icon mdi-map-marker
               v-list-item-content
               v-list-item-content
-                v-list-item-title Location
+                v-list-item-title {{$t('admin:users.location')}}
                 v-list-item-subtitle {{ user.location }}
                 v-list-item-subtitle {{ user.location }}
               v-list-item-action
               v-list-item-action
                 v-menu(
                 v-menu(
@@ -228,7 +242,7 @@
                     v-text-field(
                     v-text-field(
                       ref='iptLocation'
                       ref='iptLocation'
                       v-model='user.location'
                       v-model='user.location'
-                      label='Location'
+                      :label='$t(`admin:users.location`)'
                       solo
                       solo
                       hide-details
                       hide-details
                       append-icon='mdi-check'
                       append-icon='mdi-check'
@@ -241,7 +255,7 @@
               v-list-item-avatar(size='32')
               v-list-item-avatar(size='32')
                 v-icon mdi-account-badge-horizontal-outline
                 v-icon mdi-account-badge-horizontal-outline
               v-list-item-content
               v-list-item-content
-                v-list-item-title Job Title
+                v-list-item-title {{$t('admin:users.jobTitle')}}
                 v-list-item-subtitle {{ user.jobTitle }}
                 v-list-item-subtitle {{ user.jobTitle }}
               v-list-item-action
               v-list-item-action
                 v-menu(
                 v-menu(
@@ -257,7 +271,7 @@
                     v-text-field(
                     v-text-field(
                       ref='iptJobTitle'
                       ref='iptJobTitle'
                       v-model='user.jobTitle'
                       v-model='user.jobTitle'
-                      label='Job Title'
+                      :label='$t(`admin:users.jobTitle`)'
                       solo
                       solo
                       hide-details
                       hide-details
                       append-icon='mdi-check'
                       append-icon='mdi-check'
@@ -270,7 +284,7 @@
               v-list-item-avatar(size='32')
               v-list-item-avatar(size='32')
                 v-icon mdi-map-clock-outline
                 v-icon mdi-map-clock-outline
               v-list-item-content
               v-list-item-content
-                v-list-item-title Timezone
+                v-list-item-title {{$t('admin:users.timezone')}}
                 v-list-item-subtitle {{ user.timezone }}
                 v-list-item-subtitle {{ user.timezone }}
               v-list-item-action
               v-list-item-action
                 v-menu(
                 v-menu(
@@ -287,7 +301,7 @@
                       ref='iptTimezone'
                       ref='iptTimezone'
                       :items='timezones'
                       :items='timezones'
                       v-model='user.timezone'
                       v-model='user.timezone'
-                      label='Timezone'
+                      :label='$t(`admin:users.timezone`)'
                       solo
                       solo
                       dense
                       dense
                       hide-details
                       hide-details
@@ -308,11 +322,16 @@
 import _ from 'lodash'
 import _ from 'lodash'
 import { get } from 'vuex-pathify'
 import { get } from 'vuex-pathify'
 
 
+import { StatusIndicator } from 'vue-status-indicator'
+
 import userQuery from 'gql/admin/users/users-query-single.gql'
 import userQuery from 'gql/admin/users/users-query-single.gql'
 import groupsQuery from 'gql/admin/users/users-query-groups.gql'
 import groupsQuery from 'gql/admin/users/users-query-groups.gql'
 import updateUserMutation from 'gql/admin/users/users-mutation-update.gql'
 import updateUserMutation from 'gql/admin/users/users-mutation-update.gql'
 
 
 export default {
 export default {
+  components: {
+    StatusIndicator
+  },
   data() {
   data() {
     return {
     return {
       deleteUserDialog: false,
       deleteUserDialog: false,
@@ -334,7 +353,9 @@ export default {
         location: '',
         location: '',
         jobTitle: '',
         jobTitle: '',
         timezone: '',
         timezone: '',
-        groups: []
+        groups: [],
+        isActive: false,
+        isVerified: false
       },
       },
       timezones: [
       timezones: [
         { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
         { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
@@ -613,7 +634,7 @@ export default {
       if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) {
       if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) {
         this.$store.commit('showNotification', {
         this.$store.commit('showNotification', {
           style: 'success',
           style: 'success',
-          message: 'User updated successfully.',
+          message: this.$t('admin:users.userUpdateSuccess'),
           icon: 'check'
           icon: 'check'
         })
         })
         this.$router.push('/users')
         this.$router.push('/users')
@@ -636,7 +657,7 @@ export default {
     assignGroup() {
     assignGroup() {
       if (_.some(this.user.groups, ['id', this.newGroup])) {
       if (_.some(this.user.groups, ['id', this.newGroup])) {
         this.$store.commit('showNotification', {
         this.$store.commit('showNotification', {
-          message: 'User is already assigned to this group!',
+          message: this.$t('admin:users.userAlreadyAssignedToGroup'),
           style: 'error',
           style: 'error',
           icon: 'alert'
           icon: 'alert'
         })
         })

+ 1 - 1
client/components/common/page-delete.vue

@@ -4,7 +4,7 @@
       .dialog-header.is-short.is-red
       .dialog-header.is-short.is-red
         v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
         v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
         span {{$t('common:page.delete')}}
         span {{$t('common:page.delete')}}
-      v-card-text
+      v-card-text.pt-5
         i18next.body-1(path='common:page.deleteTitle', tag='div')
         i18next.body-1(path='common:page.deleteTitle', tag='div')
           span.red--text.text--darken-2(place='title') {{pageTitle}}
           span.red--text.text--darken-2(place='title') {{pageTitle}}
         .caption {{$t('common:page.deleteSubtitle')}}
         .caption {{$t('common:page.deleteSubtitle')}}

+ 3 - 3
client/components/common/user-search.vue

@@ -3,7 +3,7 @@
     v-model='dialogOpen'
     v-model='dialogOpen'
     max-width='650'
     max-width='650'
     )
     )
-    v-card.wiki-form
+    v-card
       .dialog-header
       .dialog-header
         span {{$t('common:user.search')}}
         span {{$t('common:user.search')}}
         v-spacer
         v-spacer
@@ -14,7 +14,7 @@
           :width='2'
           :width='2'
           v-show='searchLoading'
           v-show='searchLoading'
           )
           )
-      v-card-text
+      v-card-text.pt-5
         v-text-field(
         v-text-field(
           outlined
           outlined
           :label='$t(`common:user.searchPlaceholder`)'
           :label='$t(`common:user.searchPlaceholder`)'
@@ -56,7 +56,7 @@ import searchUsersQuery from 'gql/common/common-users-query-search.gql'
 export default {
 export default {
   filters: {
   filters: {
     initials(val) {
     initials(val) {
-      return val.split(' ').map(v => v.substring(0, 1)).join()
+      return val.split(' ').map(v => v.substring(0, 1)).join('')
     }
     }
   },
   },
   props: {
   props: {

+ 126 - 46
client/components/login.vue

@@ -11,17 +11,18 @@
             offset-xl4, xl4
             offset-xl4, xl4
             )
             )
             transition(name='fadeUp')
             transition(name='fadeUp')
-              v-card.elevation-5.md2(v-show='isShown')
+              v-card.elevation-5(v-show='isShown')
                 v-toolbar(color='primary', flat, dense, dark)
                 v-toolbar(color='primary', flat, dense, dark)
                   v-spacer
                   v-spacer
                   .subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
                   .subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
+                  .subheading(v-if='screen === "changePwd"') {{ $t('auth:changePwd.subtitle') }}
                   .subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title, interpolation: { escapeValue: false } }) }}
                   .subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title, interpolation: { escapeValue: false } }) }}
                   .subheading(v-else) {{ $t('auth:loginRequired') }}
                   .subheading(v-else) {{ $t('auth:loginRequired') }}
                   v-spacer
                   v-spacer
                 v-card-text.text-center
                 v-card-text.text-center
                   h1.display-1.primary--text.py-2 {{ siteTitle }}
                   h1.display-1.primary--text.py-2 {{ siteTitle }}
                   template(v-if='screen === "login"')
                   template(v-if='screen === "login"')
-                    v-text-field.md2.mt-3(
+                    v-text-field.mt-3(
                       solo
                       solo
                       flat
                       flat
                       prepend-icon='mdi-clipboard-account'
                       prepend-icon='mdi-clipboard-account'
@@ -31,7 +32,7 @@
                       v-model='username'
                       v-model='username'
                       :placeholder='$t("auth:fields.emailUser")'
                       :placeholder='$t("auth:fields.emailUser")'
                       )
                       )
-                    v-text-field.md2.mt-2(
+                    v-text-field.mt-2(
                       solo
                       solo
                       flat
                       flat
                       prepend-icon='mdi-textbox-password'
                       prepend-icon='mdi-textbox-password'
@@ -47,7 +48,7 @@
                     )
                     )
                   template(v-else-if='screen === "tfa"')
                   template(v-else-if='screen === "tfa"')
                     .body-2 Enter the security code generated from your trusted device:
                     .body-2 Enter the security code generated from your trusted device:
-                    v-text-field.md2.centered.mt-2(
+                    v-text-field.centered.mt-2(
                       solo
                       solo
                       flat
                       flat
                       background-color='grey lighten-4'
                       background-color='grey lighten-4'
@@ -57,12 +58,34 @@
                       :placeholder='$t("auth:tfa.placeholder")'
                       :placeholder='$t("auth:tfa.placeholder")'
                       @keyup.enter='verifySecurityCode'
                       @keyup.enter='verifySecurityCode'
                     )
                     )
+                  template(v-else-if='screen === "changePwd"')
+                    .body-2 {{$t('auth:changePwd.instructions')}}
+                    v-text-field.mt-2(
+                      type='password'
+                      solo
+                      flat
+                      background-color='grey lighten-4'
+                      hide-details
+                      ref='iptNewPassword'
+                      v-model='newPassword'
+                      :placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'
+                    )
+                    v-text-field.mt-2(
+                      type='password'
+                      solo
+                      flat
+                      background-color='grey lighten-4'
+                      hide-details
+                      v-model='newPasswordVerify'
+                      :placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'
+                      @keyup.enter='changePassword'
+                    )
                   template(v-else-if='screen === "forgot"')
                   template(v-else-if='screen === "forgot"')
                     .body-2 {{ $t('auth:forgotPasswordSubtitle') }}
                     .body-2 {{ $t('auth:forgotPasswordSubtitle') }}
-                    v-text-field.md2.mt-3(
+                    v-text-field.mt-3(
                       solo
                       solo
                       flat
                       flat
-                      prepend-icon='email'
+                      prepend-icon='mdi-email'
                       background-color='grey lighten-4'
                       background-color='grey lighten-4'
                       hide-details
                       hide-details
                       ref='iptEmailForgot'
                       ref='iptEmailForgot'
@@ -71,31 +94,48 @@
                       )
                       )
                 v-card-actions.pb-4
                 v-card-actions.pb-4
                   v-spacer
                   v-spacer
-                  v-btn.md2(
+                  v-btn(
+                    width='100%'
+                    max-width='250px'
                     v-if='screen === "login"'
                     v-if='screen === "login"'
-                    block
                     large
                     large
-                    color='primary'
+                    color='teal'
+                    dark
                     @click='login'
                     @click='login'
-                    round
+                    rounded
                     :loading='isLoading'
                     :loading='isLoading'
                     ) {{ $t('auth:actions.login') }}
                     ) {{ $t('auth:actions.login') }}
-                  v-btn.md2(
+                  v-btn(
+                    width='100%'
+                    max-width='250px'
                     v-else-if='screen === "tfa"'
                     v-else-if='screen === "tfa"'
-                    block
                     large
                     large
-                    color='primary'
+                    color='teal'
+                    dark
                     @click='verifySecurityCode'
                     @click='verifySecurityCode'
-                    round
+                    rounded
                     :loading='isLoading'
                     :loading='isLoading'
                     ) {{ $t('auth:tfa.verifyToken') }}
                     ) {{ $t('auth:tfa.verifyToken') }}
-                  v-btn.md2(
+                  v-btn(
+                    width='100%'
+                    max-width='250px'
+                    v-else-if='screen === "changePwd"'
+                    large
+                    color='teal'
+                    dark
+                    @click='changePassword'
+                    rounded
+                    :loading='isLoading'
+                    ) {{ $t('auth:changePwd.proceed') }}
+                  v-btn(
+                    width='100%'
+                    max-width='250px'
                     v-else-if='screen === "forgot"'
                     v-else-if='screen === "forgot"'
-                    block
                     large
                     large
-                    color='primary'
+                    color='teal'
+                    dark
                     @click='forgotPasswordSubmit'
                     @click='forgotPasswordSubmit'
-                    round
+                    rounded
                     :loading='isLoading'
                     :loading='isLoading'
                     ) {{ $t('auth:sendResetPassword') }}
                     ) {{ $t('auth:sendResetPassword') }}
                   v-spacer
                   v-spacer
@@ -111,15 +151,16 @@
                   v-divider
                   v-divider
                   v-card-text.grey.lighten-4.text-center
                   v-card-text.grey.lighten-4.text-center
                     .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
                     .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
-                    v-tooltip(top, v-for='strategy in strategies', :key='strategy.key')
-                      .social-login-btn.mr-2(
-                        slot='activator'
-                        v-ripple
-                        v-html='strategy.icon'
-                        :class='strategy.color + " elevation-" + (strategy.key === selectedStrategy.key ? "0" : "4")'
-                        @click='selectStrategy(strategy)'
-                        )
-                      span {{ strategy.title }}
+                    v-btn.mx-1.social-login-btn(
+                      v-for='strategy in strategies', :key='strategy.key'
+                      large
+                      @click='selectStrategy(strategy)'
+                      dark
+                      :color='strategy.color'
+                      :depressed='strategy.key === selectedStrategy.key'
+                      )
+                      v-avatar.mr-3(tile, :class='strategy.color', size='24', v-html='strategy.icon')
+                      span(style='text-transform: none;') {{ strategy.title }}
                 template(v-if='screen === "login" && selectedStrategy.selfRegistration')
                 template(v-if='screen === "login" && selectedStrategy.selfRegistration')
                   v-divider
                   v-divider
                   v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
                   v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
@@ -142,6 +183,7 @@ import Cookies from 'js-cookie'
 import strategiesQuery from 'gql/login/login-query-strategies.gql'
 import strategiesQuery from 'gql/login/login-query-strategies.gql'
 import loginMutation from 'gql/login/login-mutation-login.gql'
 import loginMutation from 'gql/login/login-mutation-login.gql'
 import tfaMutation from 'gql/login/login-mutation-tfa.gql'
 import tfaMutation from 'gql/login/login-mutation-tfa.gql'
+import changePasswordMutation from 'gql/login/login-mutation-changepassword.gql'
 
 
 export default {
 export default {
   i18nOptions: { namespaces: 'auth' },
   i18nOptions: { namespaces: 'auth' },
@@ -155,11 +197,13 @@ export default {
       password: '',
       password: '',
       hidePassword: true,
       hidePassword: true,
       securityCode: '',
       securityCode: '',
-      loginToken: '',
+      continuationToken: '',
       isLoading: false,
       isLoading: false,
       loaderColor: 'grey darken-4',
       loaderColor: 'grey darken-4',
       loaderTitle: 'Working...',
       loaderTitle: 'Working...',
-      isShown: false
+      isShown: false,
+      newPassword: '',
+      newPasswordVerify: ''
     }
     }
   },
   },
   computed: {
   computed: {
@@ -205,14 +249,14 @@ export default {
         this.$store.commit('showNotification', {
         this.$store.commit('showNotification', {
           style: 'red',
           style: 'red',
           message: this.$t('auth:invalidEmailUsername'),
           message: this.$t('auth:invalidEmailUsername'),
-          icon: 'warning'
+          icon: 'alert'
         })
         })
         this.$refs.iptEmail.focus()
         this.$refs.iptEmail.focus()
       } else if (this.password.length < 2) {
       } else if (this.password.length < 2) {
         this.$store.commit('showNotification', {
         this.$store.commit('showNotification', {
           style: 'red',
           style: 'red',
           message: this.$t('auth:invalidPassword'),
           message: this.$t('auth:invalidPassword'),
-          icon: 'warning'
+          icon: 'alert'
         })
         })
         this.$refs.iptPassword.focus()
         this.$refs.iptPassword.focus()
       } else {
       } else {
@@ -231,10 +275,16 @@ export default {
           if (_.has(resp, 'data.authentication.login')) {
           if (_.has(resp, 'data.authentication.login')) {
             let respObj = _.get(resp, 'data.authentication.login', {})
             let respObj = _.get(resp, 'data.authentication.login', {})
             if (respObj.responseResult.succeeded === true) {
             if (respObj.responseResult.succeeded === true) {
-              if (respObj.tfaRequired === true) {
+              this.continuationToken = respObj.continuationToken
+              if (respObj.mustChangePwd === true) {
+                this.screen = 'changePwd'
+                this.$nextTick(() => {
+                  this.$refs.iptNewPassword.focus()
+                })
+                this.isLoading = false
+              } else if (respObj.mustProvideTFA === true) {
                 this.screen = 'tfa'
                 this.screen = 'tfa'
                 this.securityCode = ''
                 this.securityCode = ''
-                this.loginToken = respObj.tfaLoginToken
                 this.$nextTick(() => {
                 this.$nextTick(() => {
                   this.$refs.iptTFA.focus()
                   this.$refs.iptTFA.focus()
                 })
                 })
@@ -258,7 +308,7 @@ export default {
           this.$store.commit('showNotification', {
           this.$store.commit('showNotification', {
             style: 'red',
             style: 'red',
             message: err.message,
             message: err.message,
-            icon: 'warning'
+            icon: 'alert'
           })
           })
           this.isLoading = false
           this.isLoading = false
         }
         }
@@ -280,7 +330,7 @@ export default {
         this.$apollo.mutate({
         this.$apollo.mutate({
           mutation: tfaMutation,
           mutation: tfaMutation,
           variables: {
           variables: {
-            loginToken: this.loginToken,
+            continuationToken: this.continuationToken,
             securityCode: this.securityCode
             securityCode: this.securityCode
           }
           }
         }).then(resp => {
         }).then(resp => {
@@ -307,23 +357,59 @@ export default {
           this.$store.commit('showNotification', {
           this.$store.commit('showNotification', {
             style: 'red',
             style: 'red',
             message: err.message,
             message: err.message,
-            icon: 'warning'
+            icon: 'alert'
           })
           })
           this.isLoading = false
           this.isLoading = false
         })
         })
       }
       }
     },
     },
-    forgotPassword() {
+    /**
+     * CHANGE PASSWORD
+     */
+    async changePassword () {
+      this.loaderColor = 'grey darken-4'
+      this.loaderTitle = this.$t('auth:changePwd.loading')
+      this.isLoading = true
+      const resp = await this.$apollo.mutate({
+        mutation: changePasswordMutation,
+        variables: {
+          continuationToken: this.continuationToken,
+          newPassword: this.newPassword
+        }
+      })
+      if (_.get(resp, 'data.authentication.loginChangePassword.responseResult.succeeded', false) === true) {
+        this.loaderColor = 'green darken-1'
+        this.loaderTitle = this.$t('auth:loginSuccess')
+        Cookies.set('jwt', _.get(resp, 'data.authentication.loginChangePassword.jwt', ''), { expires: 365 })
+        _.delay(() => {
+          window.location.replace('/') // TEMPORARY - USE RETURNURL
+        }, 1000)
+      } else {
+        this.$store.commit('showNotification', {
+          style: 'red',
+          message: _.get(resp, 'data.authentication.loginChangePassword.responseResult.message', false),
+          icon: 'alert'
+        })
+        this.isLoading = false
+      }
+    },
+    /**
+     * SWITCH TO FORGOT PASSWORD SCREEN
+     */
+    forgotPassword () {
       this.screen = 'forgot'
       this.screen = 'forgot'
       this.$nextTick(() => {
       this.$nextTick(() => {
         this.$refs.iptEmailForgot.focus()
         this.$refs.iptEmailForgot.focus()
       })
       })
     },
     },
-    async forgotPasswordSubmit() {
+    /**
+     * FORGOT PASSWORD SUBMIT
+     */
+    async forgotPasswordSubmit () {
       this.$store.commit('showNotification', {
       this.$store.commit('showNotification', {
         style: 'pink',
         style: 'pink',
         message: 'Coming soon!',
         message: 'Coming soon!',
-        icon: 'free_breakfast'
+        icon: 'ferry'
       })
       })
     }
     }
   },
   },
@@ -378,18 +464,12 @@ export default {
     }
     }
 
 
     .social-login-btn {
     .social-login-btn {
-      display: inline-flex;
-      justify-content: center;
-      align-items: center;
-      border-radius: 50%;
-      width: 54px;
-      height: 54px;
       cursor: pointer;
       cursor: pointer;
       transition: opacity .2s ease;
       transition: opacity .2s ease;
       &:hover {
       &:hover {
         opacity: .8;
         opacity: .8;
       }
       }
-      margin: .5rem 0;
+      margin: .25rem 0;
       svg {
       svg {
         width: 24px;
         width: 24px;
         height: 24px;
         height: 24px;

+ 13 - 1
client/graph/admin/site/site-mutation-save-config.gql

@@ -11,6 +11,12 @@ mutation (
   $featurePageRatings: Boolean!
   $featurePageRatings: Boolean!
   $featurePageComments: Boolean!
   $featurePageComments: Boolean!
   $featurePersonalWikis: Boolean!
   $featurePersonalWikis: Boolean!
+  $securityIframe: Boolean!
+  $securityReferrerPolicy: Boolean!
+  $securityHSTS: Boolean!
+  $securityHSTSDuration: Int!
+  $securityCSP: Boolean!
+  $securityCSPDirectives: String!
 ) {
 ) {
   site {
   site {
     updateConfig(
     updateConfig(
@@ -25,7 +31,13 @@ mutation (
       logoIsSquare: $logoIsSquare,
       logoIsSquare: $logoIsSquare,
       featurePageRatings: $featurePageRatings,
       featurePageRatings: $featurePageRatings,
       featurePageComments: $featurePageComments,
       featurePageComments: $featurePageComments,
-      featurePersonalWikis: $featurePersonalWikis
+      featurePersonalWikis: $featurePersonalWikis,
+      securityIframe: $securityIframe,
+      securityReferrerPolicy: $securityReferrerPolicy,
+      securityHSTS: $securityHSTS,
+      securityHSTSDuration: $securityHSTSDuration,
+      securityCSP: $securityCSP,
+      securityCSPDirectives: $securityCSPDirectives
     ) {
     ) {
       responseResult {
       responseResult {
         succeeded
         succeeded

+ 6 - 0
client/graph/admin/site/site-query-config.gql

@@ -13,6 +13,12 @@
       featurePageRatings
       featurePageRatings
       featurePageComments
       featurePageComments
       featurePersonalWikis
       featurePersonalWikis
+      securityIframe
+      securityReferrerPolicy
+      securityHSTS
+      securityHSTSDuration
+      securityCSP
+      securityCSPDirectives
     }
     }
   }
   }
 }
 }

+ 2 - 0
client/graph/admin/users/users-query-single.gql

@@ -10,6 +10,8 @@ query ($id: Int!) {
       jobTitle
       jobTitle
       timezone
       timezone
       isSystem
       isSystem
+      isActive
+      isVerified
       createdAt
       createdAt
       updatedAt
       updatedAt
       groups {
       groups {

+ 13 - 0
client/graph/login/login-mutation-changepassword.gql

@@ -0,0 +1,13 @@
+mutation($continuationToken: String!, $newPassword: String!) {
+  authentication {
+    loginChangePassword(continuationToken: $continuationToken, newPassword: $newPassword) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+      jwt
+    }
+  }
+}

+ 3 - 2
client/graph/login/login-mutation-login.gql

@@ -8,8 +8,9 @@ mutation($username: String!, $password: String!, $strategy: String!) {
         message
         message
       }
       }
       jwt
       jwt
-      tfaRequired
-      tfaLoginToken
+      mustChangePwd
+      mustProvideTFA
+      continuationToken
     }
     }
   }
   }
 }
 }

+ 3 - 2
client/graph/login/login-mutation-tfa.gql

@@ -1,12 +1,13 @@
-mutation($loginToken: String!, $securityCode: String!) {
+mutation($continuationToken: String!, $securityCode: String!) {
   authentication {
   authentication {
-    loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
+    loginTFA(continuationToken: $continuationToken, securityCode: $securityCode) {
       responseResult {
       responseResult {
         succeeded
         succeeded
         errorCode
         errorCode
         slug
         slug
         message
         message
       }
       }
+      jwt
     }
     }
   }
   }
 }
 }

+ 16 - 3
client/scss/pages/_error.scss

@@ -6,6 +6,7 @@
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
   color: mc('grey', '50');
   color: mc('grey', '50');
+  font-family: Roboto, Arial, sans-serif;
 
 
   img {
   img {
     width: 250px;
     width: 250px;
@@ -57,8 +58,20 @@
     }
     }
   }
   }
 
 
-  code {
-    color: mc('grey', '500');
-    font-size: .8rem;
+  > strong {
+    font-size: 1.5rem;
+  }
+
+  > span {
+    margin-top: 1rem;
+  }
+
+  > pre {
+    margin-top: 2rem;
+
+    code {
+      color: mc('grey', '500');
+      font-size: .8rem;
+    }
   }
   }
 }
 }

+ 20 - 19
client/themes/default/components/page.vue

@@ -70,20 +70,7 @@
                       v-list-item-title.px-3.caption.grey--text(:class='darkMode ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}
                       v-list-item-title.px-3.caption.grey--text(:class='darkMode ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}
                     //- v-divider(inset, v-if='tocIdx < toc.length - 1')
                     //- v-divider(inset, v-if='tocIdx < toc.length - 1')
 
 
-            v-card.mt-5
-              .pa-5.pt-3
-                .overline.indigo--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
-                  span {{$t('common:page.lastEditedBy')}}
-                  v-spacer
-                  v-tooltip(top, v-if='isAuthenticated')
-                    template(v-slot:activator='{ on }')
-                      v-btn.btn-animate-edit(icon, :href='"/h/" + locale + "/" + path', v-on='on', x-small)
-                        v-icon(color='grey', dense) mdi-history
-                    span History
-                .body-2.grey--text(:class='darkMode ? `` : `text--darken-3`') {{ authorName }}
-                .caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
-
-            v-card.mt-5(v-if='tags.length > 0 || true')
+            v-card.mt-5(v-if='tags.length > 0')
               .pa-5
               .pa-5
                 .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') Tags
                 .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') Tags
                 v-chip.mr-1(
                 v-chip.mr-1(
@@ -96,6 +83,19 @@
                   v-icon(color='teal', left, small) mdi-label
                   v-icon(color='teal', left, small) mdi-label
                   span.teal--text.text--darken-2 {{tag.text}}
                   span.teal--text.text--darken-2 {{tag.text}}
 
 
+            v-card.mt-5
+              .pa-5
+                .overline.indigo--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
+                  span {{$t('common:page.lastEditedBy')}}
+                  v-spacer
+                  v-tooltip(top, v-if='isAuthenticated')
+                    template(v-slot:activator='{ on }')
+                      v-btn.btn-animate-edit(icon, :href='"/h/" + locale + "/" + path', v-on='on', x-small)
+                        v-icon(color='grey', dense) mdi-history
+                    span History
+                .body-2.grey--text(:class='darkMode ? `` : `text--darken-3`') {{ authorName }}
+                .caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
+
             v-card.mt-5
             v-card.mt-5
               .pa-5
               .pa-5
                 .overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating
                 .overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating
@@ -108,20 +108,21 @@
                     hover
                     hover
                   )
                   )
                   .caption.grey--text 5 votes
                   .caption.grey--text 5 votes
-              v-divider
-              v-toolbar(:color='darkMode ? `grey darken-3` : `grey lighten-4`', flat, dense)
+
+            v-card.mt-5(flat)
+              v-toolbar(:color='darkMode ? `grey darken-3` : `grey lighten-3`', flat, dense)
                 v-spacer
                 v-spacer
                 v-tooltip(bottom)
                 v-tooltip(bottom)
                   template(v-slot:activator='{ on }')
                   template(v-slot:activator='{ on }')
-                    v-btn(icon, tile, small, v-on='on'): v-icon(color='grey') mdi-bookmark
+                    v-btn(icon, tile, v-on='on'): v-icon(color='grey') mdi-bookmark
                   span {{$t('common:page.bookmark')}}
                   span {{$t('common:page.bookmark')}}
                 v-tooltip(bottom)
                 v-tooltip(bottom)
                   template(v-slot:activator='{ on }')
                   template(v-slot:activator='{ on }')
-                    v-btn(icon, tile, small, v-on='on'): v-icon(color='grey') mdi-share-variant
+                    v-btn(icon, tile, v-on='on'): v-icon(color='grey') mdi-share-variant
                   span {{$t('common:page.share')}}
                   span {{$t('common:page.share')}}
                 v-tooltip(bottom)
                 v-tooltip(bottom)
                   template(v-slot:activator='{ on }')
                   template(v-slot:activator='{ on }')
-                    v-btn(icon, tile, small, v-on='on'): v-icon(color='grey') mdi-printer
+                    v-btn(icon, tile, v-on='on'): v-icon(color='grey') mdi-printer
                   span {{$t('common:page.printFormat')}}
                   span {{$t('common:page.printFormat')}}
                 v-spacer
                 v-spacer
 
 

+ 1 - 1
dev/templates/legacy.pug

@@ -32,7 +32,7 @@ html
       link(
       link(
         type='text/css'
         type='text/css'
         rel='stylesheet'
         rel='stylesheet'
-        href='https://use.fontawesome.com/releases/v5.9.0/css/all.css'
+        href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
         )
         )
     else if config.theming.iconset === 'fa4'
     else if config.theming.iconset === 'fa4'
       link(
       link(

+ 1 - 1
dev/templates/master.pug

@@ -36,7 +36,7 @@ html(lang=siteConfig.lang)
       link(
       link(
         type='text/css'
         type='text/css'
         rel='stylesheet'
         rel='stylesheet'
-        href='https://use.fontawesome.com/releases/v5.9.0/css/all.css'
+        href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
         )
         )
     else if config.theming.iconset === 'fa4'
     else if config.theming.iconset === 'fa4'
       link(
       link(

+ 56 - 53
package.json

@@ -35,13 +35,13 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@aoberoi/passport-slack": "1.0.5",
     "@aoberoi/passport-slack": "1.0.5",
-    "@bugsnag/js": "6.3.2",
+    "@bugsnag/js": "6.4.0",
     "algoliasearch": "3.33.0",
     "algoliasearch": "3.33.0",
     "apollo-fetch": "0.7.0",
     "apollo-fetch": "0.7.0",
-    "apollo-server": "2.8.1",
-    "apollo-server-express": "2.8.1",
+    "apollo-server": "2.9.0",
+    "apollo-server-express": "2.9.0",
     "auto-load": "3.0.4",
     "auto-load": "3.0.4",
-    "aws-sdk": "2.503.0",
+    "aws-sdk": "2.517.0",
     "axios": "0.19.0",
     "axios": "0.19.0",
     "azure-search-client": "3.1.5",
     "azure-search-client": "3.1.5",
     "bcryptjs-then": "1.0.1",
     "bcryptjs-then": "1.0.1",
@@ -67,18 +67,18 @@
     "express": "4.17.1",
     "express": "4.17.1",
     "express-brute": "1.0.1",
     "express-brute": "1.0.1",
     "express-session": "1.16.2",
     "express-session": "1.16.2",
-    "file-type": "12.1.0",
+    "file-type": "12.2.0",
     "filesize": "4.1.2",
     "filesize": "4.1.2",
     "fs-extra": "8.1.0",
     "fs-extra": "8.1.0",
     "getos": "3.1.1",
     "getos": "3.1.1",
-    "graphql": "14.4.2",
+    "graphql": "14.5.3",
     "graphql-list-fields": "2.0.2",
     "graphql-list-fields": "2.0.2",
     "graphql-rate-limit-directive": "1.1.0",
     "graphql-rate-limit-directive": "1.1.0",
     "graphql-subscriptions": "1.1.0",
     "graphql-subscriptions": "1.1.0",
     "graphql-tools": "4.0.5",
     "graphql-tools": "4.0.5",
-    "highlight.js": "9.15.9",
-    "i18next": "17.0.8",
-    "i18next-express-middleware": "1.8.0",
+    "highlight.js": "9.15.10",
+    "i18next": "17.0.12",
+    "i18next-express-middleware": "1.8.1",
     "i18next-node-fs-backend": "2.1.3",
     "i18next-node-fs-backend": "2.1.3",
     "image-size": "0.7.4",
     "image-size": "0.7.4",
     "js-base64": "2.5.1",
     "js-base64": "2.5.1",
@@ -86,12 +86,12 @@
     "js-yaml": "3.13.1",
     "js-yaml": "3.13.1",
     "jsonwebtoken": "8.5.1",
     "jsonwebtoken": "8.5.1",
     "klaw": "3.0.0",
     "klaw": "3.0.0",
-    "knex": "0.19.1",
+    "knex": "0.19.2",
     "lodash": "4.17.15",
     "lodash": "4.17.15",
-    "markdown-it": "9.0.1",
+    "markdown-it": "9.1.0",
     "markdown-it-abbr": "1.0.4",
     "markdown-it-abbr": "1.0.4",
     "markdown-it-anchor": "5.2.4",
     "markdown-it-anchor": "5.2.4",
-    "markdown-it-attrs": "3.0.0",
+    "markdown-it-attrs": "3.0.1",
     "markdown-it-emoji": "1.4.0",
     "markdown-it-emoji": "1.4.0",
     "markdown-it-expand-tabs": "1.0.13",
     "markdown-it-expand-tabs": "1.0.13",
     "markdown-it-external-links": "0.0.6",
     "markdown-it-external-links": "0.0.6",
@@ -106,7 +106,7 @@
     "mime-types": "2.1.24",
     "mime-types": "2.1.24",
     "moment": "2.24.0",
     "moment": "2.24.0",
     "moment-timezone": "0.5.26",
     "moment-timezone": "0.5.26",
-    "mongodb": "3.2.7",
+    "mongodb": "3.3.1",
     "mssql": "5.1.0",
     "mssql": "5.1.0",
     "multer": "1.4.2",
     "multer": "1.4.2",
     "mysql2": "1.6.5",
     "mysql2": "1.6.5",
@@ -116,7 +116,7 @@
     "nodemailer": "6.3.0",
     "nodemailer": "6.3.0",
     "objection": "1.6.9",
     "objection": "1.6.9",
     "passport": "0.4.0",
     "passport": "0.4.0",
-    "passport-auth0": "1.2.0",
+    "passport-auth0": "1.2.1",
     "passport-azure-ad": "4.1.0",
     "passport-azure-ad": "4.1.0",
     "passport-cas": "0.1.1",
     "passport-cas": "0.1.1",
     "passport-discord": "0.1.3",
     "passport-discord": "0.1.3",
@@ -135,7 +135,7 @@
     "passport-saml": "1.1.0",
     "passport-saml": "1.1.0",
     "passport-twitch": "1.0.3",
     "passport-twitch": "1.0.3",
     "pem-jwk": "2.0.0",
     "pem-jwk": "2.0.0",
-    "pg": "7.12.0",
+    "pg": "7.12.1",
     "pg-hstore": "2.3.3",
     "pg-hstore": "2.3.3",
     "pg-query-stream": "2.0.0",
     "pg-query-stream": "2.0.0",
     "pg-tsquery": "8.0.5",
     "pg-tsquery": "8.0.5",
@@ -152,18 +152,18 @@
     "serve-favicon": "2.5.0",
     "serve-favicon": "2.5.0",
     "simple-git": "1.124.0",
     "simple-git": "1.124.0",
     "solr-node": "1.2.1",
     "solr-node": "1.2.1",
-    "sqlite3": "4.0.9",
+    "sqlite3": "4.1.0",
     "striptags": "3.1.1",
     "striptags": "3.1.1",
     "subscriptions-transport-ws": "0.9.16",
     "subscriptions-transport-ws": "0.9.16",
     "tar-fs": "2.0.0",
     "tar-fs": "2.0.0",
     "twemoji": "12.1.2",
     "twemoji": "12.1.2",
     "uslug": "1.0.4",
     "uslug": "1.0.4",
-    "uuid": "3.3.2",
+    "uuid": "3.3.3",
     "validate.js": "0.13.1",
     "validate.js": "0.13.1",
     "validator": "11.1.0",
     "validator": "11.1.0",
     "validator-as-promised": "1.0.2",
     "validator-as-promised": "1.0.2",
     "winston": "3.2.1",
     "winston": "3.2.1",
-    "yargs": "13.3.0"
+    "yargs": "14.0.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@babel/cli": "^7.5.0",
     "@babel/cli": "^7.5.0",
@@ -179,13 +179,13 @@
     "@babel/plugin-syntax-import-meta": "^7.2.0",
     "@babel/plugin-syntax-import-meta": "^7.2.0",
     "@babel/polyfill": "^7.4.4",
     "@babel/polyfill": "^7.4.4",
     "@babel/preset-env": "^7.5.4",
     "@babel/preset-env": "^7.5.4",
-    "@mdi/font": "3.8.95",
+    "@mdi/font": "4.1.95",
     "@panter/vue-i18next": "0.15.1",
     "@panter/vue-i18next": "0.15.1",
-    "@vue/babel-preset-app": "3.10.0",
+    "@vue/babel-preset-app": "3.11.0",
     "animate-sass": "0.8.2",
     "animate-sass": "0.8.2",
     "animated-number-vue": "1.0.0",
     "animated-number-vue": "1.0.0",
-    "apollo-cache-inmemory": "1.6.2",
-    "apollo-client": "2.6.3",
+    "apollo-cache-inmemory": "1.6.3",
+    "apollo-client": "2.6.4",
     "apollo-link": "1.2.12",
     "apollo-link": "1.2.12",
     "apollo-link-batch-http": "1.2.12",
     "apollo-link-batch-http": "1.2.12",
     "apollo-link-error": "1.1.11",
     "apollo-link-error": "1.1.11",
@@ -195,9 +195,9 @@
     "apollo-utilities": "1.3.2",
     "apollo-utilities": "1.3.2",
     "autoprefixer": "9.6.1",
     "autoprefixer": "9.6.1",
     "babel-eslint": "10.0.2",
     "babel-eslint": "10.0.2",
-    "babel-jest": "24.8.0",
+    "babel-jest": "24.9.0",
     "babel-loader": "^8.0.6",
     "babel-loader": "^8.0.6",
-    "babel-plugin-graphql-tag": "2.4.0",
+    "babel-plugin-graphql-tag": "2.5.0",
     "babel-plugin-lodash": "3.3.4",
     "babel-plugin-lodash": "3.3.4",
     "babel-plugin-prismjs": "1.1.1",
     "babel-plugin-prismjs": "1.1.1",
     "babel-plugin-transform-imports": "2.0.0",
     "babel-plugin-transform-imports": "2.0.0",
@@ -206,26 +206,26 @@
     "chart.js": "2.8.0",
     "chart.js": "2.8.0",
     "clean-webpack-plugin": "3.0.0",
     "clean-webpack-plugin": "3.0.0",
     "copy-webpack-plugin": "5.0.4",
     "copy-webpack-plugin": "5.0.4",
-    "core-js": "3.1.4",
-    "css-loader": "3.1.0",
+    "core-js": "3.2.1",
+    "css-loader": "3.2.0",
     "cssnano": "4.1.10",
     "cssnano": "4.1.10",
     "duplicate-package-checker-webpack-plugin": "3.0.0",
     "duplicate-package-checker-webpack-plugin": "3.0.0",
     "epic-spinners": "1.1.0",
     "epic-spinners": "1.1.0",
-    "eslint": "6.1.0",
+    "eslint": "6.2.2",
     "eslint-config-requarks": "1.0.7",
     "eslint-config-requarks": "1.0.7",
-    "eslint-config-standard": "13.0.1",
+    "eslint-config-standard": "14.0.1",
     "eslint-plugin-import": "2.18.2",
     "eslint-plugin-import": "2.18.2",
     "eslint-plugin-node": "9.1.0",
     "eslint-plugin-node": "9.1.0",
     "eslint-plugin-promise": "4.2.1",
     "eslint-plugin-promise": "4.2.1",
-    "eslint-plugin-standard": "4.0.0",
+    "eslint-plugin-standard": "4.0.1",
     "eslint-plugin-vue": "5.2.3",
     "eslint-plugin-vue": "5.2.3",
     "fibers": "4.0.1",
     "fibers": "4.0.1",
-    "file-loader": "4.1.0",
-    "filepond": "4.4.12",
+    "file-loader": "4.2.0",
+    "filepond": "4.5.0",
     "filepond-plugin-file-validate-type": "1.2.4",
     "filepond-plugin-file-validate-type": "1.2.4",
     "filesize.js": "1.0.2",
     "filesize.js": "1.0.2",
-    "grapesjs": "0.14.62",
-    "graphiql": "0.13.2",
+    "grapesjs": "0.15.3",
+    "graphiql": "0.14.2",
     "graphql-persisted-document-loader": "1.0.1",
     "graphql-persisted-document-loader": "1.0.1",
     "graphql-tag": "^2.10.1",
     "graphql-tag": "^2.10.1",
     "graphql-voyager": "1.0.0-rc.27",
     "graphql-voyager": "1.0.0-rc.27",
@@ -234,10 +234,10 @@
     "html-webpack-pug-plugin": "2.0.0",
     "html-webpack-pug-plugin": "2.0.0",
     "i18next-chained-backend": "2.0.0",
     "i18next-chained-backend": "2.0.0",
     "i18next-localstorage-backend": "3.0.0",
     "i18next-localstorage-backend": "3.0.0",
-    "i18next-xhr-backend": "3.0.1",
+    "i18next-xhr-backend": "3.1.2",
     "ignore-loader": "0.1.2",
     "ignore-loader": "0.1.2",
-    "jest": "24.8.0",
-    "js-cookie": "2.2.0",
+    "jest": "24.9.0",
+    "js-cookie": "2.2.1",
     "mini-css-extract-plugin": "0.8.0",
     "mini-css-extract-plugin": "0.8.0",
     "moment-duration-format": "2.3.2",
     "moment-duration-format": "2.3.2",
     "offline-plugin": "5.0.7",
     "offline-plugin": "5.0.7",
@@ -254,19 +254,19 @@
     "pug-loader": "2.4.0",
     "pug-loader": "2.4.0",
     "pug-plain-loader": "1.0.0",
     "pug-plain-loader": "1.0.0",
     "raw-loader": "3.1.0",
     "raw-loader": "3.1.0",
-    "react": "16.8.6",
-    "react-dom": "16.8.6",
+    "react": "16.9.0",
+    "react-dom": "16.9.0",
     "resolve-url-loader": "3.1.0",
     "resolve-url-loader": "3.1.0",
-    "sass": "1.22.9",
-    "sass-loader": "7.1.0",
+    "sass": "1.22.10",
+    "sass-loader": "7.3.1",
     "sass-resources-loader": "2.0.1",
     "sass-resources-loader": "2.0.1",
     "script-ext-html-webpack-plugin": "2.1.4",
     "script-ext-html-webpack-plugin": "2.1.4",
     "simple-progress-webpack-plugin": "1.1.2",
     "simple-progress-webpack-plugin": "1.1.2",
-    "style-loader": "0.23.1",
-    "terser": "4.1.3",
+    "style-loader": "1.0.0",
+    "terser": "4.2.1",
     "twemoji-awesome": "1.0.6",
     "twemoji-awesome": "1.0.6",
     "url-loader": "2.1.0",
     "url-loader": "2.1.0",
-    "vee-validate": "2.2.13",
+    "vee-validate": "2.2.15",
     "velocity-animate": "1.5.2",
     "velocity-animate": "1.5.2",
     "viz.js": "2.1.2",
     "viz.js": "2.1.2",
     "vue": "2.6.10",
     "vue": "2.6.10",
@@ -274,32 +274,32 @@
     "vue-chartjs": "3.4.2",
     "vue-chartjs": "3.4.2",
     "vue-clipboards": "1.3.0",
     "vue-clipboards": "1.3.0",
     "vue-codemirror": "4.0.6",
     "vue-codemirror": "4.0.6",
-    "vue-filepond": "5.1.2",
+    "vue-filepond": "5.1.3",
     "vue-hot-reload-api": "2.3.3",
     "vue-hot-reload-api": "2.3.3",
     "vue-loader": "15.7.1",
     "vue-loader": "15.7.1",
-    "vue-material-design-icons": "3.3.1",
+    "vue-material-design-icons": "3.4.0",
     "vue-moment": "4.0.0",
     "vue-moment": "4.0.0",
-    "vue-router": "3.0.7",
+    "vue-router": "3.1.2",
     "vue-simple-breakpoints": "1.0.3",
     "vue-simple-breakpoints": "1.0.3",
     "vue-status-indicator": "1.2.1",
     "vue-status-indicator": "1.2.1",
     "vue-template-compiler": "2.6.10",
     "vue-template-compiler": "2.6.10",
     "vue-tour": "1.1.0",
     "vue-tour": "1.1.0",
-    "vue2-animate": "2.1.0",
+    "vue2-animate": "2.1.2",
     "vuedraggable": "2.23.0",
     "vuedraggable": "2.23.0",
-    "vuescroll": "4.13.1",
-    "vuetify": "2.0.4",
+    "vuescroll": "4.14.0",
+    "vuetify": "2.0.10",
     "vuetify-loader": "1.3.0",
     "vuetify-loader": "1.3.0",
     "vuex": "3.1.1",
     "vuex": "3.1.1",
     "vuex-pathify": "1.2.4",
     "vuex-pathify": "1.2.4",
     "vuex-persistedstate": "2.5.4",
     "vuex-persistedstate": "2.5.4",
-    "webpack": "4.39.1",
+    "webpack": "4.39.2",
     "webpack-bundle-analyzer": "3.4.1",
     "webpack-bundle-analyzer": "3.4.1",
-    "webpack-cli": "3.3.6",
+    "webpack-cli": "3.3.7",
     "webpack-dev-middleware": "3.7.0",
     "webpack-dev-middleware": "3.7.0",
     "webpack-hot-middleware": "2.25.0",
     "webpack-hot-middleware": "2.25.0",
     "webpack-merge": "4.2.1",
     "webpack-merge": "4.2.1",
     "webpack-subresource-integrity": "1.3.2",
     "webpack-subresource-integrity": "1.3.2",
-    "webpackbar": "3.2.0",
+    "webpackbar": "4.0.0",
     "whatwg-fetch": "3.0.0",
     "whatwg-fetch": "3.0.0",
     "write-file-webpack-plugin": "4.5.1",
     "write-file-webpack-plugin": "4.5.1",
     "xterm": "3.14.5",
     "xterm": "3.14.5",
@@ -344,7 +344,10 @@
     "requireSpaceAfterCodeOperator": true,
     "requireSpaceAfterCodeOperator": true,
     "requireStrictEqualityOperators": true,
     "requireStrictEqualityOperators": true,
     "validateAttributeQuoteMarks": "'",
     "validateAttributeQuoteMarks": "'",
-    "validateAttributeSeparator": { "separator": ", ", "multiLineSeparator": "\n  " },
+    "validateAttributeSeparator": {
+      "separator": ", ",
+      "multiLineSeparator": "\n  "
+    },
     "validateDivTags": true,
     "validateDivTags": true,
     "validateIndentation": 2,
     "validateIndentation": 2,
     "excludeFiles": [
     "excludeFiles": [

+ 7 - 0
server/app/data.yml

@@ -42,6 +42,13 @@ defaults:
       theme: 'default'
       theme: 'default'
       iconset: 'md'
       iconset: 'md'
       darkMode: false
       darkMode: false
+    security:
+      securityIframe: true
+      securityReferrerPolicy: true
+      securityHSTS: false
+      securityHSTSDuration: 300
+      securityCSP: false
+      securityCSPDirectives: ''
     flags:
     flags:
       ldapdebug: false
       ldapdebug: false
       sqllog: false
       sqllog: false

+ 23 - 9
server/graph/resolvers/authentication.js

@@ -7,16 +7,16 @@ const graphHelper = require('../../helpers/graph')
 
 
 module.exports = {
 module.exports = {
   Query: {
   Query: {
-    async authentication() { return {} }
+    async authentication () { return {} }
   },
   },
   Mutation: {
   Mutation: {
-    async authentication() { return {} }
+    async authentication () { return {} }
   },
   },
   AuthenticationQuery: {
   AuthenticationQuery: {
     /**
     /**
      * Fetch active authentication strategies
      * Fetch active authentication strategies
      */
      */
-    async strategies(obj, args, context, info) {
+    async strategies (obj, args, context, info) {
       let strategies = await WIKI.models.authentication.getStrategies(args.isEnabled)
       let strategies = await WIKI.models.authentication.getStrategies(args.isEnabled)
       strategies = strategies.map(stg => {
       strategies = strategies.map(stg => {
         const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {}
         const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {}
@@ -44,7 +44,7 @@ module.exports = {
     /**
     /**
      * Perform Login
      * Perform Login
      */
      */
-    async login(obj, args, context) {
+    async login (obj, args, context) {
       try {
       try {
         const authResult = await WIKI.models.users.login(args, context)
         const authResult = await WIKI.models.users.login(args, context)
         return {
         return {
@@ -63,7 +63,7 @@ module.exports = {
     /**
     /**
      * Perform 2FA Login
      * Perform 2FA Login
      */
      */
-    async loginTFA(obj, args, context) {
+    async loginTFA (obj, args, context) {
       try {
       try {
         const authResult = await WIKI.models.users.loginTFA(args, context)
         const authResult = await WIKI.models.users.loginTFA(args, context)
         return {
         return {
@@ -74,10 +74,24 @@ module.exports = {
         return graphHelper.generateError(err)
         return graphHelper.generateError(err)
       }
       }
     },
     },
+    /**
+     * Perform Mandatory Password Change after Login
+     */
+    async loginChangePassword (obj, args, context) {
+      try {
+        const authResult = await WIKI.models.users.loginChangePassword(args, context)
+        return {
+          ...authResult,
+          responseResult: graphHelper.generateSuccess('Password changed successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
     /**
     /**
      * Register a new account
      * Register a new account
      */
      */
-    async register(obj, args, context) {
+    async register (obj, args, context) {
       try {
       try {
         await WIKI.models.users.register({ ...args, verify: true }, context)
         await WIKI.models.users.register({ ...args, verify: true }, context)
         return {
         return {
@@ -90,7 +104,7 @@ module.exports = {
     /**
     /**
      * Update Authentication Strategies
      * Update Authentication Strategies
      */
      */
-    async updateStrategies(obj, args, context) {
+    async updateStrategies (obj, args, context) {
       try {
       try {
         WIKI.config.auth = {
         WIKI.config.auth = {
           audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
           audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
@@ -122,7 +136,7 @@ module.exports = {
     /**
     /**
      * Generate New Authentication Public / Private Key Certificates
      * Generate New Authentication Public / Private Key Certificates
      */
      */
-    async regenerateCertificates(obj, args, context) {
+    async regenerateCertificates (obj, args, context) {
       try {
       try {
         await WIKI.auth.regenerateCertificates()
         await WIKI.auth.regenerateCertificates()
         return {
         return {
@@ -135,7 +149,7 @@ module.exports = {
     /**
     /**
      * Reset Guest User
      * Reset Guest User
      */
      */
-    async resetGuestUser(obj, args, context) {
+    async resetGuestUser (obj, args, context) {
       try {
       try {
         await WIKI.auth.resetGuestUser()
         await WIKI.auth.resetGuestUser()
         return {
         return {

+ 11 - 2
server/graph/resolvers/site.js

@@ -17,7 +17,8 @@ module.exports = {
         company: WIKI.config.company,
         company: WIKI.config.company,
         ...WIKI.config.seo,
         ...WIKI.config.seo,
         ...WIKI.config.logo,
         ...WIKI.config.logo,
-        ...WIKI.config.features
+        ...WIKI.config.features,
+        ...WIKI.config.security
       }
       }
     }
     }
   },
   },
@@ -42,7 +43,15 @@ module.exports = {
           featurePageComments: args.featurePageComments,
           featurePageComments: args.featurePageComments,
           featurePersonalWikis: args.featurePersonalWikis
           featurePersonalWikis: args.featurePersonalWikis
         }
         }
-        await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'seo', 'logo', 'features'])
+        WIKI.config.security = {
+          securityIframe: args.securityIframe,
+          securityReferrerPolicy: args.securityReferrerPolicy,
+          securityHSTS: args.securityHSTS,
+          securityHSTSDuration: args.securityHSTSDuration,
+          securityCSP: args.securityCSP,
+          securityCSPDirectives: args.securityCSPDirectives
+        }
+        await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'seo', 'logo', 'features', 'security'])
 
 
         return {
         return {
           responseResult: graphHelper.generateSuccess('Site configuration updated successfully')
           responseResult: graphHelper.generateSuccess('Site configuration updated successfully')

+ 10 - 4
server/graph/schemas/authentication.graphql

@@ -32,9 +32,14 @@ type AuthenticationMutation {
   ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
   ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
 
 
   loginTFA(
   loginTFA(
-    loginToken: String!
+    continuationToken: String!
     securityCode: String!
     securityCode: String!
-  ): DefaultResponse @rateLimit(limit: 5, duration: 60)
+  ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
+
+  loginChangePassword(
+    continuationToken: String!
+    newPassword: String!
+  ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
 
 
   register(
   register(
     email: String!
     email: String!
@@ -76,8 +81,9 @@ type AuthenticationStrategy {
 type AuthenticationLoginResponse {
 type AuthenticationLoginResponse {
   responseResult: ResponseStatus
   responseResult: ResponseStatus
   jwt: String
   jwt: String
-  tfaRequired: Boolean
-  tfaLoginToken: String
+  mustChangePwd: Boolean
+  mustProvideTFA: Boolean
+  continuationToken: String
 }
 }
 
 
 type AuthenticationRegisterResponse {
 type AuthenticationRegisterResponse {

+ 12 - 0
server/graph/schemas/site.graphql

@@ -36,6 +36,12 @@ type SiteMutation {
     featurePageRatings: Boolean!
     featurePageRatings: Boolean!
     featurePageComments: Boolean!
     featurePageComments: Boolean!
     featurePersonalWikis: Boolean!
     featurePersonalWikis: Boolean!
+    securityIframe: Boolean!
+    securityReferrerPolicy: Boolean!
+    securityHSTS: Boolean!
+    securityHSTSDuration: Int!
+    securityCSP: Boolean!
+    securityCSPDirectives: String!
   ): DefaultResponse @auth(requires: ["manage:system"])
   ): DefaultResponse @auth(requires: ["manage:system"])
 }
 }
 
 
@@ -56,4 +62,10 @@ type SiteConfig {
   featurePageRatings: Boolean!
   featurePageRatings: Boolean!
   featurePageComments: Boolean!
   featurePageComments: Boolean!
   featurePersonalWikis: Boolean!
   featurePersonalWikis: Boolean!
+  securityIframe: Boolean!
+  securityReferrerPolicy: Boolean!
+  securityHSTS: Boolean!
+  securityHSTSDuration: Int!
+  securityCSP: Boolean!
+  securityCSPDirectives: String!
 }
 }

+ 2 - 0
server/graph/schemas/user.graphql

@@ -89,6 +89,8 @@ type User {
   providerKey: String!
   providerKey: String!
   providerId: String
   providerId: String
   isSystem: Boolean!
   isSystem: Boolean!
+  isActive: Boolean!
+  isVerified: Boolean!
   location: String!
   location: String!
   jobTitle: String!
   jobTitle: String!
   timezone: String!
   timezone: String!

+ 12 - 3
server/middlewares/security.js

@@ -1,4 +1,4 @@
-'use strict'
+/* global WIKI */
 
 
 /**
 /**
  * Security Middleware
  * Security Middleware
@@ -13,7 +13,9 @@ module.exports = function (req, res, next) {
   req.app.disable('x-powered-by')
   req.app.disable('x-powered-by')
 
 
   // -> Disable Frame Embedding
   // -> Disable Frame Embedding
-  res.set('X-Frame-Options', 'deny')
+  if (WIKI.config.securityIframe) {
+    res.set('X-Frame-Options', 'deny')
+  }
 
 
   // -> Re-enable XSS Fitler if disabled
   // -> Re-enable XSS Fitler if disabled
   res.set('X-XSS-Protection', '1; mode=block')
   res.set('X-XSS-Protection', '1; mode=block')
@@ -25,7 +27,14 @@ module.exports = function (req, res, next) {
   res.set('X-UA-Compatible', 'IE=edge')
   res.set('X-UA-Compatible', 'IE=edge')
 
 
   // -> Disables referrer header when navigating to a different origin
   // -> Disables referrer header when navigating to a different origin
-  res.set('Referrer-Policy', 'same-origin')
+  if (WIKI.config.securityReferrerPolicy) {
+    res.set('Referrer-Policy', 'same-origin')
+  }
+
+  // -> Enforce HSTS
+  if (WIKI.config.securityHSTS) {
+    res.set('Strict-Transport-Security', `max-age=${WIKI.config.securityHSTSDuration}; includeSubDomains`)
+  }
 
 
   return next()
   return next()
 }
 }

+ 1 - 1
server/models/userKeys.js

@@ -45,7 +45,7 @@ module.exports = class UserKey extends Model {
   }
   }
 
 
   static async generateToken ({ userId, kind }, context) {
   static async generateToken ({ userId, kind }, context) {
-    const token = await nanoid()
+    const token = nanoid()
     await WIKI.models.userKeys.query().insert({
     await WIKI.models.userKeys.query().insert({
       kind,
       kind,
       token,
       token,

+ 61 - 16
server/models/users.js

@@ -3,7 +3,6 @@
 const bcrypt = require('bcryptjs-then')
 const bcrypt = require('bcryptjs-then')
 const _ = require('lodash')
 const _ = require('lodash')
 const tfa = require('node-2fa')
 const tfa = require('node-2fa')
-const securityHelper = require('../helpers/security')
 const jwt = require('jsonwebtoken')
 const jwt = require('jsonwebtoken')
 const Model = require('objection').Model
 const Model = require('objection').Model
 const validate = require('validate.js')
 const validate = require('validate.js')
@@ -280,30 +279,46 @@ module.exports = class User extends Model {
           if (err) { return reject(err) }
           if (err) { return reject(err) }
           if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
           if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
 
 
+          // Must Change Password?
+          if (user.mustChangePwd) {
+            try {
+              const pwdChangeToken = await WIKI.models.userKeys.generateToken({
+                kind: 'changePwd',
+                userId: user.id
+              })
+
+              return resolve({
+                mustChangePwd: true,
+                continuationToken: pwdChangeToken
+              })
+            } catch (err) {
+              WIKI.logger.warn(err)
+              return reject(new WIKI.Error.AuthGenericError())
+            }
+          }
+
           // Is 2FA required?
           // Is 2FA required?
           if (user.tfaIsActive) {
           if (user.tfaIsActive) {
             try {
             try {
-              let loginToken = await securityHelper.generateToken(32)
-              await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
+              const tfaToken = await WIKI.models.userKeys.generateToken({
+                kind: 'tfa',
+                userId: user.id
+              })
               return resolve({
               return resolve({
                 tfaRequired: true,
                 tfaRequired: true,
-                tfaLoginToken: loginToken
+                continuationToken: tfaToken
               })
               })
             } catch (err) {
             } catch (err) {
               WIKI.logger.warn(err)
               WIKI.logger.warn(err)
               return reject(new WIKI.Error.AuthGenericError())
               return reject(new WIKI.Error.AuthGenericError())
             }
             }
-          } else {
-            // No 2FA, log in user
-            return context.req.logIn(user, { session: !strInfo.useForm }, async err => {
-              if (err) { return reject(err) }
-              const jwtToken = await WIKI.models.users.refreshToken(user)
-              resolve({
-                jwt: jwtToken.token,
-                tfaRequired: false
-              })
-            })
           }
           }
+
+          context.req.logIn(user, { session: !strInfo.useForm }, async err => {
+            if (err) { return reject(err) }
+            const jwtToken = await WIKI.models.users.refreshToken(user)
+            resolve({ jwt: jwtToken.token })
+          })
         })(context.req, context.res, () => {})
         })(context.req, context.res, () => {})
       })
       })
     } else {
     } else {
@@ -348,7 +363,7 @@ module.exports = class User extends Model {
     }
     }
   }
   }
 
 
-  static async loginTFA(opts, context) {
+  static async loginTFA (opts, context) {
     if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
     if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
       let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
       let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
       if (result) {
       if (result) {
@@ -374,6 +389,36 @@ module.exports = class User extends Model {
     throw new WIKI.Error.AuthTFAInvalid()
     throw new WIKI.Error.AuthTFAInvalid()
   }
   }
 
 
+  /**
+   * Change Password from a Mandatory Password Change after Login
+   */
+  static async loginChangePassword ({ continuationToken, newPassword }, context) {
+    if (!newPassword || newPassword.length < 6) {
+      throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
+    }
+    const usr = await WIKI.models.userKeys.validateToken({
+      kind: 'changePwd',
+      token: continuationToken
+    })
+
+    if (usr) {
+      await WIKI.models.users.query().patch({
+        password: newPassword,
+        mustChangePwd: false
+      }).findById(usr.id)
+
+      return new Promise((resolve, reject) => {
+        context.req.logIn(usr, { session: false }, async err => {
+          if (err) { return reject(err) }
+          const jwtToken = await WIKI.models.users.refreshToken(usr)
+          resolve({ jwt: jwtToken.token })
+        })
+      })
+    } else {
+      throw new WIKI.Error.UserNotFound()
+    }
+  }
+
   /**
   /**
    * Create a new user
    * Create a new user
    *
    *
@@ -520,7 +565,7 @@ module.exports = class User extends Model {
         }
         }
         usrData.password = newPassword
         usrData.password = newPassword
       }
       }
-      if (!_.isEmpty(groups)) {
+      if (_.isArray(groups)) {
         const usrGroupsRaw = await usr.$relatedQuery('groups')
         const usrGroupsRaw = await usr.$relatedQuery('groups')
         const usrGroups = _.map(usrGroupsRaw, 'id')
         const usrGroups = _.map(usrGroupsRaw, 'id')
         // Relate added groups
         // Relate added groups

+ 1 - 1
server/modules/authentication/local/definition.yml

@@ -3,7 +3,7 @@ title: Local
 description: Built-in authentication for Wiki.js
 description: Built-in authentication for Wiki.js
 author: requarks.io
 author: requarks.io
 logo: https://static.requarks.io/logo/wikijs.svg
 logo: https://static.requarks.io/logo/wikijs.svg
-color: yellow darken-3
+color: primary
 website: https://wiki.js.org
 website: https://wiki.js.org
 isAvailable: true
 isAvailable: true
 useForm: true
 useForm: true

+ 7 - 21
server/views/error.pug

@@ -2,25 +2,11 @@ extends master.pug
 
 
 block body
 block body
   #root.is-fullscreen
   #root.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.text-right(xs2)
-                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
+    .app-error
+      a(href='/')
+        img(src='/svg/logo-wikijs.svg')
+      strong Oops, something went wrong...
+      span= 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}
+      if error.stack
+        pre: code #{error.stack}

+ 1 - 1
server/views/legacy/master.pug

@@ -32,7 +32,7 @@ html
       link(
       link(
         type='text/css'
         type='text/css'
         rel='stylesheet'
         rel='stylesheet'
-        href='https://use.fontawesome.com/releases/v5.9.0/css/all.css'
+        href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
         )
         )
     else if config.theming.iconset === 'fa4'
     else if config.theming.iconset === 'fa4'
       link(
       link(

+ 1 - 1
server/views/master.pug

@@ -36,7 +36,7 @@ html(lang=siteConfig.lang)
       link(
       link(
         type='text/css'
         type='text/css'
         rel='stylesheet'
         rel='stylesheet'
-        href='https://use.fontawesome.com/releases/v5.9.0/css/all.css'
+        href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
         )
         )
     else if config.theming.iconset === 'fa4'
     else if config.theming.iconset === 'fa4'
       link(
       link(

文件差異過大導致無法顯示
+ 347 - 274
yarn.lock


部分文件因文件數量過多而無法顯示