Quellcode durchsuchen

feat: admin terminal + legacy code cleanup

Nicolas Giard vor 2 Jahren
Ursprung
Commit
3ebbbc8b5e
100 geänderte Dateien mit 52 neuen und 14707 gelöschten Zeilen
  1. 1 3
      .gitignore
  2. 0 5
      client/client-app.js
  3. 0 335
      client/components/admin.vue
  4. 0 181
      client/components/admin/admin-analytics.vue
  5. 0 236
      client/components/admin/admin-api-create.vue
  6. 0 239
      client/components/admin/admin-api.vue
  7. 0 433
      client/components/admin/admin-auth.vue
  8. 0 206
      client/components/admin/admin-comments.vue
  9. 0 256
      client/components/admin/admin-contribute.vue
  10. 0 256
      client/components/admin/admin-dashboard.vue
  11. 0 94
      client/components/admin/admin-dev-flags.vue
  12. 0 66
      client/components/admin/admin-editor.vue
  13. 0 119
      client/components/admin/admin-extensions.vue
  14. 0 369
      client/components/admin/admin-general.vue
  15. 0 224
      client/components/admin/admin-groups-edit-permissions.vue
  16. 0 336
      client/components/admin/admin-groups-edit-rules.vue
  17. 0 149
      client/components/admin/admin-groups-edit-users.vue
  18. 0 271
      client/components/admin/admin-groups-edit.vue
  19. 0 170
      client/components/admin/admin-groups.vue
  20. 0 302
      client/components/admin/admin-locale.vue
  21. 0 106
      client/components/admin/admin-logging-console.vue
  22. 0 194
      client/components/admin/admin-logging.vue
  23. 0 258
      client/components/admin/admin-mail.vue
  24. 0 526
      client/components/admin/admin-navigation.vue
  25. 0 235
      client/components/admin/admin-pages-edit.vue
  26. 0 405
      client/components/admin/admin-pages-visualize.vue
  27. 0 169
      client/components/admin/admin-pages.vue
  28. 0 261
      client/components/admin/admin-rendering.vue
  29. 0 217
      client/components/admin/admin-search.vue
  30. 0 444
      client/components/admin/admin-security.vue
  31. 0 269
      client/components/admin/admin-ssl.vue
  32. 0 32
      client/components/admin/admin-stats.vue
  33. 0 372
      client/components/admin/admin-storage.vue
  34. 0 238
      client/components/admin/admin-system.vue
  35. 0 248
      client/components/admin/admin-tags.vue
  36. 0 255
      client/components/admin/admin-theme.vue
  37. 0 256
      client/components/admin/admin-users-create.vue
  38. 0 1077
      client/components/admin/admin-users-edit.vue
  39. 0 192
      client/components/admin/admin-users.vue
  40. 0 93
      client/components/admin/admin-utilities-auth.vue
  41. 0 108
      client/components/admin/admin-utilities-cache.vue
  42. 0 318
      client/components/admin/admin-utilities-content.vue
  43. 0 507
      client/components/admin/admin-utilities-importv1.vue
  44. 0 162
      client/components/admin/admin-utilities-telemetry.vue
  45. 0 91
      client/components/admin/admin-utilities.vue
  46. 0 116
      client/components/admin/admin-webhooks.vue
  47. 1 1
      client/components/common/loader.vue
  48. 3 3
      client/components/common/nav-header.vue
  49. 3 3
      client/components/common/search-results.vue
  50. 32 36
      client/components/editor.vue
  51. 2 2
      client/components/editor/editor-api.vue
  52. 2 2
      client/components/editor/editor-markdown.vue
  53. 4 125
      client/components/editor/editor-modal-editorselect.vue
  54. 0 801
      client/components/login.vue
  55. 1 1
      client/components/new-page.vue
  56. 0 98
      client/components/profile.vue
  57. 0 20
      client/components/profile/comments.vue
  58. 0 121
      client/components/profile/pages.vue
  59. 0 923
      client/components/profile/profile.vue
  60. 0 306
      client/components/register.vue
  61. 0 273
      client/components/setup.vue
  62. 3 3
      client/components/tags.vue
  63. 0 35
      client/components/welcome.vue
  64. 0 12
      client/graph/admin/analytics/analytics-mutation-save-providers.gql
  65. 0 17
      client/graph/admin/analytics/analytics-query-providers.gql
  66. 0 8
      client/graph/admin/auth/auth-query-groups.gql
  67. 0 7
      client/graph/admin/auth/auth-query-host.gql
  68. 0 21
      client/graph/admin/auth/auth-query-strategies.gql
  69. 0 17
      client/graph/admin/contribute/contribute-query-contributors.gql
  70. 0 12
      client/graph/admin/dashboard/dashboard-query-stats.gql
  71. 0 16
      client/graph/admin/dev/dev-mutation-save-flags.gql
  72. 0 8
      client/graph/admin/dev/dev-query-flags.gql
  73. 0 12
      client/graph/admin/groups/groups-mutation-assign.gql
  74. 0 18
      client/graph/admin/groups/groups-mutation-create.gql
  75. 0 12
      client/graph/admin/groups/groups-mutation-unassign.gql
  76. 0 12
      client/graph/admin/groups/groups-query-list.gql
  77. 0 12
      client/graph/admin/locale/locale-mutation-download.gql
  78. 0 12
      client/graph/admin/locale/locale-mutation-save.gql
  79. 0 21
      client/graph/admin/locale/locale-query-list.gql
  80. 0 12
      client/graph/admin/logging/logging-mutation-save-loggers.gql
  81. 0 17
      client/graph/admin/logging/logging-query-loggers.gql
  82. 0 7
      client/graph/admin/logging/logging-subscription-livetrail.gql
  83. 0 38
      client/graph/admin/mail/mail-mutation-save-config.gql
  84. 0 12
      client/graph/admin/mail/mail-mutation-sendtest.gql
  85. 0 18
      client/graph/admin/mail/mail-query-config.gql
  86. 0 17
      client/graph/admin/pages/pages-query-list.gql
  87. 0 27
      client/graph/admin/pages/pages-query-single.gql
  88. 0 12
      client/graph/admin/rendering/rendering-mutation-save-renderers.gql
  89. 0 18
      client/graph/admin/rendering/rendering-query-renderers.gql
  90. 0 12
      client/graph/admin/search/search-mutation-rebuild-index.gql
  91. 0 12
      client/graph/admin/search/search-mutation-save-engines.gql
  92. 0 17
      client/graph/admin/search/search-query-engines.gql
  93. 0 12
      client/graph/admin/storage/storage-mutation-executeaction.gql
  94. 0 12
      client/graph/admin/storage/storage-mutation-save-targets.gql
  95. 0 11
      client/graph/admin/storage/storage-query-status.gql
  96. 0 27
      client/graph/admin/storage/storage-query-targets.gql
  97. 0 12
      client/graph/admin/system/system-mutation-upgrade.gql
  98. 0 21
      client/graph/admin/system/system-query-info.gql
  99. 0 12
      client/graph/admin/theme/theme-mutation-save.gql
  100. 0 12
      client/graph/admin/theme/theme-query-config.gql

+ 1 - 3
.gitignore

@@ -19,9 +19,7 @@ npm-debug.log*
 # Generated assets
 /assets
 /assets-legacy
-server/views/master.pug
-server/views/legacy/master.pug
-server/views/setup.pug
+server/views/base.pug
 
 # Webpack
 .webpack-cache

+ 0 - 5
client/client-app.js

@@ -134,27 +134,22 @@ Vue.prototype.Velocity = Velocity
 // Register Vue Components
 // ====================================
 
-Vue.component('Admin', () => import(/* webpackChunkName: "admin" */ './components/admin.vue'))
 Vue.component('Comments', () => import(/* webpackChunkName: "comments" */ './components/comments.vue'))
 Vue.component('Editor', () => import(/* webpackPrefetch: -100, webpackChunkName: "editor" */ './components/editor.vue'))
 Vue.component('History', () => import(/* webpackChunkName: "history" */ './components/history.vue'))
 Vue.component('Loader', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/loader.vue'))
-Vue.component('Login', () => import(/* webpackPrefetch: true, webpackChunkName: "login" */ './components/login.vue'))
 Vue.component('NavHeader', () => import(/* webpackMode: "eager" */ './components/common/nav-header.vue'))
 Vue.component('NewPage', () => import(/* webpackChunkName: "new-page" */ './components/new-page.vue'))
 Vue.component('Notify', () => import(/* webpackMode: "eager" */ './components/common/notify.vue'))
 Vue.component('NotFound', () => import(/* webpackChunkName: "not-found" */ './components/not-found.vue'))
 Vue.component('PageSelector', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/page-selector.vue'))
 Vue.component('PageSource', () => import(/* webpackChunkName: "source" */ './components/source.vue'))
-Vue.component('Profile', () => import(/* webpackChunkName: "profile" */ './components/profile.vue'))
-Vue.component('Register', () => import(/* webpackChunkName: "register" */ './components/register.vue'))
 Vue.component('SearchResults', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/search-results.vue'))
 Vue.component('SocialSharing', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/social-sharing.vue'))
 Vue.component('Tags', () => import(/* webpackChunkName: "tags" */ './components/tags.vue'))
 Vue.component('Unauthorized', () => import(/* webpackChunkName: "unauthorized" */ './components/unauthorized.vue'))
 Vue.component('VCardChin', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-chin.vue'))
 Vue.component('VCardInfo', () => import(/* webpackPrefetch: true, webpackChunkName: "ui-extra" */ './components/common/v-card-info.vue'))
-Vue.component('Welcome', () => import(/* webpackChunkName: "welcome" */ './components/welcome.vue'))
 
 Vue.component('NavFooter', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/nav-footer.vue'))
 Vue.component('Page', () => import(/* webpackChunkName: "theme" */ './themes/' + siteConfig.theme + '/components/page.vue'))

+ 0 - 335
client/components/admin.vue

@@ -1,335 +0,0 @@
-<template lang='pug'>
-  v-app.admin
-    nav-header(hide-search)
-      template(slot='mid')
-        v-spacer
-        .overline.grey--text {{$t('admin:adminArea')}}
-        v-spacer
-    v-navigation-drawer.pb-0.admin-sidebar(v-model='adminDrawerShown', app, fixed, clipped, :right='$vuetify.rtl', permanent, width='300', :class='$vuetify.theme.dark ? `grey darken-4` : ``')
-      vue-scroll(:ops='scrollStyle')
-        v-list.radius-0(dense, nav)
-          v-list-item(to='/dashboard', color='primary')
-            v-list-item-avatar(size='24', tile): v-icon mdi-view-dashboard-variant
-            v-list-item-title {{ $t('admin:dashboard.title') }}
-          template(v-if='hasPermission([`manage:system`, `manage:navigation`, `write:pages`, `manage:pages`, `delete:pages`])')
-            v-divider.my-2
-            v-subheader.pl-4 {{ $t('admin:nav.site') }}
-            v-list-item(to='/general', color='primary', v-if='hasPermission(`manage:system`)')
-              v-list-item-avatar(size='24', tile): v-icon mdi-widgets
-              v-list-item-title {{ $t('admin:general.title') }}
-            v-list-item(to='/locale', color='primary', v-if='hasPermission(`manage:system`)')
-              v-list-item-avatar(size='24', tile): v-icon mdi-web
-              v-list-item-title {{ $t('admin:locale.title') }}
-            v-list-item(to='/navigation', color='primary', v-if='hasPermission([`manage:system`, `manage:navigation`])')
-              v-list-item-avatar(size='24', tile): v-icon mdi-near-me
-              v-list-item-title {{ $t('admin:navigation.title') }}
-            v-list-item(to='/pages', color='primary', v-if='hasPermission([`manage:system`, `write:pages`, `manage:pages`, `delete:pages`])')
-              v-list-item-avatar(size='24', tile): v-icon mdi-file-document-outline
-              v-list-item-title {{ $t('admin:pages.title') }}
-              v-list-item-action(style='min-width:auto;')
-                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-5`')
-                  .caption.grey--text {{ info.pagesTotal }}
-            v-list-item(to='/tags', v-if='hasPermission([`manage:system`])')
-              v-list-item-avatar(size='24', tile): v-icon mdi-tag-multiple
-              v-list-item-title {{ $t('admin:tags.title') }}
-              v-list-item-action(style='min-width:auto;')
-                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-5`')
-                  .caption.grey--text {{ info.tagsTotal }}
-            v-list-item(to='/theme', color='primary', v-if='hasPermission([`manage:system`, `manage:theme`])')
-              v-list-item-avatar(size='24', tile): v-icon mdi-palette-outline
-              v-list-item-title {{ $t('admin:theme.title') }}
-          template(v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')
-            v-divider.my-2
-            v-subheader.pl-4 {{ $t('admin:nav.users') }}
-            v-list-item(to='/groups', color='primary', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`])')
-              v-list-item-avatar(size='24', tile): v-icon mdi-account-group
-              v-list-item-title {{ $t('admin:groups.title') }}
-              v-list-item-action(style='min-width:auto;')
-                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-4`')
-                  .caption.grey--text {{ info.groupsTotal }}
-            v-list-item(to='/users', color='primary', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')
-              v-list-item-avatar(size='24', tile): v-icon mdi-account-box
-              v-list-item-title {{ $t('admin:users.title') }}
-              v-list-item-action(style='min-width:auto;')
-                v-chip(x-small, :color='$vuetify.theme.dark ? `grey darken-3-d4` : `grey lighten-4`')
-                  .caption.grey--text {{ info.usersTotal }}
-          template(v-if='hasPermission(`manage:system`)')
-            v-divider.my-2
-            v-subheader.pl-4 {{ $t('admin:nav.modules') }}
-            v-list-item(to='/analytics', color='primary')
-              v-list-item-avatar(size='24', tile): v-icon mdi-chart-timeline-variant
-              v-list-item-title {{ $t('admin:analytics.title') }}
-            v-list-item(to='/auth', color='primary')
-              v-list-item-avatar(size='24', tile): v-icon mdi-lock-outline
-              v-list-item-title {{ $t('admin:auth.title') }}
-            v-list-item(to='/comments')
-              v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline
-              v-list-item-title {{ $t('admin:comments.title') }}
-            v-list-item(to='/editor', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-playlist-edit
-              v-list-item-title {{ $t('admin:editor.title') }}
-            v-list-item(to='/extensions')
-              v-list-item-avatar(size='24', tile): v-icon mdi-chip
-              v-list-item-title {{ $t('admin:extensions.title') }}
-            v-list-item(to='/logging', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-script-text-outline
-              v-list-item-title {{ $t('admin:logging.title') }}
-            v-list-item(to='/rendering', color='primary')
-              v-list-item-avatar(size='24', tile): v-icon mdi-cogs
-              v-list-item-title {{ $t('admin:rendering.title') }}
-            v-list-item(to='/search', color='primary')
-              v-list-item-avatar(size='24', tile): v-icon mdi-cloud-search-outline
-              v-list-item-title {{ $t('admin:search.title') }}
-            v-list-item(to='/storage', color='primary')
-              v-list-item-avatar(size='24', tile): v-icon mdi-harddisk
-              v-list-item-title {{ $t('admin:storage.title') }}
-          template(v-if='hasPermission([`manage:system`, `manage:api`])')
-            v-divider.my-2
-            v-subheader.pl-4 {{ $t('admin:nav.system') }}
-            v-list-item(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])')
-              v-list-item-avatar(size='24', tile): v-icon mdi-call-split
-              v-list-item-title {{ $t('admin:api.title') }}
-            v-list-item(to='/mail', color='primary', v-if='hasPermission(`manage:system`)')
-              v-list-item-avatar(size='24', tile): v-icon mdi-email-multiple-outline
-              v-list-item-title {{ $t('admin:mail.title') }}
-            v-list-item(to='/security', v-if='hasPermission(`manage:system`)')
-              v-list-item-avatar(size='24', tile): v-icon mdi-lock-check
-              v-list-item-title {{ $t('admin:security.title') }}
-            v-list-item(to='/ssl', v-if='hasPermission(`manage:system`)')
-              v-list-item-avatar(size='24', tile): v-icon mdi-cloud-lock-outline
-              v-list-item-title {{ $t('admin:ssl.title') }}
-            v-list-item(to='/system', color='primary', v-if='hasPermission(`manage:system`)')
-              v-list-item-avatar(size='24', tile): v-icon mdi-tune
-              v-list-item-title {{ $t('admin:system.title') }}
-            v-list-item(to='/utilities', color='primary', v-if='hasPermission(`manage:system`)')
-              v-list-item-avatar(size='24', tile): v-icon mdi-wrench-outline
-              v-list-item-title {{ $t('admin:utilities.title') }}
-            v-list-item(to='/webhooks', v-if='hasPermission(`manage:system`)', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-webhook
-              v-list-item-title {{ $t('admin:webhooks.title') }}
-            v-list-group(
-              to='/dev'
-              no-action
-              v-if='hasPermission([`manage:system`, `manage:api`])'
-              )
-              v-list-item(slot='activator')
-                v-list-item-avatar(size='24', tile): v-icon mdi-dev-to
-                v-list-item-title {{ $t('admin:dev.title') }}
-
-              v-list-item(to='/dev-flags', color='primary')
-                v-list-item-title {{ $t('admin:dev.flags.title') }}
-              v-list-item(href='/graphql', color='primary')
-                v-list-item-title GraphQL
-              //- v-list-item(to='/dev-graphiql')
-              //-   v-list-item-title {{ $t('admin:dev.graphiql.title') }}
-              //- v-list-item(to='/dev-voyager')
-              //-   v-list-item-title {{ $t('admin:dev.voyager.title') }}
-            v-divider.my-2
-          v-list-item(to='/contribute', color='primary')
-            v-list-item-avatar(size='24', tile): v-icon mdi-heart-outline
-            v-list-item-title {{ $t('admin:contribute.title') }}
-
-    v-main(:class='$vuetify.theme.dark ? "grey darken-5" : "grey lighten-5"')
-      transition(name='admin-router')
-        router-view
-
-    nav-footer
-    notify
-    search-results
-</template>
-
-<script>
-import _ from 'lodash'
-import VueRouter from 'vue-router'
-import { get, sync } from 'vuex-pathify'
-
-import statsQuery from 'gql/admin/dashboard/dashboard-query-stats.gql'
-
-import adminStore from '../store/admin'
-
-/* global WIKI */
-
-WIKI.$store.registerModule('admin', adminStore)
-
-const router = new VueRouter({
-  mode: 'history',
-  base: '/a',
-  routes: [
-    { path: '/', redirect: '/dashboard' },
-    { path: '/dashboard', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-dashboard.vue') },
-    { path: '/general', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-general.vue') },
-    { path: '/locale', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-locale.vue') },
-    { path: '/navigation', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-navigation.vue') },
-    { path: '/pages', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages.vue') },
-    { path: '/pages/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-edit.vue') },
-    { path: '/pages/visualize', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-visualize.vue') },
-    { path: '/tags', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-tags.vue') },
-    { path: '/theme', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-theme.vue') },
-    { path: '/groups', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups.vue') },
-    { path: '/groups/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups-edit.vue') },
-    { path: '/users', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users.vue') },
-    { path: '/users/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users-edit.vue') },
-    { path: '/analytics', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-analytics.vue') },
-    { path: '/auth', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-auth.vue') },
-    { path: '/comments', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-comments.vue') },
-    { path: '/rendering', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-rendering.vue') },
-    { path: '/editor', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-editor.vue') },
-    { path: '/extensions', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-extensions.vue') },
-    { path: '/logging', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-logging.vue') },
-    { path: '/search', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-search.vue') },
-    { path: '/storage', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-storage.vue') },
-    { path: '/api', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-api.vue') },
-    { path: '/mail', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-mail.vue') },
-    { path: '/security', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-security.vue') },
-    { path: '/ssl', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-ssl.vue') },
-    { path: '/system', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-system.vue') },
-    { path: '/utilities', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-utilities.vue') },
-    { path: '/webhooks', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-webhooks.vue') },
-    { path: '/dev-flags', component: () => import(/* webpackChunkName: "admin-dev" */ './admin/admin-dev-flags.vue') },
-    { path: '/contribute', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-contribute.vue') }
-  ]
-})
-
-export default {
-  i18nOptions: { namespaces: 'admin' },
-  data() {
-    return {
-      adminDrawerShown: true,
-      scrollStyle: {
-        vuescroll: {},
-        scrollPanel: {
-          initialScrollY: 0,
-          initialScrollX: 0,
-          scrollingX: false,
-          easing: 'easeOutQuad',
-          speed: 1000,
-          verticalNativeBarPos: this.$vuetify.rtl ? `left` : `right`
-        },
-        rail: {
-          gutterOfEnds: '2px'
-        },
-        bar: {
-          onlyShowBarOnScroll: false,
-          background: '#CCC',
-          hoverStyle: {
-            background: '#999'
-          }
-        }
-      }
-    }
-  },
-  computed: {
-    info: sync('admin/info'),
-    permissions: get('user/permissions')
-  },
-  router,
-  created() {
-    this.$store.commit('page/SET_MODE', 'admin')
-  },
-  methods: {
-    hasPermission(prm) {
-      if (_.isArray(prm)) {
-        return _.some(prm, p => {
-          return _.includes(this.permissions, p)
-        })
-      } else {
-        return _.includes(this.permissions, prm)
-      }
-    }
-  },
-  apollo: {
-    info: {
-      query: statsQuery,
-      fetchPolicy: 'network-only',
-      manual: true,
-      result({ data, loading, networkStatus }) {
-        this.info = data.system.info
-      },
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-stats-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-.admin {
-  &.theme--light .application--wrap {
-    background-color: lighten(mc('grey', '200'), 2%);
-  }
-}
-
-.admin-router {
-  &-enter-active, &-leave-active {
-    transition: opacity .25s ease;
-    opacity: 1;
-  }
-  &-enter-active {
-    transition-delay: .25s;
-  }
-  &-enter, &-leave-to {
-    opacity: 0;
-  }
-}
-
-.admin-sidebar {
-  .v-list__tile--active {
-    background-color: rgba(mc('theme', 'primary'), .1);
-
-    .v-icon {
-      color: mc('theme', 'primary');
-    }
-  }
-
-  .v-list-group > .v-list-item {
-    padding-left: 0;
-  }
-}
-
-.theme--dark {
-  .admin-sidebar .v-list__tile--active {
-    background-color: rgba(0,0,0, .2);
-    color: mc('blue', '500') !important;
-
-    .v-icon {
-      color: mc('blue', '500');
-    }
-  }
-}
-
-.admin-header {
-  display: flex;
-  justify-content: flex-start;
-  align-items: center;
-
-  &-title {
-    margin-left: 1rem;
-  }
-}
-
-.admin-providerlogo {
-  width: 250px;
-  height: 50px;
-  float: right;
-  display: flex;
-  justify-content: flex-end;
-  align-items: center;
-  margin-left: 16px;
-
-  img {
-    max-width: 100%;
-    max-height: 50px;
-  }
-}
-
-.v-application.admin {
-  code {
-    box-shadow: none;
-    font-family: 'Roboto Mono', monospace;
-    color: mc('pink', '500');
-  }
-}
-
-</style>

+ 0 - 181
client/components/admin/admin-analytics.vue

@@ -1,181 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-line-chart.svg', alt='Analytics', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:analytics.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:analytics.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')
-            v-icon mdi-refresh
-          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-
-      v-flex(lg3, xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(flat, color='primary', dark, dense)
-            .subtitle-1 {{$t('admin:analytics.providers')}}
-          v-list(two-line, dense).py-0
-            template(v-for='(str, idx) in providers')
-              v-list-item(:key='str.key', @click='selectedProvider = str.key', :disabled='!str.isAvailable')
-                v-list-item-avatar(size='24')
-                  v-icon(color='grey', v-if='!str.isAvailable') mdi-minus-box-outline
-                  v-icon(color='primary', v-else-if='str.isEnabled', v-ripple, @click='str.isEnabled = false') mdi-checkbox-marked-outline
-                  v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') mdi-checkbox-blank-outline
-                v-list-item-content
-                  v-list-item-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedProvider === str.key ? `primary--text` : ``)') {{ str.title }}
-                  v-list-item-subtitle: .caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedProvider === str.key ? `blue--text ` : ``)') {{ str.description }}
-                v-list-item-avatar(v-if='selectedProvider === str.key', size='24')
-                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
-              v-divider(v-if='idx < providers.length - 1')
-
-      v-flex(xs12, lg9)
-
-        v-card.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, flat, dark)
-            .subtitle-1 {{provider.title}}
-            v-spacer
-            v-switch(
-              dark
-              color='blue lighten-5'
-              label='Active'
-              v-model='provider.isEnabled'
-              hide-details
-              inset
-              )
-          v-card-info(color='blue')
-            div
-              div {{provider.description}}
-              span.caption: a(:href='provider.website') {{provider.website}}
-            v-spacer
-            .admin-providerlogo
-              img(:src='provider.logo', :alt='provider.title')
-          v-card-text
-            v-form
-              .overline.pb-5 {{$t('admin:analytics.providerConfiguration')}}
-              .body-1.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:analytics.providerNoConfiguration')}}
-              template(v-else, v-for='cfg in provider.config')
-                v-select(
-                  v-if='cfg.value.type === "string" && cfg.value.enum'
-                  outlined
-                  :items='cfg.value.enum'
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  :class='cfg.value.hint ? "mb-2" : ""'
-                )
-                v-switch.mb-3(
-                  v-else-if='cfg.value.type === "boolean"'
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  color='primary'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  inset
-                  )
-                v-textarea(
-                  v-else-if='cfg.value.type === "string" && cfg.value.multiline'
-                  outlined
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  :class='cfg.value.hint ? "mb-2" : ""'
-                  )
-                v-text-field(
-                  v-else
-                  outlined
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  :class='cfg.value.hint ? "mb-2" : ""'
-                  )
-
-</template>
-
-<script>
-import _ from 'lodash'
-
-import providersQuery from 'gql/admin/analytics/analytics-query-providers.gql'
-import providersSaveMutation from 'gql/admin/analytics/analytics-mutation-save-providers.gql'
-
-export default {
-  data() {
-    return {
-      providers: [],
-      selectedProvider: '',
-      provider: {}
-    }
-  },
-  watch: {
-    selectedProvider(newValue, oldValue) {
-      this.provider = _.find(this.providers, ['key', newValue]) || {}
-    },
-    providers(newValue, oldValue) {
-      this.selectedProvider = 'google'
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.providers.refetch()
-      this.$store.commit('showNotification', {
-        message: this.$t('admin:analytics.refreshSuccess'),
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save() {
-      this.$store.commit(`loadingStart`, 'admin-analytics-saveproviders')
-      try {
-        await this.$apollo.mutate({
-          mutation: providersSaveMutation,
-          variables: {
-            providers: this.providers.map(str => _.pick(str, [
-              'isEnabled',
-              'key',
-              'config'
-            ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
-          }
-        })
-        this.$store.commit('showNotification', {
-          message: this.$t('admin:analytics.saveSuccess'),
-          style: 'success',
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-analytics-saveproviders')
-    }
-  },
-  apollo: {
-    providers: {
-      query: providersQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.analytics.providers).map(str => ({
-        ...str,
-        config: _.sortBy(str.config.map(cfg => ({
-          ...cfg,
-          value: JSON.parse(cfg.value)
-        })), [t => t.value.order])
-      })),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-analytics-refresh')
-      }
-    }
-  }
-}
-</script>

+ 0 - 236
client/components/admin/admin-api-create.vue

@@ -1,236 +0,0 @@
-<template lang="pug">
-  div
-    v-dialog(v-model='isShown', max-width='650', persistent)
-      v-card
-        .dialog-header.is-short
-          v-icon.mr-3(color='white') mdi-plus
-          span {{$t('admin:api.newKeyTitle')}}
-        v-card-text.pt-5
-          v-text-field(
-            outlined
-            prepend-icon='mdi-format-title'
-            v-model='name'
-            :label='$t(`admin:api.newKeyName`)'
-            persistent-hint
-            ref='keyNameInput'
-            :hint='$t(`admin:api.newKeyNameHint`)'
-            counter='255'
-            )
-          v-select.mt-3(
-            :items='expirations'
-            outlined
-            prepend-icon='mdi-clock'
-            v-model='expiration'
-            :label='$t(`admin:api.newKeyExpiration`)'
-            :hint='$t(`admin:api.newKeyExpirationHint`)'
-            persistent-hint
-            )
-          v-divider.mt-4
-          v-subheader.pl-2: strong.indigo--text {{$t('admin:api.newKeyPermissionScopes')}}
-          v-list.pl-8(nav)
-            v-list-item-group(v-model='fullAccess')
-              v-list-item(
-                :value='true'
-                active-class='indigo--text'
-                )
-                template(v-slot:default='{ active, toggle }')
-                  v-list-item-action
-                    v-checkbox(
-                      :input-value='active'
-                      :true-value='true'
-                      color='indigo'
-                      @click='toggle'
-                    )
-                  v-list-item-content
-                    v-list-item-title {{$t('admin:api.newKeyFullAccess')}}
-            v-divider.mt-3
-            v-subheader.caption.indigo--text {{$t('admin:api.newKeyGroupPermissions')}}
-            v-list-item
-              v-select(
-                :disabled='fullAccess'
-                :items='groups'
-                item-text='name'
-                item-value='id'
-                outlined
-                color='indigo'
-                v-model='group'
-                :label='$t(`admin:api.newKeyGroup`)'
-                :hint='$t(`admin:api.newKeyGroupHint`)'
-                persistent-hint
-                )
-        v-card-chin
-          v-spacer
-          v-btn(text, @click='isShown = false', :disabled='loading') {{$t('common:actions.cancel')}}
-          v-btn.px-3(depressed, color='primary', @click='generate', :loading='loading')
-            v-icon(left) mdi-chevron-right
-            span {{$t('common:actions.generate')}}
-
-    v-dialog(
-      v-model='isCopyKeyDialogShown'
-      max-width='750'
-      persistent
-      overlay-color='blue darken-5'
-      overlay-opacity='.9'
-      )
-      v-card
-        v-toolbar(dense, flat, color='primary', dark) {{$t('admin:api.newKeyTitle')}}
-        v-card-text.pt-5
-          .body-2.text-center
-            i18next(tag='span', path='admin:api.newKeyCopyWarn')
-              strong(place='bold') {{$t('admin:api.newKeyCopyWarnBold')}}
-          v-textarea.mt-3(
-            ref='keyContentsIpt'
-            filled
-            no-resize
-            readonly
-            v-model='key'
-            :rows='10'
-            hide-details
-          )
-        v-card-chin
-          v-spacer
-          v-btn.px-3(depressed, dark, color='primary', @click='isCopyKeyDialogShown = false') {{$t('common:actions.close')}}
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-
-import groupsQuery from 'gql/admin/users/users-query-groups.gql'
-
-export default {
-  props: {
-    value: {
-      type: Boolean,
-      default: false
-    }
-  },
-  data() {
-    return {
-      loading: false,
-      name: '',
-      expiration: '1y',
-      fullAccess: true,
-      groups: [],
-      group: null,
-      isCopyKeyDialogShown: false,
-      key: ''
-    }
-  },
-  computed: {
-    isShown: {
-      get() { return this.value },
-      set(val) { this.$emit('input', val) }
-    },
-    expirations() {
-      return [
-        { value: '30d', text: this.$t('admin:api.expiration30d') },
-        { value: '90d', text: this.$t('admin:api.expiration90d') },
-        { value: '180d', text: this.$t('admin:api.expiration180d') },
-        { value: '1y', text: this.$t('admin:api.expiration1y') },
-        { value: '3y', text: this.$t('admin:api.expiration3y') }
-      ]
-    }
-  },
-  watch: {
-    value (newValue, oldValue) {
-      if (newValue) {
-        setTimeout(() => {
-          this.$refs.keyNameInput.focus()
-        }, 400)
-      }
-    }
-  },
-  methods: {
-    async generate () {
-      try {
-        if (_.trim(this.name).length < 2 || this.name.length > 255) {
-          throw new Error(this.$t('admin:api.newKeyNameError'))
-        } else if (!this.fullAccess && !this.group) {
-          throw new Error(this.$t('admin:api.newKeyGroupError'))
-        } else if (!this.fullAccess && this.group === 2) {
-          throw new Error(this.$t('admin:api.newKeyGuestGroupError'))
-        }
-      } catch (err) {
-        return this.$store.commit('showNotification', {
-          style: 'red',
-          message: err,
-          icon: 'alert'
-        })
-      }
-
-      this.loading = true
-
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($name: String!, $expiration: String!, $fullAccess: Boolean!, $group: Int) {
-              authentication {
-                createApiKey (name: $name, expiration: $expiration, fullAccess: $fullAccess, group: $group) {
-                  key
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            name: this.name,
-            expiration: this.expiration,
-            fullAccess: (this.fullAccess === true),
-            group: this.group
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-create')
-          }
-        })
-        if (_.get(resp, 'data.authentication.createApiKey.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.$t('admin:api.newKeySuccess'),
-            icon: 'check'
-          })
-
-          this.name = ''
-          this.expiration = '1y'
-          this.fullAccess = true
-          this.group = null
-          this.isShown = false
-          this.$emit('refresh')
-
-          this.key = _.get(resp, 'data.authentication.createApiKey.key', '???')
-          this.isCopyKeyDialogShown = true
-
-          setTimeout(() => {
-            this.$refs.keyContentsIpt.$refs.input.select()
-          }, 400)
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _.get(resp, 'data.authentication.createApiKey.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'alert'
-          })
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.loading = false
-    }
-  },
-  apollo: {
-    groups: {
-      query: groupsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.groups.list,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-groups-refresh')
-      }
-    }
-  }
-}
-</script>

+ 0 - 239
client/components/admin/admin-api.vue

@@ -1,239 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-rest-api.svg', alt='API', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('admin:api.title')}}
-            .subtitle-1.grey--text.animated.fadeInLeft {{$t('admin:api.subtitle')}}
-          v-spacer
-          template(v-if='enabled')
-            status-indicator.mr-3(positive, pulse)
-            .caption.green--text.animated.fadeInLeft {{$t('admin:api.enabled')}}
-          template(v-else)
-            status-indicator.mr-3(negative, pulse)
-            .caption.red--text.animated.fadeInLeft {{$t('admin:api.disabled')}}
-          v-spacer
-          v-btn.mr-3.animated.fadeInDown.wait-p2s(outlined, color='grey', icon, @click='refresh')
-            v-icon mdi-refresh
-          v-btn.mr-3.animated.fadeInDown.wait-p1s(:color='enabled ? `red` : `green`', depressed, @click='globalSwitch', dark, :loading='isToggleLoading')
-            v-icon(left) mdi-power
-            span(v-if='!enabled') {{$t('admin:api.enableButton')}}
-            span(v-else) {{$t('admin:api.disableButton')}}
-          v-btn.animated.fadeInDown(color='primary', depressed, large, @click='newKey', dark)
-            v-icon(left) mdi-plus
-            span {{$t('admin:api.newKeyButton')}}
-        v-card.mt-3.animated.fadeInUp
-          v-simple-table(v-if='keys && keys.length > 0')
-            template(v-slot:default)
-              thead
-                tr.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-5`')
-                  th {{$t('admin:api.headerName')}}
-                  th {{$t('admin:api.headerKeyEnding')}}
-                  th {{$t('admin:api.headerExpiration')}}
-                  th {{$t('admin:api.headerCreated')}}
-                  th {{$t('admin:api.headerLastUpdated')}}
-                  th(width='100') {{$t('admin:api.headerRevoke')}}
-              tbody
-                tr(v-for='key of keys', :key='`key-` + key.id')
-                  td
-                    strong(:class='key.isRevoked ? `red--text` : ``') {{ key.name }}
-                    em.caption.ml-1.red--text(v-if='key.isRevoked') (revoked)
-                  td.caption {{ key.keyShort }}
-                  td(:style='key.isRevoked ? `text-decoration: line-through;` : ``') {{ key.expiration | moment('LL') }}
-                  td {{ key.createdAt | moment('calendar') }}
-                  td {{ key.updatedAt | moment('calendar') }}
-                  td: v-btn(icon, @click='revoke(key)', :disabled='key.isRevoked'): v-icon(color='error') mdi-cancel
-          v-card-text(v-else)
-            v-alert.mb-0(icon='mdi-information', :value='true', outlined, color='info') {{$t('admin:api.noKeyInfo')}}
-
-    create-api-key(v-model='isCreateDialogShown', @refresh='refresh(false)')
-
-    v-dialog(v-model='isRevokeConfirmDialogShown', max-width='500', persistent)
-      v-card
-        .dialog-header.is-red {{$t('admin:api.revokeConfirm')}}
-        v-card-text.pa-4
-          i18next(tag='span', path='admin:api.revokeConfirmText')
-            strong(place='name') {{ current.name }}
-        v-card-actions
-          v-spacer
-          v-btn(text, @click='isRevokeConfirmDialogShown = false', :disabled='revokeLoading') {{$t('common:actions.cancel')}}
-          v-btn(color='red', dark, @click='revokeConfirm', :loading='revokeLoading') {{$t('admin:api.revoke')}}
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-import { StatusIndicator } from 'vue-status-indicator'
-
-import CreateApiKey from './admin-api-create.vue'
-
-export default {
-  components: {
-    StatusIndicator,
-    CreateApiKey
-  },
-  data() {
-    return {
-      enabled: false,
-      isToggleLoading: false,
-      keys: [],
-      isCreateDialogShown: false,
-      isRevokeConfirmDialogShown: false,
-      revokeLoading: false,
-      current: {}
-    }
-  },
-  methods: {
-    async refresh (notify = true) {
-      this.$apollo.queries.keys.refetch()
-      if (notify) {
-        this.$store.commit('showNotification', {
-          message: this.$t('admin:api.refreshSuccess'),
-          style: 'success',
-          icon: 'cached'
-        })
-      }
-    },
-    async globalSwitch () {
-      this.isToggleLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($enabled: Boolean!) {
-              authentication {
-                setApiState (enabled: $enabled) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            enabled: !this.enabled
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-toggle')
-          }
-        })
-        if (_.get(resp, 'data.authentication.setApiState.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.enabled ? this.$t('admin:api.toggleStateDisabledSuccess') : this.$t('admin:api.toggleStateEnabledSuccess'),
-            icon: 'check'
-          })
-          await this.$apollo.queries.enabled.refetch()
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _.get(resp, 'data.authentication.setApiState.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'alert'
-          })
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.isToggleLoading = false
-    },
-    async newKey () {
-      this.isCreateDialogShown = true
-    },
-    revoke (key) {
-      this.current = key
-      this.isRevokeConfirmDialogShown = true
-    },
-    async revokeConfirm () {
-      this.revokeLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($id: Int!) {
-              authentication {
-                revokeApiKey (id: $id) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            id: this.current.id
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-revoke')
-          }
-        })
-        if (_.get(resp, 'data.authentication.revokeApiKey.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.$t('admin:api.revokeSuccess'),
-            icon: 'check'
-          })
-          this.refresh(false)
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _.get(resp, 'data.authentication.revokeApiKey.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'alert'
-          })
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.isRevokeConfirmDialogShown = false
-      this.revokeLoading = false
-    }
-  },
-  apollo: {
-    enabled: {
-      query: gql`
-        {
-          authentication {
-            apiState
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => data.authentication.apiState,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-state-refresh')
-      }
-    },
-    keys: {
-      query: gql`
-        {
-          authentication {
-            apiKeys {
-              id
-              name
-              keyShort
-              expiration
-              isRevoked
-              createdAt
-              updatedAt
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => data.authentication.apiKeys,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-api-keys-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

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

@@ -1,433 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-unlock.svg', alt='Authentication', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:auth.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:auth.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/auth', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.animated.fadeInDown.wait-p2s.mx-3(icon, outlined, color='grey', @click='refresh')
-            v-icon mdi-refresh
-          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-
-      v-flex(lg3, xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(flat, color='teal', dark, dense)
-            .subtitle-1 {{$t('admin:auth.activeStrategies')}}
-          v-list(two-line, dense).py-0
-            draggable(
-              v-model='activeStrategies'
-              handle='.is-handle'
-              direction='vertical'
-              )
-              transition-group
-                v-list-item(
-                  v-for='(str, idx) in activeStrategies'
-                  :key='str.key'
-                  @click='selectedStrategy = str.key'
-                  :class='selectedStrategy === str.key ? ($vuetify.theme.dark ? `grey darken-5` : `teal lighten-5`) : ``'
-                  )
-                  v-list-item-avatar.is-handle(size='24')
-                    v-icon(:color='selectedStrategy === str.key ? `teal` : `grey`') mdi-drag-horizontal
-                  v-list-item-content
-                    v-list-item-title.body-2(:class='selectedStrategy === str.key ? `teal--text` : ``') {{ str.displayName }}
-                    v-list-item-subtitle: .caption(:class='selectedStrategy === str.key ? `teal--text ` : ``') {{ str.strategy.title }}
-                  v-list-item-avatar(v-if='selectedStrategy === str.key', size='24')
-                    v-icon.animated.fadeInLeft(color='teal', large) mdi-chevron-right
-          v-card-chin
-            v-menu(offset-y, bottom, min-width='250px', max-width='550px', max-height='50vh', style='flex: 1 1;', center)
-              template(v-slot:activator='{ on }')
-                v-btn(v-on='on', color='primary', depressed, block)
-                  v-icon(left) mdi-plus
-                  span {{$t('admin:auth.addStrategy')}}
-              v-list(dense)
-                template(v-for='(str, idx) of strategies')
-                  v-list-item(
-                    :key='str.key'
-                    :disabled='str.isDisabled'
-                    @click='addStrategy(str)'
-                    )
-                    v-list-item-avatar(height='24', width='48', tile)
-                      v-img(:src='str.logo', width='48px', height='24px', contain, :style='str.isDisabled ? `opacity: .25;` : ``')
-                    v-list-item-content
-                      v-list-item-title {{str.title}}
-                      v-list-item-subtitle: .caption(:style='str.isDisabled ? `opacity: .4;` : ``') {{str.description}}
-                  v-divider(v-if='idx < strategies.length - 1')
-
-      v-flex(xs12, lg9)
-        v-card.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, flat, dark)
-            .subtitle-1 {{strategy.displayName}} #[em ({{strategy.strategy.title}})]
-            v-spacer
-            v-btn(small, outlined, dark, color='white', :disabled='strategy.key === `local`', @click='deleteStrategy()')
-              v-icon(left) mdi-close
-              span {{$t('common:actions.delete')}}
-          v-card-info(color='blue')
-            div
-              span {{strategy.strategy.description}}
-              .caption: a(:href='strategy.strategy.website') {{strategy.strategy.website}}
-            v-spacer
-            .admin-providerlogo
-              img(:src='strategy.strategy.logo', :alt='strategy.strategy.title')
-          v-card-text
-            .row
-              .col-8
-                v-text-field(
-                  outlined
-                  :label='$t(`admin:auth.displayName`)'
-                  v-model='strategy.displayName'
-                  prepend-icon='mdi-format-title'
-                  :hint='$t(`admin:auth.displayNameHint`)'
-                  persistent-hint
-                  )
-              .col-4
-                v-switch.mt-1(
-                  :label='$t(`admin:auth.strategyIsEnabled`)'
-                  v-model='strategy.isEnabled'
-                  color='primary'
-                  prepend-icon='mdi-power'
-                  :hint='$t(`admin:auth.strategyIsEnabledHint`)'
-                  persistent-hint
-                  inset
-                  :disabled='strategy.key === `local`'
-                  )
-            template(v-if='strategy.config && Object.keys(strategy.config).length > 0')
-              v-divider
-              .overline.my-5 {{$t('admin:auth.strategyConfiguration')}}
-              .pr-3
-                template(v-for='cfg in strategy.config')
-                  v-select.mb-3(
-                    v-if='cfg.value.type === "string" && cfg.value.enum'
-                    outlined
-                    :items='cfg.value.enum'
-                    :key='cfg.key'
-                    :label='cfg.value.title'
-                    v-model='cfg.value.value'
-                    prepend-icon='mdi-cog-box'
-                    :hint='cfg.value.hint ? cfg.value.hint : ""'
-                    persistent-hint
-                    :class='cfg.value.hint ? "mb-2" : ""'
-                    :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
-                  )
-                  v-switch.mb-6(
-                    v-else-if='cfg.value.type === "boolean"'
-                    :key='cfg.key'
-                    :label='cfg.value.title'
-                    v-model='cfg.value.value'
-                    color='primary'
-                    prepend-icon='mdi-cog-box'
-                    :hint='cfg.value.hint ? cfg.value.hint : ""'
-                    persistent-hint
-                    inset
-                    )
-                  v-textarea.mb-3(
-                    v-else-if='cfg.value.type === "string" && cfg.value.multiline'
-                    outlined
-                    :key='cfg.key'
-                    :label='cfg.value.title'
-                    v-model='cfg.value.value'
-                    prepend-icon='mdi-cog-box'
-                    :hint='cfg.value.hint ? cfg.value.hint : ""'
-                    persistent-hint
-                    :class='cfg.value.hint ? "mb-2" : ""'
-                    )
-                  v-text-field.mb-3(
-                    v-else
-                    outlined
-                    :key='cfg.key'
-                    :label='cfg.value.title'
-                    v-model='cfg.value.value'
-                    prepend-icon='mdi-cog-box'
-                    :hint='cfg.value.hint ? cfg.value.hint : ""'
-                    persistent-hint
-                    :class='cfg.value.hint ? "mb-2" : ""'
-                    :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
-                    )
-            v-divider
-            .overline.my-5 {{$t('admin:auth.registration')}}
-            .pr-3
-              v-switch.ml-3(
-                v-model='strategy.selfRegistration'
-                :label='$t(`admin:auth.selfRegistration`)'
-                color='primary'
-                :hint='$t(`admin:auth.selfRegistrationHint`)'
-                persistent-hint
-                inset
-              )
-              v-combobox.ml-3.mt-5(
-                :label='$t(`admin:auth.domainsWhitelist`)'
-                v-model='strategy.domainWhitelist'
-                prepend-icon='mdi-email-check-outline'
-                outlined
-                :disabled='!strategy.selfRegistration'
-                :hint='$t(`admin:auth.domainsWhitelistHint`)'
-                persistent-hint
-                small-chips
-                deletable-chips
-                clearable
-                multiple
-                chips
-                )
-              v-autocomplete.mt-3.ml-3(
-                outlined
-                :disabled='!strategy.selfRegistration'
-                :items='groups'
-                item-text='name'
-                item-value='id'
-                :label='$t(`admin:auth.autoEnrollGroups`)'
-                v-model='strategy.autoEnrollGroups'
-                prepend-icon='mdi-account-group'
-                :hint='$t(`admin:auth.autoEnrollGroupsHint`)'
-                small-chips
-                persistent-hint
-                deletable-chips
-                clearable
-                multiple
-                chips
-                )
-
-        v-card.mt-4.wiki-form.animated.fadeInUp.wait-p4s(v-if='selectedStrategy !== `local`')
-          v-toolbar(color='primary', dense, flat, dark)
-            .subtitle-1 {{$t('admin:auth.configReference')}}
-          v-card-text
-            .body-2 {{$t('admin:auth.configReferenceSubtitle')}}
-            v-alert.mt-3.radius-7(v-if='host.length < 8', color='red', outlined, :value='true', icon='mdi-alert')
-              i18next(path='admin:auth.siteUrlNotSetup', tag='span')
-                strong(place='siteUrl') {{$t('admin:general.siteUrl')}}
-                strong(place='general') {{$t('admin:general.title')}}
-            .pa-3.mt-3.radius-7.grey(v-else, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`')
-              .body-2: strong {{$t('admin:auth.allowedWebOrigins')}}
-              .body-2 {{host}}
-              v-divider.my-3
-              .body-2: strong {{$t('admin:auth.callbackUrl')}}
-              .body-2 {{host}}/login/{{strategy.key}}/callback
-              v-divider.my-3
-              .body-2: strong {{$t('admin:auth.loginUrl')}}
-              .body-2 {{host}}/login
-              v-divider.my-3
-              .body-2: strong {{$t('admin:auth.logoutUrl')}}
-              .body-2 {{host}}
-              v-divider.my-3
-              .body-2: strong {{$t('admin:auth.tokenEndpointAuthMethod')}}
-              .body-2 HTTP-POST
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-import { v4 as uuid } from 'uuid'
-
-import groupsQuery from 'gql/admin/auth/auth-query-groups.gql'
-import hostQuery from 'gql/admin/auth/auth-query-host.gql'
-
-import draggable from 'vuedraggable'
-
-export default {
-  components: {
-    draggable
-  },
-  filters: {
-    startCase(val) { return _.startCase(val) }
-  },
-  data() {
-    return {
-      groups: [],
-      strategies: [],
-      activeStrategies: [],
-      selectedStrategy: '',
-      host: '',
-      strategy: {
-        strategy: {}
-      }
-    }
-  },
-  watch: {
-    selectedStrategy(newValue, oldValue) {
-      this.strategy = _.find(this.activeStrategies, ['key', newValue]) || {}
-    },
-    activeStrategies(newValue, oldValue) {
-      this.selectedStrategy = 'local'
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.strategies.refetch()
-      await this.$apollo.queries.activeStrategies.refetch()
-      this.$store.commit('showNotification', {
-        message: this.$t('admin:auth.refreshSuccess'),
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    addStrategy (str) {
-      const newStr = {
-        key: uuid(),
-        strategy: str,
-        config: str.props.map(c => ({
-          key: c.key,
-          value: {
-            ...c,
-            value: c.default
-          }
-        })),
-        order: this.activeStrategies.length,
-        isEnabled: true,
-        displayName: str.title,
-        selfRegistration: false,
-        domainWhitelist: [],
-        autoEnrollGroups: []
-      }
-      this.activeStrategies = [...this.activeStrategies, newStr]
-      this.$nextTick(() => {
-        this.selectedStrategy = newStr.key
-      })
-    },
-    deleteStrategy () {
-      this.activeStrategies = _.reject(this.activeStrategies, ['key', this.strategy.key])
-    },
-    async save() {
-      this.$store.commit(`loadingStart`, 'admin-auth-savestrategies')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation($strategies: [AuthenticationStrategyInput]!) {
-              authentication {
-                updateStrategies(strategies: $strategies) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            strategies: this.activeStrategies.map((str, idx) => ({
-              key: str.key,
-              strategyKey: str.strategy.key,
-              displayName: str.displayName,
-              order: idx,
-              isEnabled: str.isEnabled,
-              config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })})),
-              selfRegistration: str.selfRegistration,
-              domainWhitelist: str.domainWhitelist,
-              autoEnrollGroups: str.autoEnrollGroups
-            }))
-          }
-        })
-        if (_.get(resp, 'data.authentication.updateStrategies.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            message: this.$t('admin:auth.saveSuccess'),
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(_.get(resp, 'data.authentication.updateStrategies.responseResult.message', this.$t('common:error.unexpected')))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-auth-savestrategies')
-    }
-  },
-  apollo: {
-    strategies: {
-      query: gql`
-        query {
-          authentication {
-            strategies {
-              key
-              title
-              description
-              isAvailable
-              useForm
-              logo
-              website
-              props {
-                key
-                value
-              }
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.get(data, 'authentication.strategies', []).map(str => ({
-        ...str,
-        isDisabled: !str.isAvailable || str.key === `local`,
-        props: _.sortBy(str.props.map(cfg => ({
-          key: cfg.key,
-          ...JSON.parse(cfg.value)
-        })), [t => t.order])
-      })),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-strategies-refresh')
-      }
-    },
-    activeStrategies: {
-      query: gql`
-        query {
-          authentication {
-            activeStrategies {
-              key
-              strategy {
-                key
-                title
-                description
-                useForm
-                logo
-                website
-              }
-              config {
-                key
-                value
-              }
-              order
-              isEnabled
-              displayName
-              selfRegistration
-              domainWhitelist
-              autoEnrollGroups
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.sortBy(_.get(data, 'authentication.activeStrategies', []).map(str => ({
-        ...str,
-        config: _.sortBy(str.config.map(cfg => ({
-          ...cfg,
-          value: JSON.parse(cfg.value)
-        })), [t => t.value.order])
-      })), ['order']),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-activestrategies-refresh')
-      }
-    },
-    groups: {
-      query: groupsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.groups.list,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-groups-refresh')
-      }
-    },
-    host: {
-      query: hostQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.site.config.host),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-host-refresh')
-      }
-    }
-  }
-}
-</script>

+ 0 - 206
client/components/admin/admin-comments.vue

@@ -1,206 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-chat-bubble.svg', alt='Comments', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('admin:comments.title')}}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:comments.subtitle')}}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/comments', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
-            v-icon mdi-refresh
-          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-
-      v-flex(lg3, xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(flat, color='primary', dark, dense)
-            .subtitle-1 {{$t('admin:comments.provider')}}
-          v-list.py-0(two-line, dense)
-            template(v-for='(provider, idx) in providers')
-              v-list-item(:key='provider.key', @click='selectedProvider = provider.key', :disabled='!provider.isAvailable')
-                v-list-item-avatar(size='24')
-                  v-icon(color='grey', v-if='!provider.isAvailable') mdi-minus-box-outline
-                  v-icon(color='primary', v-else-if='provider.key === selectedProvider') mdi-checkbox-marked-circle-outline
-                  v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline
-                v-list-item-content
-                  v-list-item-title.body-2(:class='!provider.isAvailable ? `grey--text` : (selectedProvider === provider.key ? `primary--text` : ``)') {{ provider.title }}
-                  v-list-item-subtitle: .caption(:class='!provider.isAvailable ? `grey--text text--lighten-1` : (selectedProvider === provider.key ? `blue--text ` : ``)') {{ provider.description }}
-                v-list-item-avatar(v-if='selectedProvider === provider.key', size='24')
-                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
-              v-divider(v-if='idx < providers.length - 1')
-
-      v-flex(lg9, xs12)
-        v-card.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, flat, dark)
-            .subtitle-1 {{provider.title}}
-          v-card-info(color='blue')
-            div
-              div {{provider.description}}
-              span.caption: a(:href='provider.website') {{provider.website}}
-            v-spacer
-            .admin-providerlogo
-              img(:src='provider.logo', :alt='provider.title')
-          v-card-text
-            .overline.my-5 {{$t('admin:comments.providerConfig')}}
-            .body-2.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:comments.providerNoConfig')}}
-            template(v-else, v-for='cfg in provider.config')
-              v-select.mb-3(
-                v-if='cfg.value.type === "string" && cfg.value.enum'
-                outlined
-                :items='cfg.value.enum'
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-                :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
-              )
-              v-switch.mb-6(
-                v-else-if='cfg.value.type === "boolean"'
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                color='primary'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                inset
-                )
-              v-textarea.mb-3(
-                v-else-if='cfg.value.type === "string" && cfg.value.multiline'
-                outlined
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-                )
-              v-text-field.mb-3(
-                v-else
-                outlined
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-                :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``'
-                )
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-
-export default {
-  data() {
-    return {
-      providers: [],
-      selectedProvider: '',
-      provider: {}
-    }
-  },
-  watch: {
-    selectedProvider(newValue, oldValue) {
-      this.provider = _.find(this.providers, ['key', newValue]) || {}
-    },
-    providers(newValue, oldValue) {
-      this.selectedProvider = _.get(_.find(this.providers, 'isEnabled'), 'key', 'db')
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.providers.refetch()
-      this.$store.commit('showNotification', {
-        message: this.$t('admin:comments.listRefreshSuccess'),
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save() {
-      this.$store.commit(`loadingStart`, 'admin-comments-saveproviders')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation($providers: [CommentProviderInput]!) {
-              comments {
-                updateProviders(providers: $providers) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            providers: this.providers.map(tgt => ({
-              isEnabled: tgt.key === this.selectedProvider,
-              key: tgt.key,
-              config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))
-            }))
-          }
-        })
-        if (_.get(resp, 'data.comments.updateProviders.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            message: this.$t('admin:comments.configSaveSuccess'),
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(_.get(resp, 'data.comments.updateProviders.responseResult.message', this.$t('common:error.unexpected')))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-comments-saveproviders')
-    }
-  },
-  apollo: {
-    providers: {
-      query: gql`
-        query {
-          comments {
-            providers {
-              isEnabled
-              key
-              title
-              description
-              logo
-              website
-              isAvailable
-              config {
-                key
-                value
-              }
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.comments.providers).map(str => ({
-        ...str,
-        config: _.sortBy(str.config.map(cfg => ({
-          ...cfg,
-          value: JSON.parse(cfg.value)
-        })), [t => t.value.order])
-      })),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-comments-refresh')
-      }
-    }
-  }
-}
-</script>

+ 0 - 256
client/components/admin/admin-contribute.vue

@@ -1,256 +0,0 @@
-<template lang='pug'>
-  v-container.admin-contribute(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:contribute.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:contribute.subtitle') }}
-        v-card.mt-3.animated.fadeInUp
-          v-card-text
-            i18next.body-2.pl-3(path='admin:contribute.openSource', tag='div')
-              v-icon(color='red') mdi-heart
-              a(href='https://requarks.io', target='_blank') requarks.io
-              a(href='https://github.com/Requarks/wiki/graphs/contributors', target='_blank') {{ $t('admin:contribute.openSourceContributors') }}
-            .body-2.pt-3.pl-3 {{ $t('admin:contribute.needYourHelp') }}
-            v-divider.mt-3
-            v-subheader.subtitle-2 {{ $t('admin:contribute.fundOurWork') }}
-            v-tabs.mx-3.radius-7.admin-contribute-tabs(
-              centered
-              fixed-tabs
-              background-color='primary'
-              color='white'
-              dark
-              slider-color='#FFF'
-              icons-and-text
-              )
-              v-tab
-                span GitHub
-                v-icon.my-1(size='24') mdi-github
-              v-tab
-                span Patreon
-                img.my-1(src='/_assets/svg/icon-patreon.svg', style='height: 24px;')
-              v-tab
-                span OpenCollective
-                img.my-1(src='/_assets/svg/icon-opencollective.svg', style='height: 24px;')
-              v-tab
-                span PayPal
-                img.my-1(src='/_assets/svg/icon-paypal.svg', style='height: 24px;')
-              v-tab
-                span Ethereum
-                img.my-1(src='/_assets/svg/icon-ethereum.svg', style='height: 24px;')
-              v-tab
-                span T-Shirts
-                img.my-1(src='/_assets/svg/icon-t-shirt.svg', style='height: 24px;')
-              v-tab-item(:transition='false', :reverse-transition='false')
-                .body-2.pa-3 {{ $t('admin:contribute.github') }}
-                a.ml-3(href='https://github.com/users/NGPixel/sponsorship', :title='$t(`admin:contribute.becomeASponsor`)')
-                  img(src='/_assets/img/donate_github.svg', :alt='$t(`admin:contribute.becomeASponsor`)' style='width:200px;')
-              v-tab-item(:transition='false', :reverse-transition='false')
-                .body-2.pa-3 {{ $t('admin:contribute.patreon') }}
-                a.ml-3(href='https://www.patreon.com/bePatron?u=16744039', :title='$t(`admin:contribute.becomeAPatron`)')
-                  img(src='/_assets/img/donate_patreon.png', :alt='$t(`admin:contribute.becomeAPatron`)' style='width:200px;')
-              v-tab-item(:transition='false', :reverse-transition='false')
-                .body-2.pa-3 {{ $t('admin:contribute.openCollective') }}
-                a.ml-3(href='https://opencollective.com/wikijs/donate', :title='$t(`admin:contribute.makeADonation`)')
-                  img(src='/_assets/img/donate_opencollective.png', :alt='$t(`admin:contribute.makeADonation`)' style='width:300px;')
-              v-tab-item(:transition='false', :reverse-transition='false')
-                .body-2.pa-3 {{ $t('admin:contribute.paypal') }}
-                .ml-3
-                  form(action='https://www.paypal.com/cgi-bin/webscr', method='post', target='_top')
-                    input(type='hidden', name='cmd', value='_s-xclick')
-                    input(type='hidden', name='hosted_button_id', value='FLV5X255Z9CJU')
-                    input(type='image', src='/_assets/img/donate_paypal.png', border='0', name='submit', title='PayPal - The safer, easier way to pay online!', alt='Donate with PayPal button')
-                    img(alt='', border='0', src='https://www.paypal.com/en_CA/i/scr/pixel.gif', width='1', height='1')
-              v-tab-item(:transition='false', :reverse-transition='false')
-                .body-2.pa-3 {{ $t('admin:contribute.ethereum') }}
-                .ml-3
-                  .admin-contribute-ethaddress
-                    strong Ethereum Address
-                    span 0xE1d55C19aE86f6Bcbfb17e7f06aCe96BdBb22Cb5
-                  div: img(src='/_assets/img/donate_eth_qr.png')
-              v-tab-item(:transition='false', :reverse-transition='false')
-                .body-2.pa-3 {{ $t('admin:contribute.tshirts') }}
-                v-card-actions.ml-2
-                  v-btn(outlined, :color='$vuetify.theme.dark ? `blue lighten-1` : `primary`', href='https://wikijs.threadless.com', large)
-                    v-icon(left) mdi-tshirt-crew
-                    span {{ $t('admin:contribute.shop') }}
-            v-divider.mt-3
-            v-subheader.subtitle-2  {{ $t('admin:contribute.contribute') }}
-            .body-2.pl-3
-              ul
-                i18next(path='admin:contribute.submitAnIdea', tag='li')
-                  a(href='https://requests.requarks.io/wiki', target='_blank') {{ $t('admin:contribute.submitAnIdeaLink') }}
-                i18next(path='admin:contribute.foundABug', tag='li')
-                  a(href='https://github.com/Requarks/wiki/issues', target='_blank') Github
-                i18next(path='admin:contribute.helpTranslate', tag='li')
-                  a(href='https://wiki.requarks.io/slack', target='_blank') Slack
-            v-divider.mt-3
-            v-subheader.subtitle-2  {{ $t('admin:contribute.spreadTheWord') }}
-            .body-2.pl-3
-              ul
-                li {{ $t('admin:contribute.talkToFriends') }}
-                i18next(path='admin:contribute.followUsOnTwitter', tag='li')
-                  a(href='https://twitter.com/requarks', target='_blank') Twitter
-          v-toolbar(color='indigo', dense, dark)
-            .subtitle-1 Sponsors &amp; Backers
-          v-container.pa-5.grey(fluid, :class='$vuetify.theme.dark ? `darken-3` : `lighten-4`')
-            v-progress-circular(indeterminate, color='indigo', size='24', width='2', v-if='backers.length < 1')
-            v-row(dense)
-              v-col(cols='12', lg='6', xl='4', v-for='(backer, idx) in backers', :key='backer.id')
-                v-card.grey(flat, :class='$vuetify.theme.dark ? `darken-4` : `lighten-2`')
-                  v-list-item
-                    v-list-item-avatar
-                      img(v-if='backer.avatar', :src='backer.avatar')
-                      v-avatar(v-else, color='blue-grey', size='40')
-                        span.white--text.subtitle-1 {{backer.name[0].toUpperCase()}}
-                    v-list-item-content
-                      v-list-item-title {{backer.name}}
-                      v-list-item-subtitle: .caption Since {{backer.joined | moment('MMMM DD, YYYY')}} on {{backer.source}}
-                    v-list-item-action(v-if='backer.twitter')
-                      v-btn(icon, :href='backer.twitter', target='_blank')
-                        v-icon(color='grey') mdi-twitter
-                    v-list-item-action(v-if='backer.website')
-                      v-btn(icon, :href='backer.website', target='_blank')
-                        v-icon(color='grey') mdi-earth
-          v-toolbar(color='primary', dense, dark)
-            .subtitle-1 Special Thanks
-          v-list(two-line)
-            v-list-item
-              v-list-item-avatar
-                img(src='https://static.requarks.io/logo/algolia.svg', alt='Algolia')
-              v-list-item-content
-                v-list-item-title Algolia
-                v-list-item-subtitle Algolia is a powerful search-as-a-service solution, made easy to use with API clients, UI libraries, and pre-built integrations.
-              v-list-item-action
-                v-btn(icon, href='https://www.algolia.com/', target='_blank')
-                  v-icon(color='grey') mdi-earth
-            v-divider
-            v-list-item
-              v-list-item-avatar
-                img(src='https://static.requarks.io/logo/browserstack.svg', alt='Browserstack')
-              v-list-item-content
-                v-list-item-title BrowserStack
-                v-list-item-subtitle BrowserStack is a cloud web and mobile testing platform that enables developers to test their websites and mobile applications.
-              v-list-item-action
-                v-btn(icon, href='https://www.browserstack.com/', target='_blank')
-                  v-icon(color='grey') mdi-earth
-            v-divider
-            v-list-item
-              v-list-item-avatar
-                img(src='https://static.requarks.io/logo/cloudflare.svg', alt='Cloudflare')
-              v-list-item-content
-                v-list-item-title Cloudflare
-                v-list-item-subtitle Providing content delivery network services, DDoS mitigation, Internet security and distributed domain name server services.
-              v-list-item-action
-                v-btn(icon, href='https://www.cloudflare.com/', target='_blank')
-                  v-icon(color='grey') mdi-earth
-            v-divider
-            v-list-item
-              v-list-item-avatar
-                img(src='https://static.requarks.io/logo/digitalocean.svg', alt='DigitalOcean')
-              v-list-item-content
-                v-list-item-title DigitalOcean
-                v-list-item-subtitle Providing developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces), and more.
-              v-list-item-action
-                v-btn(icon, href='https://m.do.co/c/5f7445bfa4d0', target='_blank')
-                  v-icon(color='grey') mdi-earth
-            v-divider
-            v-list-item
-              v-list-item-avatar(tile)
-                img(src='/_assets/svg/logo-icons8.svg', alt='Icons8')
-              v-list-item-content
-                v-list-item-title Icons8
-                v-list-item-subtitle All the Icons You Need. Guaranteed.
-              v-list-item-action
-                v-btn(icon, href='https://icons8.com', target='_blank')
-                  v-icon(color='grey') mdi-earth
-            v-divider
-            v-list-item
-              v-list-item-avatar(tile)
-                img(src='https://static.requarks.io/logo/lokalise.png', alt='Lokalise')
-              v-list-item-content
-                v-list-item-title Lokalise
-                v-list-item-subtitle Lokalise is a translation management system built for agile teams who want to automate their localization process.
-              v-list-item-action
-                v-btn(icon, href='https://lokalise.co', target='_blank')
-                  v-icon(color='grey') mdi-earth
-            v-divider
-            v-list-item
-              v-list-item-avatar(tile)
-                img(src='https://static.requarks.io/logo/netlify.svg', alt='Netlify')
-              v-list-item-content
-                v-list-item-title Netlify
-                v-list-item-subtitle Deploy modern static websites with Netlify. Get CDN, Continuous deployment, 1-click HTTPS, and all the services you need.
-              v-list-item-action
-                v-btn(icon, href='https://wwwnetlify.com', target='_blank')
-                  v-icon(color='grey') mdi-earth
-
-</template>
-
-<script>
-import gql from 'graphql-tag'
-
-export default {
-  data() {
-    return {
-      backers: []
-    }
-  },
-  apollo: {
-    backers: {
-      query: gql`
-        {
-          contribute {
-            contributors {
-              id
-              source
-              name
-              joined
-              website
-              twitter
-              avatar
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => data.contribute.contributors,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-contribute-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-.admin-contribute {
-
-  &-tabs {
-    .v-tabs__item img {
-      height: 24px;
-      margin-bottom: 5px;
-    }
-  }
-
-  &-ethaddress {
-    display: inline-block;
-    margin-bottom: 12px;
-    border-radius: 7px;
-    background-color: mc('grey', '100');
-    color: mc('grey', '700');
-    padding: 12px;
-
-    strong {
-      display: block;
-    }
-  }
-
-  ul {
-    margin-left: 1rem;
-    list-style-type: square;
-  }
-}
-</style>

+ 0 - 256
client/components/admin/admin-dashboard.vue

@@ -1,256 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-browse-page.svg', alt='Dashboard', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:dashboard.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:dashboard.subtitle') }}
-      v-flex(xs12 md6 lg4 xl3 d-flex)
-        v-card.primary.dashboard-card.animated.fadeInUp(dark)
-          v-card-text
-            v-icon.dashboard-icon mdi-file-document-outline
-            .overline {{$t('admin:dashboard.pages')}}
-            animated-number.display-1(
-              :value='info.pagesTotal'
-              :duration='2000'
-              :formatValue='round'
-              easing='easeOutQuint'
-              )
-      v-flex(xs12 md6 lg4 xl3 d-flex)
-        v-card.blue.darken-3.dashboard-card.animated.fadeInUp.wait-p2s(dark)
-          v-card-text
-            v-icon.dashboard-icon mdi-account
-            .overline {{$t('admin:dashboard.users')}}
-            animated-number.display-1(
-              :value='info.usersTotal'
-              :duration='2000'
-              :formatValue='round'
-              easing='easeOutQuint'
-              )
-      v-flex(xs12 md6 lg4 xl3 d-flex)
-        v-card.blue.darken-4.dashboard-card.animated.fadeInUp.wait-p4s(dark)
-          v-card-text
-            v-icon.dashboard-icon mdi-account-group
-            .overline {{$t('admin:dashboard.groups')}}
-            animated-number.display-1(
-              :value='info.groupsTotal'
-              :duration='2000'
-              :formatValue='round'
-              easing='easeOutQuint'
-              )
-      v-flex(xs12 md6 lg12 xl3 d-flex)
-        v-card.dashboard-card.animated.fadeInUp.wait-p6s(
-          :class='isLatestVersion ? "green" : "red lighten-2"'
-          dark
-          )
-          v-btn.btn-animate-wrench(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, to='system', v-if='hasPermission(`manage:system`)')
-            v-icon(:color='isLatestVersion ? `green` : `red darken-4`', small) mdi-wrench
-          v-card-text
-            v-icon.dashboard-icon mdi-blur
-            .subtitle-1 Wiki.js {{info.currentVersion}}
-            .body-2(v-if='isLatestVersion') {{$t('admin:dashboard.versionLatest')}}
-            .body-2(v-else) {{$t('admin:dashboard.versionNew', { version: info.latestVersion })}}
-      v-flex(xs12, xl6)
-        v-card.radius-7.animated.fadeInUp.wait-p2s
-          v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)
-            v-spacer
-            .overline {{$t('admin:dashboard.recentPages')}}
-            v-spacer
-          v-data-table.pb-2(
-            :items='recentPages'
-            :headers='recentPagesHeaders'
-            :loading='recentPagesLoading'
-            hide-default-footer
-            hide-default-header
-            )
-            template(slot='item', slot-scope='props')
-              tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')
-                td
-                  .body-2: strong {{ props.item.title }}
-                td.admin-pages-path
-                  v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
-                  span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
-                td.text-right.caption(width='250') {{ props.item.updatedAt | moment('calendar') }}
-      v-flex(xs12, xl6)
-        v-card.radius-7.animated.fadeInUp.wait-p4s
-          v-toolbar(:color='$vuetify.theme.dark ? `grey darken-2` : `grey lighten-5`', dense, flat)
-            v-spacer
-            .overline {{$t('admin:dashboard.lastLogins')}}
-            v-spacer
-          v-data-table.pb-2(
-            :items='lastLogins'
-            :headers='lastLoginsHeaders'
-            :loading='lastLoginsLoading'
-            hide-default-footer
-            hide-default-header
-            )
-            template(slot='item', slot-scope='props')
-              tr.is-clickable(:active='props.selected', @click='$router.push(`/users/` + props.item.id)')
-                td
-                  .body-2: strong {{ props.item.name }}
-                td.text-right.caption(width='250') {{ props.item.lastLoginAt | moment('calendar') }}
-
-      v-flex(xs12)
-        v-card.dashboard-contribute.animated.fadeInUp.wait-p4s
-          v-card-text
-            img(src='/_assets/svg/icon-heart-health.svg', alt='Contribute', style='height: 80px;')
-            .pl-5
-              .subtitle-1 {{$t('admin:contribute.title')}}
-              .body-2.mt-3: strong {{$t('admin:dashboard.contributeSubtitle')}}
-              .body-2 {{$t('admin:dashboard.contributeHelp')}}
-              v-btn.mx-0.mt-4(:color='$vuetify.theme.dark ? `indigo lighten-3` : `indigo`', outlined, small, to='/contribute')
-                .caption: strong {{$t('admin:dashboard.contributeLearnMore')}}
-
-</template>
-
-<script>
-import _ from 'lodash'
-import AnimatedNumber from 'animated-number-vue'
-import { get } from 'vuex-pathify'
-import gql from 'graphql-tag'
-import semverLte from 'semver/functions/lte'
-
-export default {
-  components: {
-    AnimatedNumber
-  },
-  data() {
-    return {
-      recentPages: [],
-      recentPagesLoading: false,
-      recentPagesHeaders: [
-        { text: 'Title', value: 'title' },
-        { text: 'Path', value: 'path' },
-        { text: 'Last Updated', value: 'updatedAt', width: 250 }
-      ],
-      lastLogins: [],
-      lastLoginsLoading: false,
-      lastLoginsHeaders: [
-        { text: 'User', value: 'displayName' },
-        { text: 'Last Login', value: 'lastLoginAt', width: 250 }
-      ]
-    }
-  },
-  computed: {
-    isLatestVersion() {
-      if (this.info.latestVersion === 'n/a' || this.info.currentVersion === 'n/a') {
-        return true
-      } else {
-        return semverLte(this.info.latestVersion, this.info.currentVersion)
-      }
-    },
-    info: get('admin/info'),
-    permissions: get('user/permissions')
-  },
-  methods: {
-    round(val) { return Math.round(val) },
-    hasPermission(prm) {
-      if (_.isArray(prm)) {
-        return _.some(prm, p => {
-          return _.includes(this.permissions, p)
-        })
-      } else {
-        return _.includes(this.permissions, prm)
-      }
-    }
-  },
-  apollo: {
-    recentPages: {
-      query: gql`
-        query {
-          pages {
-            list(limit: 10, orderBy: UPDATED, orderByDirection: DESC) {
-              id
-              locale
-              path
-              title
-              description
-              contentType
-              isPublished
-              isPrivate
-              privateNS
-              createdAt
-              updatedAt
-            }
-          }
-        }
-      `,
-      update: (data) => data.pages.list,
-      watchLoading (isLoading) {
-        this.recentPagesLoading = isLoading
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-recentpages')
-      }
-    },
-    lastLogins: {
-      query: gql`
-        query {
-          users {
-            lastLogins {
-              id
-              name
-              lastLoginAt
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => data.users.lastLogins,
-      watchLoading (isLoading) {
-        this.lastLoginsLoading = isLoading
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dashboard-lastlogins')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-.dashboard-card {
-  display: flex;
-  width: 100%;
-  border-radius: 7px;
-
-  .v-card__text {
-    overflow: hidden;
-    position: relative;
-  }
-}
-
-.dashboard-contribute {
-  background-color: #FFF;
-  background-image: linear-gradient(to bottom, #FFF 0%, lighten(mc('indigo', '50'), 3%) 100%);
-  border-radius: 7px;
-
-  @at-root .theme--dark & {
-    background-color: mc('grey', '800');
-    background-image: linear-gradient(to bottom, mc('grey', '800') 0%, darken(mc('grey', '800'), 6%) 100%);
-  }
-
-  .v-card__text {
-    display: flex;
-    align-items: center;
-    color: mc('indigo', '500') !important;
-
-    @at-root .theme--dark & {
-      color: mc('grey', '300') !important;
-    }
-  }
-}
-
-.v-icon.dashboard-icon {
-  position: absolute !important;
-  right: 0;
-  top: 12px;
-  font-size: 100px !important;
-  opacity: .25;
-
-  @at-root .v-application--is-rtl & {
-    left: 0;
-    right: initial;
-  }
-}
-
-</style>

+ 0 - 94
client/components/admin/admin-dev-flags.vue

@@ -1,94 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img(src='/_assets/svg/icon-console.svg', alt='Developer Tools', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text Developer Tools
-            .subtitle-1.grey--text Flags
-          v-spacer
-          v-btn(color='success', depressed, @click='save', large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-
-        v-card.mt-3(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `white grey--text text--darken-3`')
-          v-alert(color='red', :value='true', icon='mdi-alert', dark, prominent)
-            span Do NOT enable these flags unless you know what you're doing!
-            .caption Doing so may result in data loss or broken installation!
-          v-card-text
-            v-switch.mt-3(
-              color='primary'
-              hint='Log detailed debug info on LDAP/AD login attempts.'
-              persistent-hint
-              label='LDAP Debug'
-              v-model='flags.ldapdebug'
-              inset
-            )
-            v-divider.mt-3
-            v-switch.mt-3(
-              color='red'
-              hint='Log all queries made to the database to console.'
-              persistent-hint
-              label='SQL Query Logging'
-              v-model='flags.sqllog'
-              inset
-            )
-</template>
-
-<script>
-import _ from 'lodash'
-
-import flagsQuery from 'gql/admin/dev/dev-query-flags.gql'
-import flagsMutation from 'gql/admin/dev/dev-mutation-save-flags.gql'
-
-export default {
-  data() {
-    return {
-      flags: {
-        sqllog: false
-      }
-    }
-  },
-  methods: {
-    async save() {
-      try {
-        await this.$apollo.mutate({
-          mutation: flagsMutation,
-          variables: {
-            flags: _.transform(this.flags, (result, value, key) => {
-              result.push({ key, value })
-            }, [])
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dev-flags-update')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: 'Flags applied successfully.',
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    }
-  },
-  apollo: {
-    flags: {
-      query: flagsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.transform(data.system.flags, (result, row) => {
-        _.set(result, row.key, row.value)
-      }, {}),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-dev-flags-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 66
client/components/admin/admin-editor.vue

@@ -1,66 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img(src='/_assets/svg/icon-web-design.svg', alt='Editor', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text Editor
-            .subtitle-1.grey--text Configure the content editors #[v-chip(label, color='primary', small).white--text coming soon]
-          v-spacer
-          v-btn(outline, color='grey', @click='refresh', large)
-            v-icon refresh
-          v-btn(color='success', @click='save', depressed, large)
-            v-icon(left) check
-            span {{$t('common:actions.apply')}}
-
-        v-card.mt-3
-          v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark)
-            v-tab(key='settings'): v-icon settings
-            v-tab(key='code') Markdown
-
-            v-tab-item(key='settings', :transition='false', :reverse-transition='false')
-              v-card.pa-3(flat, tile)
-                .body-2.grey--text.text--darken-1 Select which editors to enable:
-                .caption.grey--text.pb-2 Some editors require additional configuration in their dedicated tab (when selected).
-                v-form
-                  v-checkbox.my-0(
-                    v-for='editor in editors'
-                    v-model='editor.isEnabled'
-                    :key='editor.key'
-                    :label='editor.title'
-                    color='primary'
-                    disabled
-                    hide-details
-                  )
-            v-tab-item(key='code', :transition='false', :reverse-transition='false')
-              v-card.wiki-form.pa-3(flat, tile)
-                v-form
-                  v-subheader Editor Configuration
-                  .body-1.ml-3 This editor has no configuration options you can modify.
-</template>
-
-<script>
-export default {
-  data() {
-    return {
-      editors: [
-        { title: 'API Docs', key: 'api', isEnabled: false },
-        { title: 'Code', key: 'code', isEnabled: true },
-        { title: 'Markdown', key: 'markdown', isEnabled: true },
-        { title: 'Tabular', key: 'tabular', isEnabled: false },
-        { title: 'Visual Builder', key: 'visual', isEnabled: false },
-        { title: 'WikiText', key: 'wikitext', isEnabled: false }
-      ]
-    }
-  },
-  methods: {
-    save() {},
-    refresh() {}
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 119
client/components/admin/admin-extensions.vue

@@ -1,119 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-installing-updates.svg', alt='Extensions', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:extensions.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:extensions.subtitle') }}
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(xl6 lg8 xs12)
-              v-alert.mb-4(outlined, color='error', icon='mdi-alert')
-                span New extensions cannot be installed at the moment. This feature is coming in a future release.
-              v-expansion-panels.admin-extensions-exp(hover, popout)
-                v-expansion-panel(v-for='ext of extensions', :key='`ext-` + ext.key')
-                  v-expansion-panel-header(disable-icon-rotate)
-                    span {{ext.title}}
-                    template(v-slot:actions)
-                      v-chip(label, color='success', small, v-if='ext.isInstalled') Installed
-                      v-chip(label, color='warning', small, v-else) Not Installed
-                  v-expansion-panel-content.pa-0
-                    v-card(flat, :class='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-5`', tile)
-                      v-card-text
-                        .body-2 {{ext.description}}
-                        v-divider.my-4
-                        .body-2
-                          strong.mr-2 This extension is
-                          v-chip.mr-2(v-if='ext.isCompatible', label, outlined, small, color='success') compatible
-                          v-chip.mr-2(v-else, label, small, color='error') not compatible
-                          strong with your host.
-                      v-card-chin
-                        v-spacer
-                        v-btn(disabled)
-                          v-icon(left) mdi-plus
-                          span Install
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-
-export default {
-  data() {
-    return {
-      extensions: []
-    }
-  },
-  methods: {
-    async save () {
-      // try {
-      //   await this.$apollo.mutate({
-      //     mutation: gql`
-      //       mutation (
-      //         $host: String!
-      //       ) {
-      //         site {
-      //           updateConfig(
-      //             host: $host
-      //           ) {
-      //             responseResult {
-      //               succeeded
-      //               errorCode
-      //               slug
-      //               message
-      //             }
-      //           }
-      //         }
-      //       }
-      //     `,
-      //     variables: {
-      //       host: _.get(this.config, 'host', '')
-      //     },
-      //     watchLoading (isLoading) {
-      //       this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-extensions-update')
-      //     }
-      //   })
-      //   this.$store.commit('showNotification', {
-      //     style: 'success',
-      //     message: 'Configuration saved successfully.',
-      //     icon: 'check'
-      //   })
-      // } catch (err) {
-      //   this.$store.commit('pushGraphError', err)
-      // }
-    }
-  },
-  apollo: {
-    extensions: {
-      query: gql`
-        {
-          system {
-            extensions {
-              key
-              title
-              description
-              isInstalled
-              isCompatible
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.system.extensions),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-extensions-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-.admin-extensions-exp {
-  .v-expansion-panel-content__wrap {
-    padding: 0;
-  }
-}
-</style>

+ 0 - 369
client/components/admin/admin-general.vue

@@ -1,369 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-categorize.svg', alt='General', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:general.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:general.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(lg6 xs12)
-              v-form
-                v-card.animated.fadeInUp
-                  v-toolbar(color='primary', dark, dense, flat)
-                    v-toolbar-title.subtitle-1 {{ $t('admin:general.siteInfo') }}
-                  .overline.grey--text.pa-4 {{$t('admin:general.general')}}
-                  .px-3.pb-3
-                    v-text-field(
-                      outlined
-                      :label='$t(`admin:general.siteUrl`)'
-                      required
-                      :counter='255'
-                      v-model='config.host'
-                      prepend-icon='mdi-label-variant-outline'
-                      :hint='$t(`admin:general.siteUrlHint`)'
-                      persistent-hint
-                      )
-                    v-text-field.mt-3(
-                      outlined
-                      :label='$t(`admin:general.siteTitle`)'
-                      required
-                      :counter='50'
-                      v-model='config.title'
-                      prepend-icon='mdi-earth'
-                      :hint='$t(`admin:general.siteTitleHint`)'
-                      persistent-hint
-                      )
-                  v-divider
-                  .overline.grey--text.pa-4 {{$t('admin:general.logo')}}
-                  .pt-2.pb-7.pl-10.pr-3
-                    .d-flex.align-center
-                      v-avatar(size='100', tile)
-                        v-img(
-                          :src='config.logoUrl'
-                          lazy-src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNcWQ8AAdcBKrJda2oAAAAASUVORK5CYII='
-                          aspect-ratio='1'
-                          )
-                      .ml-4(style='flex: 1 1 auto;')
-                        v-text-field(
-                          outlined
-                          :label='$t(`admin:general.logoUrl`)'
-                          v-model='config.logoUrl'
-                          :hint='$t(`admin:general.logoUrlHint`)'
-                          persistent-hint
-                          append-icon='mdi-folder-image'
-                          @click:append='browseLogo'
-                          @keyup.enter='refreshLogo'
-                        )
-                  v-divider
-                  .overline.grey--text.pa-4 {{$t('admin:general.footerCopyright')}}
-                  .px-3.pb-3
-                    v-text-field(
-                      outlined
-                      :label='$t(`admin:general.companyName`)'
-                      v-model='config.company'
-                      :counter='255'
-                      prepend-icon='mdi-domain'
-                      persistent-hint
-                      :hint='$t(`admin:general.companyNameHint`)'
-                      )
-                    v-select.mt-3(
-                      outlined
-                      :label='$t(`admin:general.contentLicense`)'
-                      :items='contentLicenses'
-                      v-model='config.contentLicense'
-                      prepend-icon='mdi-creative-commons'
-                      :return-object='false'
-                      :hint='$t(`admin:general.contentLicenseHint`)'
-                      persistent-hint
-                      )
-                  v-divider
-                  .overline.grey--text.pa-4 SEO
-                  .px-3.pb-3
-                    v-text-field(
-                      outlined
-                      :label='$t(`admin:general.siteDescription`)'
-                      :counter='255'
-                      v-model='config.description'
-                      prepend-icon='mdi-compass'
-                      :hint='$t(`admin:general.siteDescriptionHint`)'
-                      persistent-hint
-                      )
-                    v-select.mt-3(
-                      outlined
-                      :label='$t(`admin:general.metaRobots`)'
-                      multiple
-                      :items='metaRobots'
-                      v-model='config.robots'
-                      prepend-icon='mdi-compass'
-                      :return-object='false'
-                      :hint='$t(`admin:general.metaRobotsHint`)'
-                      persistent-hint
-                      )
-
-            v-flex(lg6 xs12)
-              v-card.animated.fadeInUp.wait-p4s
-                v-toolbar(color='indigo', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 Features
-                v-card-text
-                  //- v-switch(
-                  //-   inset
-                  //-   label='Asset Image Optimization'
-                  //-   color='indigo'
-                  //-   v-model='config.featureTinyPNG'
-                  //-   persistent-hint
-                  //-   hint='Image optimization tool to reduce filesize and bandwidth costs.'
-                  //-   disabled
-                  //-   )
-                  //- v-text-field.mt-3(
-                  //-   outlined
-                  //-   label='TinyPNG API Key'
-                  //-   :counter='255'
-                  //-   v-model='config.description'
-                  //-   prepend-icon='mdi-subdirectory-arrow-right'
-                  //-   hint='Get your API key at https://tinypng.com/developers'
-                  //-   persistent-hint
-                  //-   disabled
-                  //-   )
-
-                  //- v-divider.mt-3
-                  //- v-switch(
-                  //-   inset
-                  //-   label='Page Ratings'
-                  //-   color='indigo'
-                  //-   v-model='config.featurePageRatings'
-                  //-   persistent-hint
-                  //-   hint='Allow users to rate pages.'
-                  //-   disabled
-                  //-   )
-
-                  //- v-divider.mt-3
-                  v-switch(
-                    inset
-                    label='Comments'
-                    color='indigo'
-                    v-model='config.featurePageComments'
-                    persistent-hint
-                    hint='Allow users to leave comments on pages.'
-                    )
-
-                  //- v-divider.mt-3
-                  //- v-switch(
-                  //-   inset
-                  //-   label='Personal Wikis'
-                  //-   color='indigo'
-                  //-   v-model='config.featurePersonalWikis'
-                  //-   persistent-hint
-                  //-   hint='Allow users to have their own personal wiki.'
-                  //-   disabled
-                  //-   )
-
-    component(:is='activeModal')
-
-</template>
-
-<script>
-import _ from 'lodash'
-import { sync } from 'vuex-pathify'
-import gql from 'graphql-tag'
-
-import editorStore from '../../store/editor'
-
-/* global WIKI */
-
-const titleRegex = /[<>"]/i
-
-WIKI.$store.registerModule('editor', editorStore)
-
-export default {
-  i18nOptions: { namespaces: 'editor' },
-  components: {
-    editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
-  },
-  data() {
-    return {
-      config: {
-        host: '',
-        title: '',
-        description: '',
-        robots: [],
-        analyticsService: '',
-        analyticsId: '',
-        company: '',
-        contentLicense: '',
-        logoUrl: '',
-        featureAnalytics: false,
-        featurePageRatings: false,
-        featurePageComments: false,
-        featurePersonalWikis: false,
-        featureTinyPNG: false
-      },
-      metaRobots: [
-        { text: 'Index', value: 'index' },
-        { text: 'Follow', value: 'follow' },
-        { text: 'No Index', value: 'noindex' },
-        { text: 'No Follow', value: 'nofollow' }
-      ]
-    }
-  },
-  computed: {
-    siteTitle: sync('site/title'),
-    logoUrl: sync('site/logoUrl'),
-    company: sync('site/company'),
-    contentLicense: sync('site/contentLicense'),
-    activeModal: sync('editor/activeModal'),
-    contentLicenses () {
-      return [
-        { value: '', text: this.$t('common:license.none') },
-        { value: 'alr', text: this.$t('common:license.alr') },
-        { value: 'cc0', text: this.$t('common:license.cc0') },
-        { value: 'ccby', text: this.$t('common:license.ccby') },
-        { value: 'ccbysa', text: this.$t('common:license.ccbysa') },
-        { value: 'ccbynd', text: this.$t('common:license.ccbynd') },
-        { value: 'ccbync', text: this.$t('common:license.ccbync') },
-        { value: 'ccbyncsa', text: this.$t('common:license.ccbyncsa') },
-        { value: 'ccbyncnd', text: this.$t('common:license.ccbyncnd') }
-      ]
-    }
-  },
-  methods: {
-    async save () {
-      const title = _.get(this.config, 'title', '')
-      if (titleRegex.test(title)) {
-        this.$store.commit('showNotification', {
-          style: 'error',
-          message: this.$t('admin:general.siteTitleInvalidChars'),
-          icon: 'alert'
-        })
-        return
-      }
-      try {
-        await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $host: String!
-              $title: String!
-              $description: String!
-              $robots: [String]!
-              $analyticsService: String!
-              $analyticsId: String!
-              $company: String!
-              $contentLicense: String!
-              $logoUrl: String!
-              $featurePageRatings: Boolean!
-              $featurePageComments: Boolean!
-              $featurePersonalWikis: Boolean!
-            ) {
-              site {
-                updateConfig(
-                  host: $host,
-                  title: $title,
-                  description: $description,
-                  robots: $robots,
-                  analyticsService: $analyticsService,
-                  analyticsId: $analyticsId,
-                  company: $company,
-                  contentLicense: $contentLicense,
-                  logoUrl: $logoUrl,
-                  featurePageRatings: $featurePageRatings,
-                  featurePageComments: $featurePageComments,
-                  featurePersonalWikis: $featurePersonalWikis
-                ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            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', ''),
-            contentLicense: _.get(this.config, 'contentLicense', ''),
-            logoUrl: _.get(this.config, 'logoUrl', ''),
-            featurePageRatings: _.get(this.config, 'featurePageRatings', false),
-            featurePageComments: _.get(this.config, 'featurePageComments', false),
-            featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false)
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:general.saveSuccess'),
-          icon: 'check'
-        })
-        this.siteTitle = this.config.title
-        this.company = this.config.company
-        this.contentLicense = this.config.contentLicense
-        this.logoUrl = this.config.logoUrl
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    },
-    browseLogo () {
-      this.$store.set('editor/editorKey', 'common')
-      this.activeModal = 'editorModalMedia'
-    },
-    refreshLogo () {
-      this.$forceUpdate()
-    }
-  },
-  mounted () {
-    this.$root.$on('editorInsert', opts => {
-      this.config.logoUrl = opts.path
-    })
-  },
-  beforeDestroy() {
-    this.$root.$off('editorInsert')
-  },
-  apollo: {
-    config: {
-      query: gql`
-        {
-          site {
-            config {
-              host
-              title
-              description
-              robots
-              analyticsService
-              analyticsId
-              company
-              contentLicense
-              logoUrl
-              featurePageRatings
-              featurePageComments
-              featurePersonalWikis
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.site.config),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 224
client/components/admin/admin-groups-edit-permissions.vue

@@ -1,224 +0,0 @@
-<template lang="pug">
-  v-card(flat)
-    v-container.px-3.pb-3.pt-3(fluid, grid-list-md)
-      v-layout(row, wrap)
-        v-flex(xs12, v-if='group.isSystem')
-          v-alert.radius-7.mb-0(
-            color='orange darken-2'
-            :class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
-            outlined
-            :value='true'
-            icon='mdi-lock-outline'
-            ) This is a system group. Some permissions cannot be modified.
-        v-flex(xs12, md6, lg4, v-for='pmGroup in permissions', :key='pmGroup.category')
-          v-card.md2(flat, :class='$vuetify.theme.dark ? "grey darken-3-d5" : "grey lighten-5"')
-            .overline.px-5.pt-5.pb-3.grey--text.text--darken-2 {{pmGroup.category}}
-            v-card-text.pt-0
-              template(v-for='(pm, idx) in pmGroup.items')
-                v-checkbox.pt-0(
-                  style='justify-content: space-between;'
-                  :key='pm.permission'
-                  :label='pm.permission'
-                  :hint='pm.hint'
-                  persistent-hint
-                  color='primary'
-                  v-model='group.permissions'
-                  :value='pm.permission'
-                  :append-icon='pm.warning ? "mdi-alert" : null',
-                  :disabled='(group.isSystem && pm.restrictedForSystem) || group.id === 1 || pm.disabled'
-                )
-                v-divider.mt-3(v-if='idx < pmGroup.items.length - 1')
-</template>
-
-<script>
-export default {
-  props: {
-    value: {
-      type: Object,
-      default: () => ({})
-    }
-  },
-  data() {
-    return {
-      permissions: [
-        {
-          category: 'Content',
-          items: [
-            {
-              permission: 'read:pages',
-              hint: 'Can view pages, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: false,
-              disabled: false
-            },
-            {
-              permission: 'write:pages',
-              hint: 'Can create / edit pages, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'manage:pages',
-              hint: 'Can move existing pages as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'delete:pages',
-              hint: 'Can delete existing pages, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'write:styles',
-              hint: 'Can insert CSS styles in pages, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'write:scripts',
-              hint: 'Can insert JavaScript in pages, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'read:source',
-              hint: 'Can view pages source, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: false,
-              disabled: false
-            },
-            {
-              permission: 'read:history',
-              hint: 'Can view pages history, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: false,
-              disabled: false
-            },
-            {
-              permission: 'read:assets',
-              hint: 'Can view / use assets (such as images and files), as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: false,
-              disabled: false
-            },
-            {
-              permission: 'write:assets',
-              hint: 'Can upload new assets (such as images and files), as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'manage:assets',
-              hint: 'Can edit and delete existing assets (such as images and files), as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'read:comments',
-              hint: 'Can view comments, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: false,
-              disabled: false
-            },
-            {
-              permission: 'write:comments',
-              hint: 'Can post new comments, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: false,
-              disabled: false
-            },
-            {
-              permission: 'manage:comments',
-              hint: 'Can edit and delete existing comments, as specified in the Page Rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            }
-          ]
-        },
-        {
-          category: 'Users',
-          items: [
-            {
-              permission: 'write:users',
-              hint: 'Can create or authorize new users, but not modify existing ones',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'manage:users',
-              hint: 'Can manage all users (but not users with administrative permissions)',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'write:groups',
-              hint: 'Can manage groups and assign CONTENT permissions / page rules',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'manage:groups',
-              hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules',
-              warning: true,
-              restrictedForSystem: true,
-              disabled: false
-            }
-          ]
-        },
-        {
-          category: 'Administration',
-          items: [
-            {
-              permission: 'manage:navigation',
-              hint: 'Can manage the site navigation',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'manage:theme',
-              hint: 'Can manage and modify themes',
-              warning: false,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'manage:api',
-              hint: 'Can generate and revoke API keys',
-              warning: true,
-              restrictedForSystem: true,
-              disabled: false
-            },
-            {
-              permission: 'manage:system',
-              hint: 'Can manage and access everything. Root administrator.',
-              warning: true,
-              restrictedForSystem: true,
-              disabled: true
-
-            }
-          ]
-        }
-      ]
-    }
-  },
-  computed: {
-    group: {
-      get() { return this.value },
-      set(val) { this.$set('input', val) }
-    }
-  }
-}
-</script>

+ 0 - 336
client/components/admin/admin-groups-edit-rules.vue

@@ -1,336 +0,0 @@
-<template lang="pug">
-  v-card(flat)
-    v-card-text(v-if='group.id === 1')
-      v-alert.radius-7.mb-0(
-        :class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
-        color='orange darken-2'
-        outlined
-        icon='mdi-lock-outline'
-        ) This group has access to everything.
-    template(v-else)
-      v-card-title(:class='$vuetify.theme.dark ? `grey darken-3-d5` : ``')
-        v-alert.radius-7.caption(
-          :class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-4`'
-          color='grey'
-          outlined
-          icon='mdi-information'
-          ) You must enable global content permissions (under Permissions tab) for page rules to have any effect.
-        v-spacer
-        v-btn.mx-2(depressed, color='primary', @click='addRule')
-          v-icon(left) mdi-plus
-          | Add Rule
-        v-menu(
-          right
-          offset-y
-          nudge-left='115'
-          )
-          template(v-slot:activator='{ on }')
-            v-btn.is-icon(v-on='on', outlined, color='primary')
-              v-icon mdi-dots-horizontal
-          v-list(dense)
-            v-list-item(@click='comingSoon')
-              v-list-item-avatar
-                v-icon mdi-application-import
-              v-list-item-title Load Preset
-            v-divider
-            v-list-item(@click='comingSoon')
-              v-list-item-avatar
-                v-icon mdi-application-export
-              v-list-item-title Save As Preset
-            v-divider
-            v-list-item(@click='comingSoon')
-              v-list-item-avatar
-                v-icon mdi-cloud-upload
-              v-list-item-title Import Rules
-            v-divider
-            v-list-item(@click='comingSoon')
-              v-list-item-avatar
-                v-icon mdi-cloud-download
-              v-list-item-title Export Rules
-      v-card-text(:class='$vuetify.theme.dark ? `grey darken-4-l5` : `white`')
-        .rules
-          .caption(v-if='group.pageRules.length === 0')
-            em(:class='$vuetify.theme.dark ? `grey--text` : `blue-grey--text`') This group has no page rules yet.
-          .rule(v-for='rule of group.pageRules', :key='rule.id')
-            v-btn.ma-0.radius-4.rule-deny-btn(
-              solo
-              :color='rule.deny ? "red" : "green"'
-              dark
-              @click='rule.deny = !rule.deny'
-              height='48'
-              )
-              v-icon(v-if='rule.deny') mdi-cancel
-              v-icon(v-else) mdi-check-circle
-            //- Roles
-            v-select.ml-1(
-              solo
-              :items='roles'
-              v-model='rule.roles'
-              placeholder='Select Role(s)...'
-              hide-details
-              multiple
-              chips
-              deletable-chips
-              small-chips
-              height='48px'
-              style='flex: 0 1 440px;'
-              :menu-props='{ "maxHeight": 500 }'
-              clearable
-              dense
-              )
-              template(slot='selection', slot-scope='{ item, index }')
-                v-chip.white--text.ml-0(v-if='index <= 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value }}
-                v-chip.white--text.ml-0(v-if='index === 2', small, label, :color='rule.deny ? `red lighten-2` : `green lighten-2`').caption + {{ rule.roles.length - 2 }} more
-              template(slot='item', slot-scope='props')
-                v-list-item-action(style='min-width: 30px;')
-                  v-checkbox(
-                    v-model='props.attrs.inputValue'
-                    hide-details
-                    color='primary'
-                  )
-                v-icon.mr-2(:color='rule.deny ? `red` : `green`') {{props.item.icon}}
-                v-list-item-content
-                  v-list-item-title.body-2 {{props.item.text}}
-                v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.value}}
-
-            //- Match
-            v-select.ml-1.mr-1(
-              solo
-              :items='matches'
-              v-model='rule.match'
-              placeholder='Match...'
-              hide-details
-              height='48px'
-              style='flex: 0 1 250px;'
-              dense
-              )
-              template(slot='selection', slot-scope='{ item, index }')
-                .body-2 {{item.text}}
-              template(slot='item', slot-scope='data')
-                v-list-item-avatar
-                  v-avatar.white--text.radius-4(color='blue', size='30', tile) {{ data.item.icon }}
-                v-list-item-content
-                  v-list-item-title(v-html='data.item.text')
-            //- Locales
-            v-select.mr-1(
-              :background-color='$vuetify.theme.dark ? `grey darken-3-d5` : `blue-grey lighten-5`'
-              solo
-              :items='locales'
-              v-model='rule.locales'
-              placeholder='Any Locale'
-              item-value='code'
-              item-text='name'
-              multiple
-              hide-details
-              height='48px'
-              dense
-              :menu-props='{ "minWidth": 250 }'
-              style='flex: 0 1 150px;'
-              )
-              template(slot='selection', slot-scope='{ item, index }')
-                v-chip.white--text.ml-0(v-if='rule.locales.length === 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.code.toUpperCase() }}
-                v-chip.white--text.ml-0(v-else-if='index === 0', small, label, :color='rule.deny ? `red` : `green`').caption {{ rule.locales.length }} locales
-              v-list-item(slot='prepend-item', @click='rule.locales = []')
-                v-list-item-action(style='min-width: 30px;')
-                  v-checkbox(
-                    :input-value='rule.locales.length === 0'
-                    hide-details
-                    color='primary'
-                    readonly
-                  )
-                v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-earth
-                v-list-item-content
-                  v-list-item-title.body-2 Any Locale
-              v-divider(slot='prepend-item')
-              template(slot='item', slot-scope='props')
-                v-list-item-action(style='min-width: 30px;')
-                  v-checkbox(
-                    v-model='props.attrs.inputValue'
-                    hide-details
-                    color='primary'
-                  )
-                v-icon.mr-2(:color='rule.deny ? `red` : `green`') mdi-web
-                v-list-item-content
-                  v-list-item-title.body-2 {{props.item.name}}
-                v-chip.mr-2.grey--text(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.code.toUpperCase()}}
-
-            //- Path
-            v-text-field(
-              solo
-              v-model='rule.path'
-              label='Path'
-              :prefix='(rule.match !== `END` && rule.match !== `TAG`) ? `/` : null'
-              :placeholder='rule.match === `REGEX` ? `Regular Expression` : rule.match === `TAG` ? `Tag` : `Path`'
-              :suffix='rule.match === `REGEX` ? `/` : null'
-              hide-details
-              :color='$vuetify.theme.dark ? `grey` : `blue-grey`'
-              )
-
-            v-btn.ml-2(icon, @click='removeRule(rule.id)', small)
-              v-icon(:color='$vuetify.theme.dark ? `grey` : `blue-grey`') mdi-close
-
-        v-divider.mt-3
-        .overline.py-3 Rules Order
-        .body-2.pl-3 Rules are applied in order of path specificity. A more precise path will always override a less defined path.
-        .body-2.pl-5 For example, #[span.teal--text /geography/countries] will override #[span.teal--text /geography].
-        .body-2.pl-3.pt-2 When 2 rules have the same specificity, the priority is given from lowest to highest as follows:
-        .body-2.pl-3.pt-1
-          ul
-            li
-              strong Path Starts With...
-              em.caption.pl-1 (lowest)
-            li
-              strong Path Ends With...
-            li
-              strong Path Matches Regex...
-            li
-              strong Tag Matches...
-            li
-              strong Path Is Exactly...
-              em.caption.pl-1 (highest)
-        .body-2.pl-3.pt-2 When 2 rules have the same path specificity AND the same match type, #[strong.red--text DENY] will always override an #[strong.green--text ALLOW] rule.
-        v-divider.mt-3
-        .overline.py-3 Regular Expressions
-        span Expressions that are deemed unsafe or could result in exponential time processing will be rejected upon saving.
-
-</template>
-
-<script>
-import _ from 'lodash'
-import { customAlphabet } from 'nanoid/non-secure'
-
-/* global siteLangs */
-
-const nanoid = customAlphabet('1234567890abcdef', 10)
-
-export default {
-  props: {
-    value: {
-      type: Object,
-      default: () => ({})
-    }
-  },
-  data() {
-    return {
-      roles: [
-        { text: 'Read Pages', value: 'read:pages', icon: 'mdi-file-eye-outline' },
-        { text: 'Create Pages', value: 'write:pages', icon: 'mdi-file-plus-outline' },
-        { text: 'Edit + Move Pages', value: 'manage:pages', icon: 'mdi-file-document-edit-outline' },
-        { text: 'Delete Pages', value: 'delete:pages', icon: 'mdi-file-remove-outline' },
-        { text: 'View Pages Source', value: 'read:source', icon: 'mdi-code-tags' },
-        { text: 'View Pages History', value: 'read:history', icon: 'mdi-history' },
-        { text: 'Read / Use Assets', value: 'read:assets', icon: 'mdi-image-search-outline' },
-        { text: 'Upload Assets', value: 'write:assets', icon: 'mdi-image-plus' },
-        { text: 'Edit + Delete Assets', value: 'manage:assets', icon: 'mdi-image-size-select-large' },
-        { text: 'Edit Scripts', value: 'write:scripts', icon: 'mdi-language-javascript' },
-        { text: 'Edit Styles', value: 'write:styles', icon: 'mdi-language-css3' },
-        { text: 'Read Comments', value: 'read:comments', icon: 'mdi-comment-search-outline' },
-        { text: 'Create Comments', value: 'write:comments', icon: 'mdi-comment-plus-outline' },
-        { text: 'Edit + Delete Comments', value: 'manage:comments', icon: 'mdi-comment-remove-outline' }
-      ],
-      matches: [
-        { text: 'Path Starts With...', value: 'START', icon: '/...' },
-        { text: 'Path is Exactly...', value: 'EXACT', icon: '=' },
-        { text: 'Path Ends With...', value: 'END', icon: '.../' },
-        { text: 'Path Matches Regex...', value: 'REGEX', icon: '$.*' },
-        { text: 'Tag Matches...', value: 'TAG', icon: 'T' }
-      ]
-    }
-  },
-  computed: {
-    group: {
-      get() { return this.value },
-      set(val) { this.$set('input', val) }
-    },
-    locales() { return siteLangs }
-  },
-  methods: {
-    addRule(group) {
-      this.group.pageRules.push({
-        id: nanoid(),
-        path: '',
-        roles: [],
-        match: 'START',
-        deny: false,
-        locales: []
-      })
-    },
-    removeRule(ruleId) {
-      this.group.pageRules.splice(_.findIndex(this.group.pageRules, ['id', ruleId]), 1)
-    },
-    comingSoon() {
-      this.$store.commit('showNotification', {
-        style: 'indigo',
-        message: `Coming soon...`,
-        icon: 'directions_boat'
-      })
-    },
-    dude (stuff) {
-      console.info(stuff)
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-.rules {
-  background-color: mc('blue-grey', '50');
-  border-radius: 4px;
-  padding: 1rem;
-  position: relative;
-
-  @at-root .v-application.theme--dark & {
-    background-color: mc('grey', '800');
-  }
-}
-
-.rule {
-  display: flex;
-  background-color: mc('blue-grey', '100');
-  border-radius: 4px;
-  padding: .5rem;
-  align-items: center;
-
-  &-enter-active, &-leave-active {
-    transition: all .5s ease;
-  }
-  &-enter, &-leave-to {
-    opacity: 0;
-  }
-
-  @at-root .v-application.theme--dark & {
-    background-color: mc('grey', '700');
-  }
-
-  & + .rule {
-    margin-top: .5rem;
-    position: relative;
-
-    &::before {
-      content: '+';
-      position: absolute;
-      width: 2rem;
-      height: 2rem;
-      border-radius: 50%;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      font-weight: 600;
-      color: mc('blue-grey', '700');
-      font-size: 1.25rem;
-      background-color: mc('blue-grey', '50');
-      left: -2rem;
-      top: -1.3rem;
-
-      @at-root .v-application.theme--dark & {
-        background-color: mc('grey', '800');
-        color: mc('grey', '600');
-      }
-    }
-  }
-
-  .input-group + * {
-    margin-left: .5rem;
-  }
-}
-</style>

+ 0 - 149
client/components/admin/admin-groups-edit-users.vue

@@ -1,149 +0,0 @@
-<template lang="pug">
-  v-card(flat)
-    v-card-title.pb-4(:class='$vuetify.theme.dark ? `grey darken-3-d3` : `grey lighten-5`')
-      v-text-field(
-        outlined
-        flat
-        prepend-inner-icon='mdi-magnify'
-        v-model='search'
-        label='Search Group Users...'
-        hide-details
-        dense
-        style='max-width: 450px;'
-      )
-      v-spacer
-      v-btn(color='primary', depressed, @click='searchUserDialog = true', :disabled='group.id === 2')
-        v-icon(left) mdi-clipboard-account
-        | Assign User
-    v-data-table(
-      :items='group.users',
-      :headers='headers',
-      :search='search'
-      :page.sync='pagination'
-      :items-per-page='15'
-      @page-count='pageCount = $event'
-      must-sort,
-      hide-default-footer
-    )
-      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')
-        v-alert.ma-3(icon='mdi-alert', outlined) No users to display.
-    .text-center.py-2(v-if='group.users.length > 15')
-      v-pagination(v-model='pagination', :length='pageCount')
-
-    user-search(v-model='searchUserDialog', @select='assignUser')
-</template>
-
-<script>
-import UserSearch from '../common/user-search.vue'
-
-import assignUserMutation from 'gql/admin/groups/groups-mutation-assign.gql'
-import unassignUserMutation from 'gql/admin/groups/groups-mutation-unassign.gql'
-
-export default {
-  props: {
-    value: {
-      type: Object,
-      default: () => ({})
-    }
-  },
-  components: {
-    UserSearch
-  },
-  data() {
-    return {
-      headers: [
-        { text: 'ID', value: 'id', width: 70 },
-        { text: 'Name', value: 'name' },
-        { text: 'Email', value: 'email' },
-        { text: 'Actions', value: 'actions', sortable: false, width: 50 }
-      ],
-      searchUserDialog: false,
-      pagination: 1,
-      pageCount: 0,
-      search: ''
-    }
-  },
-  computed: {
-    group: {
-      get() { return this.value },
-      set(val) { this.$set('input', val) }
-    },
-    pages () {
-      if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null) {
-        return 0
-      }
-
-      return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
-    }
-  },
-  methods: {
-    async assignUser({ id, email, name }) {
-      try {
-        await this.$apollo.mutate({
-          mutation: assignUserMutation,
-          variables: {
-            groupId: this.group.id,
-            userId: id
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-assign')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: `User has been assigned to ${this.group.name}.`,
-          icon: 'assignment_ind'
-        })
-        this.$emit('refresh')
-      } catch (err) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: err.message,
-          icon: 'warning'
-        })
-      }
-    },
-    async unassignUser(id) {
-      try {
-        await this.$apollo.mutate({
-          mutation: unassignUserMutation,
-          variables: {
-            groupId: this.group.id,
-            userId: id
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-unassign')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: `User has been unassigned from ${this.group.name}.`,
-          icon: 'assignment_ind'
-        })
-        this.$emit('refresh')
-      } catch (err) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: err.message,
-          icon: 'warning'
-        })
-      }
-    }
-  }
-}
-</script>

+ 0 - 271
client/components/admin/admin-groups-edit.vue

@@ -1,271 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img(src='/_assets/svg/icon-social-group.svg', alt='Edit Group', style='width: 80px;')
-          .admin-header-title
-            .headline.blue--text.text--darken-2 Edit Group
-            .subtitle-1.grey--text {{group.name}}
-          v-spacer
-          v-btn(color='grey', icon, outlined, to='/groups')
-            v-icon mdi-arrow-left
-          v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem')
-            template(v-slot:activator='{ on }')
-              v-btn.ml-3(color='red', icon, outlined, v-on='on')
-                v-icon(color='red') mdi-trash-can-outline
-            v-card
-              .dialog-header.is-red Delete Group?
-              v-card-text.pa-4 Are you sure you want to delete group #[strong {{ group.name }}]? All users will be unassigned from this group.
-              v-card-actions
-                v-spacer
-                v-btn(text, @click='deleteGroupDialog = false') Cancel
-                v-btn(color='red', dark, @click='deleteGroup') Delete
-          v-btn.ml-3(color='success', large, depressed, @click='updateGroup')
-            v-icon(left) mdi-check
-            span Update Group
-        v-card.mt-3
-          v-tabs.grad-tabs(v-model='tab', :color='$vuetify.theme.dark ? `blue` : `primary`', fixed-tabs, show-arrows, icons-and-text)
-            v-tab(key='settings')
-              span Settings
-              v-icon mdi-cog-box
-            v-tab(key='permissions')
-              span Permissions
-              v-icon mdi-lock-pattern
-            v-tab(key='rules')
-              span Page Rules
-              v-icon mdi-file-lock
-            v-tab(key='users')
-              span Users
-              v-icon mdi-account-group
-
-            v-tab-item(key='settings', :transition='false', :reverse-transition='false')
-              v-card(flat)
-                template(v-if='group.id <= 2')
-                  v-card-text
-                    v-alert.radius-7.mb-0(
-                      color='orange darken-2'
-                      :class='$vuetify.theme.dark ? "grey darken-4" : "orange lighten-5"'
-                      outlined
-                      :value='true'
-                      icon='mdi-lock-outline'
-                      ) This is a system group and its settings cannot be modified.
-                  v-divider
-                v-card-text
-                  v-text-field(
-                    outlined
-                    v-model='group.name'
-                    label='Group Name'
-                    hide-details
-                    prepend-icon='mdi-account-group'
-                    style='max-width: 600px;'
-                    :disabled='group.id <= 2'
-                  )
-                template(v-if='group.id !== 2')
-                  v-divider
-                  v-card-text
-                    v-text-field(
-                      outlined
-                      v-model='group.redirectOnLogin'
-                      label='Redirect on Login'
-                      persistent-hint
-                      hint='The path / URL where the user will be redirected upon successful login.'
-                      prepend-icon='mdi-arrow-top-left-thick'
-                      append-icon='mdi-folder-search'
-                      @click:append='selectPage'
-                      style='max-width: 850px;'
-                      :counter='255'
-                    )
-
-            v-tab-item(key='permissions', :transition='false', :reverse-transition='false')
-              group-permissions(v-model='group', @refresh='refresh')
-
-            v-tab-item(key='rules', :transition='false', :reverse-transition='false')
-              group-rules(v-model='group', @refresh='refresh')
-
-            v-tab-item(key='users', :transition='false', :reverse-transition='false')
-              group-users(v-model='group', @refresh='refresh')
-
-          v-card-chin
-            v-spacer
-            .caption.grey--text.pr-2 Group ID #[strong {{group.id}}]
-
-    page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-
-import GroupPermissions from './admin-groups-edit-permissions.vue'
-import GroupRules from './admin-groups-edit-rules.vue'
-import GroupUsers from './admin-groups-edit-users.vue'
-
-/* global siteConfig */
-
-export default {
-  components: {
-    GroupPermissions,
-    GroupRules,
-    GroupUsers
-  },
-  data() {
-    return {
-      group: {
-        id: 0,
-        name: '',
-        isSystem: false,
-        permissions: [],
-        pageRules: [],
-        users: [],
-        redirectOnLogin: '/'
-      },
-      deleteGroupDialog: false,
-      tab: null,
-      selectPageModal: false,
-      currentLang: siteConfig.lang
-    }
-  },
-  methods: {
-    selectPage () {
-      this.selectPageModal = true
-    },
-    selectPageHandle ({ path, locale }) {
-      this.group.redirectOnLogin = `/${locale}/${path}`
-    },
-    async updateGroup() {
-      try {
-        await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $id: Int!
-              $name: String!
-              $redirectOnLogin: String!
-              $permissions: [String]!
-              $pageRules: [PageRuleInput]!
-            ) {
-              groups {
-                update(
-                  id: $id
-                  name: $name
-                  redirectOnLogin: $redirectOnLogin
-                  permissions: $permissions
-                  pageRules: $pageRules
-                ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            id: this.group.id,
-            name: this.group.name,
-            redirectOnLogin: this.group.redirectOnLogin,
-            permissions: this.group.permissions,
-            pageRules: this.group.pageRules
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-update')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: `Group changes have been saved.`,
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    },
-    async deleteGroup() {
-      this.deleteGroupDialog = false
-      try {
-        await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($id: Int!) {
-              groups {
-                delete(id: $id) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            id: this.group.id
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-delete')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: `Group ${this.group.name} has been deleted.`,
-          icon: 'delete'
-        })
-        this.$router.replace('/groups')
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    },
-    async refresh() {
-      return this.$apollo.queries.group.refetch()
-    }
-  },
-  apollo: {
-    group: {
-      query: gql`
-        query ($id: Int!) {
-          groups {
-            single(id: $id) {
-              id
-              name
-              redirectOnLogin
-              isSystem
-              permissions
-              pageRules {
-                id
-                path
-                roles
-                match
-                deny
-                locales
-              }
-              users {
-                id
-                name
-                email
-              }
-              createdAt
-              updatedAt
-            }
-          }
-        }
-      `,
-      variables() {
-        return {
-          id: _.toSafeInteger(this.$route.params.id)
-        }
-      },
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.groups.single),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 170
client/components/admin/admin-groups.vue

@@ -1,170 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-people.svg', alt='Groups', style='width: 80px;')
-          .admin-header-title
-            .headline.blue--text.text--darken-2.animated.fadeInLeft Groups
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s Manage groups and their permissions
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/groups', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.animated.fadeInDown.wait-p2s.mx-3(color='grey', outlined, @click='refresh', icon)
-            v-icon mdi-refresh
-          v-dialog(v-model='newGroupDialog', max-width='500')
-            template(v-slot:activator='{ on }')
-              v-btn.animated.fadeInDown(color='primary', depressed, v-on='on', large)
-                v-icon(left) mdi-plus
-                span New Group
-            v-card
-              .dialog-header.is-short New Group
-              v-card-text.pt-5
-                v-text-field.md2(
-                  outlined
-                  prepend-icon='mdi-account-group'
-                  v-model='newGroupName'
-                  label='Group Name'
-                  counter='255'
-                  @keyup.enter='createGroup'
-                  @keyup.esc='newGroupDialog = false'
-                  ref='groupNameIpt'
-                  )
-              v-card-chin
-                v-spacer
-                v-btn(text, @click='newGroupDialog = false') Cancel
-                v-btn(color='primary', @click='createGroup') Create
-        v-card.mt-3.animated.fadeInUp
-          v-data-table(
-            :items='groups'
-            :headers='headers'
-            :search='search'
-            :page.sync='pagination'
-            :items-per-page='15'
-            :loading='loading'
-            @page-count='pageCount = $event'
-            must-sort,
-            hide-default-footer
-          )
-            template(slot='item', slot-scope='props')
-              tr.is-clickable(:active='props.selected', @click='$router.push("/groups/" + props.item.id)')
-                td {{ props.item.id }}
-                td: strong {{ props.item.name }}
-                td {{ props.item.userCount }}
-                td {{ props.item.createdAt | moment('calendar') }}
-                td {{ props.item.updatedAt | moment('calendar') }}
-                td
-                  v-tooltip(left, v-if='props.item.isSystem')
-                    template(v-slot:activator='{ on }')
-                      v-icon(v-on='on') mdi-lock-outline
-                    span System Group
-            template(slot='no-data')
-              v-alert.ma-3(icon='mdi-alert', :value='true', outline) No groups to display.
-          .text-xs-center.py-2(v-if='pageCount > 1')
-            v-pagination(v-model='pagination', :length='pageCount')
-</template>
-
-<script>
-import _ from 'lodash'
-
-import groupsQuery from 'gql/admin/groups/groups-query-list.gql'
-import createGroupMutation from 'gql/admin/groups/groups-mutation-create.gql'
-
-export default {
-  data() {
-    return {
-      newGroupDialog: false,
-      newGroupName: '',
-      selectedGroup: {},
-      pagination: 1,
-      pageCount: 0,
-      groups: [],
-      headers: [
-        { text: 'ID', value: 'id', width: 80, sortable: true },
-        { text: 'Name', value: 'name' },
-        { text: 'Users', value: 'userCount', width: 200 },
-        { text: 'Created', value: 'createdAt', width: 250 },
-        { text: 'Last Updated', value: 'updatedAt', width: 250 },
-        { text: '', value: 'isSystem', width: 20, sortable: false }
-      ],
-      search: '',
-      loading: false
-    }
-  },
-  watch: {
-    newGroupDialog(newValue, oldValue) {
-      if (newValue) {
-        this.$nextTick(() => {
-          this.$refs.groupNameIpt.focus()
-        })
-      }
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.groups.refetch()
-      this.$store.commit('showNotification', {
-        message: 'Groups have been refreshed.',
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async createGroup() {
-      if (_.trim(this.newGroupName).length < 1) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: 'Enter a group name.',
-          icon: 'warning'
-        })
-        return
-      }
-      this.newGroupDialog = false
-      try {
-        await this.$apollo.mutate({
-          mutation: createGroupMutation,
-          variables: {
-            name: this.newGroupName
-          },
-          update (store, resp) {
-            const data = _.get(resp, 'data.groups.create', { responseResult: {} })
-            if (data.responseResult.succeeded === true) {
-              const apolloData = store.readQuery({ query: groupsQuery })
-              data.group.userCount = 0
-              apolloData.groups.list.push(data.group)
-              store.writeQuery({ query: groupsQuery, data: apolloData })
-            } else {
-              throw new Error(data.responseResult.message)
-            }
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-create')
-          }
-        })
-        this.newGroupName = ''
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: `Group has been created successfully.`,
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    }
-  },
-  apollo: {
-    groups: {
-      query: groupsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.groups.list,
-      watchLoading (isLoading) {
-        this.loading = isLoading
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

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

@@ -1,302 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-globe-earth.svg', alt='Locale', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:locale.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:locale.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/locales', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.animated.fadeInDown.ml-3(color='success', depressed, @click='save', large, :loading='loading')
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(xl6 lg5 xs12)
-              v-card.wiki-form.animated.fadeInUp
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.settings') }}
-                v-card-text
-                  v-select(
-                    outlined
-                    :items='installedLocales'
-                    prepend-icon='mdi-web'
-                    v-model='selectedLocale'
-                    item-value='code'
-                    item-text='nativeName'
-                    :label='namespacing ? $t("admin:locale.base.labelWithNS") : $t("admin:locale.base.label")'
-                    persistent-hint
-                    :hint='$t("admin:locale.base.hint")'
-                  )
-                    template(slot='item', slot-scope='data')
-                      template(v-if='typeof data.item !== "object"')
-                        v-list-item-content(v-text='data.item')
-                      template(v-else)
-                        v-list-item-avatar
-                          v-avatar.blue.white--text(tile, size='40', v-html='data.item.code.toUpperCase()')
-                        v-list-item-content
-                          v-list-item-title(v-html='data.item.name')
-                          v-list-item-subtitle(v-html='data.item.nativeName')
-                  v-divider.mt-3
-                  v-switch(
-                    inset
-                    v-model='autoUpdate'
-                    :label='$t("admin:locale.autoUpdate.label")'
-                    color='primary'
-                    persistent-hint
-                    :hint='namespacing ? $t("admin:locale.autoUpdate.hintWithNS") : $t("admin:locale.autoUpdate.hint")'
-                  )
-
-              v-card.wiki-form.mt-3.animated.fadeInUp.wait-p2s
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.namespacing') }}
-                v-card-text
-                  v-switch(
-                    inset
-                    v-model='namespacing'
-                    :label='$t("admin:locale.namespaces.label")'
-                    color='primary'
-                    persistent-hint
-                    :hint='$t("admin:locale.namespaces.hint")'
-                    )
-                  v-alert.mt-3(
-                    outlined
-                    color='orange'
-                    :value='true'
-                    icon='mdi-alert'
-                    )
-                    span {{ $t('admin:locale.namespacingPrefixWarning.title', { langCode: selectedLocale }) }}
-                    .caption.grey--text {{ $t('admin:locale.namespacingPrefixWarning.subtitle') }}
-                  v-divider.mt-3.mb-4
-                  v-select(
-                    outlined
-                    :disabled='!namespacing'
-                    :items='installedLocales'
-                    prepend-icon='mdi-web'
-                    multiple
-                    chips
-                    deletable-chips
-                    v-model='namespaces'
-                    item-value='code'
-                    item-text='name'
-                    :label='$t("admin:locale.activeNamespaces.label")'
-                    persistent-hint
-                    small-chips
-                    :hint='$t("admin:locale.activeNamespaces.hint")'
-                    )
-                    template(slot='item', slot-scope='data')
-                      template(v-if='typeof data.item !== "object"')
-                        v-list-item-content(v-text='data.item')
-                      template(v-else)
-                        v-list-item-avatar
-                          v-avatar.blue.white--text(tile, size='40', v-html='data.item.code.toUpperCase()')
-                        v-list-item-content
-                          v-list-item-title(v-html='data.item.name')
-                          v-list-item-subtitle(v-html='data.item.nativeName')
-                        v-list-item-action
-                          v-checkbox(:input-value='data.attrs.inputValue', color='primary', value)
-            v-flex(xl6 lg7 xs12)
-              v-card.animated.fadeInUp.wait-p4s
-                v-toolbar(color='teal', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.downloadTitle') }}
-                v-data-table(
-                  :headers='headers',
-                  :items='locales',
-                  hide-default-footer,
-                  item-key='code',
-                  :items-per-page='1000'
-                  )
-                  template(v-slot:item.code='{ item }')
-                    v-chip.white--text(label, color='teal', small) {{item.code}}
-                  template(v-slot:item.name='{ item }')
-                    strong {{item.name}}
-                  template(v-slot:item.isRTL='{ item }')
-                    v-icon(v-if='item.isRTL') mdi-check
-                  template(v-slot:item.availability='{ item }')
-                    .d-flex.align-center.pl-4
-                      v-progress-circular(:value='item.availability', width='2', size='20', :color='item.availability <= 33 ? `red` : (item.availability <= 66) ? `orange` : `green`')
-                      .caption.mx-2(:class='item.availability <= 33 ? `red--text` : (item.availability <= 66) ? `orange--text` : `green--text`') {{item.availability}}%
-                  template(v-slot:item.isInstalled='{ item }')
-                    v-progress-circular(v-if='item.isDownloading', indeterminate, color='blue', size='20', :width='2')
-                    v-btn(v-else-if='item.isInstalled && item.installDate < item.updatedAt', icon, small, @click='download(item)')
-                      v-icon.blue--text mdi-cached
-                    v-btn(v-else-if='item.isInstalled', icon, small, @click='download(item)')
-                      v-icon.green--text mdi-check-bold
-                    v-btn(v-else, icon, small, @click='download(item)')
-                      v-icon.grey--text mdi-cloud-download
-              v-card.wiki-form.mt-3.animated.fadeInUp.wait-p5s
-                v-toolbar(color='teal', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{ $t('admin:locale.sideload') }}
-                  v-spacer
-                  v-chip(label, color='white', small).teal--text coming soon
-                v-card-text
-                  div {{ $t('admin:locale.sideloadHelp') }}
-                  v-btn.ml-0.mt-3(color='teal', disabled) {{ $t('common:actions.browse') }}
-</template>
-
-<script>
-import _ from 'lodash'
-
-/* global WIKI */
-
-import localesQuery from 'gql/admin/locale/locale-query-list.gql'
-import localesDownloadMutation from 'gql/admin/locale/locale-mutation-download.gql'
-import localesSaveMutation from 'gql/admin/locale/locale-mutation-save.gql'
-
-export default {
-  data() {
-    return {
-      loading: false,
-      locales: [],
-      selectedLocale: 'en',
-      autoUpdate: false,
-      namespacing: false,
-      namespaces: []
-    }
-  },
-  computed: {
-    installedLocales() {
-      return _.filter(this.locales, ['isInstalled', true])
-    },
-    headers() {
-      return [
-        {
-          text: this.$t('admin:locale.code'),
-          align: 'left',
-          value: 'code',
-          width: 90
-        },
-        {
-          text: this.$t('admin:locale.name'),
-          align: 'left',
-          value: 'name'
-        },
-        {
-          text: this.$t('admin:locale.nativeName'),
-          align: 'left',
-          value: 'nativeName'
-        },
-        {
-          text: this.$t('admin:locale.rtl'),
-          align: 'center',
-          value: 'isRTL',
-          sortable: false,
-          width: 10
-        },
-        {
-          text: this.$t('admin:locale.availability'),
-          align: 'center',
-          value: 'availability',
-          sortable: false,
-          width: 120
-        },
-        {
-          text: this.$t('admin:locale.download'),
-          align: 'center',
-          value: 'isInstalled',
-          sortable: false,
-          width: 100
-        }
-      ]
-    }
-  },
-  methods: {
-    async download(lc) {
-      lc.isDownloading = true
-      const respRaw = await this.$apollo.mutate({
-        mutation: localesDownloadMutation,
-        variables: {
-          locale: lc.code
-        }
-      })
-      const resp = _.get(respRaw, 'data.localization.downloadLocale.responseResult', {})
-      if (resp.succeeded) {
-        lc.isDownloading = false
-        lc.isInstalled = true
-        lc.updatedAt = new Date().toISOString()
-        lc.installDate = lc.updatedAt
-        this.$store.commit('showNotification', {
-          message: `Locale ${lc.name} has been installed successfully.`,
-          style: 'success',
-          icon: 'get_app'
-        })
-      } else {
-        this.$store.commit('showNotification', {
-          message: `Error: ${resp.message}`,
-          style: 'error',
-          icon: 'warning'
-        })
-      }
-      this.isDownloading = false
-    },
-    async save() {
-      this.loading = true
-      const respRaw = await this.$apollo.mutate({
-        mutation: localesSaveMutation,
-        variables: {
-          locale: this.selectedLocale,
-          autoUpdate: this.autoUpdate,
-          namespacing: this.namespacing,
-          namespaces: this.namespaces
-        }
-      })
-      const resp = _.get(respRaw, 'data.localization.updateLocale.responseResult', {})
-      if (resp.succeeded) {
-        // Change UI language
-        WIKI.$i18n.i18next.changeLanguage(this.selectedLocale)
-        WIKI.$moment.locale(this.selectedLocale)
-
-        // Check for RTL
-        const curLocale = _.find(this.locales, ['code', this.selectedLocale])
-        this.$vuetify.rtl = curLocale && curLocale.isRTL
-
-        this.$store.commit('showNotification', {
-          message: 'Locale settings updated successfully.',
-          style: 'success',
-          icon: 'check'
-        })
-
-        _.delay(() => {
-          window.location.reload(true)
-        }, 1000)
-      } else {
-        this.$store.commit('showNotification', {
-          message: `Error: ${resp.message}`,
-          style: 'error',
-          icon: 'warning'
-        })
-      }
-      this.loading = false
-    }
-  },
-  apollo: {
-    locales: {
-      query: localesQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.localization.locales.map(lc => ({ ...lc, isDownloading: false })),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-locale-refresh')
-      }
-    },
-    selectedLocale: {
-      query: localesQuery,
-      update: (data) => data.localization.config.locale
-    },
-    autoUpdate: {
-      query: localesQuery,
-      update: (data) => data.localization.config.autoUpdate
-    },
-    namespacing: {
-      query: localesQuery,
-      update: (data) => data.localization.config.namespacing
-    },
-    namespaces: {
-      query: localesQuery,
-      update: (data) => data.localization.config.namespaces
-    }
-  }
-}
-</script>

+ 0 - 106
client/components/admin/admin-logging-console.vue

@@ -1,106 +0,0 @@
-<template lang='pug'>
-  v-dialog(v-model='isShown', width='90vw', max-width='1200')
-    .dialog-header
-      span Live Console
-      v-spacer
-      .caption.blue--text.text--lighten-3.mr-3 Streaming...
-      v-progress-circular(
-        indeterminate
-        color='blue lighten-3'
-        :size='20'
-        :width='2'
-        )
-    .consoleTerm(ref='consoleContainer')
-    v-toolbar(flat, color='grey darken-3', dark)
-      v-spacer
-      v-btn(outline, @click='clear')
-        v-icon(left) cancel_presentation
-        span Clear
-      v-btn(outline, @click='close')
-        v-icon(left) close
-        span Close
-</template>
-
-<script>
-import _ from 'lodash'
-// import { Terminal } from 'xterm'
-// import * as fit from 'xterm/lib/addons/fit/fit'
-
-import livetrailSubscription from 'gql/admin/logging/logging-subscription-livetrail.gql'
-
-// Terminal.applyAddon(fit)
-
-export default {
-  term: null,
-  props: {
-    value: {
-      type: Boolean,
-      default: false
-    }
-  },
-  computed: {
-    isShown: {
-      get() { return this.value },
-      set(val) { this.$emit('input', val) }
-    }
-  },
-  watch: {
-    value(newValue, oldValue) {
-      if (newValue) {
-        _.delay(() => {
-          // this.term = new Terminal()
-          this.term.open(this.$refs.consoleContainer)
-          this.term.writeln('Connecting to \x1B[1;3;31mconsole output\x1B[0m...')
-
-          this.attach()
-        }, 100)
-      } else {
-        this.term.dispose()
-        this.term = null
-      }
-    }
-  },
-  mounted() {
-
-  },
-  methods: {
-    clear() {
-      this.term.clear()
-    },
-    close() {
-      this.isShown = false
-    },
-    attach() {
-      const self = this
-      const observer = this.$apollo.subscribe({
-        query: livetrailSubscription
-      })
-      observer.subscribe({
-        next(data) {
-          const item = _.get(data, `data.loggingLiveTrail`, {})
-          console.info(item)
-          self.term.writeln(`${item.level}: ${item.output}`)
-        },
-        error(error) {
-          self.$store.commit('showNotification', {
-            style: 'red',
-            message: error.message,
-            icon: 'warning'
-          })
-        }
-      })
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-.consoleTerm {
-  background-color: #000;
-  padding: 16px;
-  width: 100%;
-  height: 415px;
-}
-
-</style>

+ 0 - 194
client/components/admin/admin-logging.vue

@@ -1,194 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img(src='/_assets/svg/icon-registry-editor.svg', alt='Logging', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text Logging
-            .subtitle-1.grey--text Configure the system logger(s) #[v-chip(label, color='primary', small).white--text coming soon]
-          v-spacer
-          v-btn(outline, color='grey', @click='refresh', large)
-            v-icon refresh
-          v-btn(color='black', disabled, depressed, @click='toggleConsole', large)
-            v-icon check
-            span Live Trail
-          v-btn(color='success', @click='save', depressed, large)
-            v-icon(left) check
-            span {{$t('common:actions.apply')}}
-
-        v-card.mt-3
-          v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark)
-            v-tab(key='settings'): v-icon settings
-            v-tab(v-for='logger in activeLoggers', :key='logger.key') {{ logger.title }}
-
-            v-tab-item(key='settings', :transition='false', :reverse-transition='false')
-              v-card.pa-3(flat, tile)
-                .body-2.grey--text.text--darken-1 Select which logging service to enable:
-                .caption.grey--text.pb-2 Some loggers require additional configuration in their dedicated tab (when selected).
-                v-form
-                  v-checkbox.my-0(
-                    v-for='(logger, n) in loggers'
-                    v-model='logger.isEnabled'
-                    :key='logger.key'
-                    :label='logger.title'
-                    color='primary'
-                    hide-details
-                    disabled
-                  )
-
-            v-tab-item(v-for='(logger, n) in activeLoggers', :key='logger.key', :transition='false', :reverse-transition='false')
-              v-card.wiki-form.pa-3(flat, tile)
-                v-form
-                  .loggerlogo
-                    img(:src='logger.logo', :alt='logger.title')
-                  v-subheader.pl-0 {{logger.title}}
-                  .caption {{logger.description}}
-                  .caption: a(:href='logger.website') {{logger.website}}
-                  v-divider.mt-3
-                  v-subheader.pl-0 Logger Configuration
-                  .body-1.ml-3(v-if='!logger.config || logger.config.length < 1') This logger has no configuration options you can modify.
-                  template(v-else, v-for='cfg in logger.config')
-                    v-select(
-                      v-if='cfg.value.type === "string" && cfg.value.enum'
-                      outline
-                      background-color='grey lighten-2'
-                      :items='cfg.value.enum'
-                      :key='cfg.key'
-                      :label='cfg.value.title'
-                      v-model='cfg.value.value'
-                      prepend-icon='settings_applications'
-                      :hint='cfg.value.hint ? cfg.value.hint : ""'
-                      persistent-hint
-                      :class='cfg.value.hint ? "mb-2" : ""'
-                    )
-                    v-switch(
-                      v-else-if='cfg.value.type === "boolean"'
-                      :key='cfg.key'
-                      :label='cfg.value.title'
-                      v-model='cfg.value.value'
-                      color='primary'
-                      prepend-icon='settings_applications'
-                      :hint='cfg.value.hint ? cfg.value.hint : ""'
-                      persistent-hint
-                      )
-                    v-text-field(
-                      v-else
-                      outline
-                      background-color='grey lighten-2'
-                      :key='cfg.key'
-                      :label='cfg.value.title'
-                      v-model='cfg.value.value'
-                      prepend-icon='settings_applications'
-                      :hint='cfg.value.hint ? cfg.value.hint : ""'
-                      persistent-hint
-                      :class='cfg.value.hint ? "mb-2" : ""'
-                      )
-                  v-divider.mt-3
-                  v-subheader.pl-0 Log Level
-                  .body-1.ml-3 Select the minimum error level that will be reported to this logger.
-                  v-layout(row)
-                    v-flex(xs12, md6, lg4)
-                      .pt-3
-                        v-select(
-                          single-line
-                          outline
-                          background-color='grey lighten-2'
-                          :items='levels'
-                          label='Level'
-                          v-model='logger.level'
-                          prepend-icon='graphic_eq'
-                          hint='Default: warn'
-                          persistent-hint
-                        )
-
-    logging-console(v-model='showConsole')
-</template>
-
-<script>
-import _ from 'lodash'
-
-import LoggingConsole from './admin-logging-console.vue'
-
-import loggersQuery from 'gql/admin/logging/logging-query-loggers.gql'
-import loggersSaveMutation from 'gql/admin/logging/logging-mutation-save-loggers.gql'
-
-export default {
-  components: {
-    LoggingConsole
-  },
-  data() {
-    return {
-      showConsole: false,
-      loggers: [],
-      levels: ['error', 'warn', 'info', 'debug', 'verbose']
-    }
-  },
-  computed: {
-    activeLoggers() {
-      return _.filter(this.loggers, 'isEnabled')
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.loggers.refetch()
-      this.$store.commit('showNotification', {
-        message: 'List of loggers has been refreshed.',
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save() {
-      this.$store.commit(`loadingStart`, 'admin-logging-saveloggers')
-      await this.$apollo.mutate({
-        mutation: loggersSaveMutation,
-        variables: {
-          loggers: this.loggers.map(tgt => _.pick(tgt, [
-            'isEnabled',
-            'key',
-            'config',
-            'level'
-          ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
-        }
-      })
-      this.$store.commit('showNotification', {
-        message: 'Logging configuration saved successfully.',
-        style: 'success',
-        icon: 'check'
-      })
-      this.$store.commit(`loadingStop`, 'admin-logging-saveloggers')
-    },
-    toggleConsole() {
-      this.showConsole = !this.showConsole
-    }
-  },
-  apollo: {
-    loggers: {
-      query: loggersQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.logging.loggers).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-logging-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss' scoped>
-
-.loggerlogo {
-  width: 250px;
-  height: 85px;
-  float:right;
-  display: flex;
-  justify-content: flex-end;
-  align-items: center;
-
-  img {
-    max-width: 100%;
-    max-height: 50px;
-  }
-}
-
-</style>

+ 0 - 258
client/components/admin/admin-mail.vue

@@ -1,258 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-new-post.svg', alt='Mail', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:mail.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:mail.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(lg6 xs12)
-              v-form
-                v-card.animated.fadeInUp
-                  v-toolbar(color='primary', dark, dense, flat)
-                    v-toolbar-title.subtitle-1 {{ $t('admin:mail.configuration') }}
-                  .overline.pa-4.grey--text {{ $t('admin:mail.sender') }}
-                  .px-4
-                    v-text-field(
-                      outlined
-                      v-model='config.senderName'
-                      :label='$t(`admin:mail.senderName`)'
-                      required
-                      :counter='255'
-                      prepend-icon='mdi-mailbox'
-                      )
-                    v-text-field(
-                      outlined
-                      v-model='config.senderEmail'
-                      :label='$t(`admin:mail.senderEmail`)'
-                      required
-                      :counter='255'
-                      prepend-icon='mdi-mailbox'
-                      )
-                  v-divider
-                  .overline.pa-4.grey--text {{ $t('admin:mail.smtp') }}
-                  .px-4
-                    v-text-field(
-                      outlined
-                      v-model='config.host'
-                      :label='$t(`admin:mail.smtpHost`)'
-                      required
-                      :counter='255'
-                      prepend-icon='mdi-memory'
-                      )
-                    v-text-field(
-                      outlined
-                      v-model='config.port'
-                      :label='$t(`admin:mail.smtpPort`)'
-                      required
-                      prepend-icon='mdi-serial-port'
-                      persistent-hint
-                      :hint='$t(`admin:mail.smtpPortHint`)'
-                      style='max-width: 300px;'
-                      )
-                    v-switch(
-                      v-model='config.secure'
-                      :label='$t(`admin:mail.smtpTLS`)'
-                      color='primary'
-                      persistent-hint
-                      :hint='$t(`admin:mail.smtpTLSHint`)'
-                      prepend-icon='mdi-security-network'
-                      inset
-                      )
-                    v-switch(
-                      v-model='config.verifySSL'
-                      :label='$t(`admin:mail.smtpVerifySSL`)'
-                      color='primary'
-                      persistent-hint
-                      :hint='$t(`admin:mail.smtpVerifySSLHint`)'
-                      prepend-icon='mdi-security-network'
-                      inset
-                      )
-                    v-text-field.mt-8(
-                      outlined
-                      v-model='config.user'
-                      :label='$t(`admin:mail.smtpUser`)'
-                      required
-                      :counter='255'
-                      prepend-icon='mdi-shield-account-outline'
-                      )
-                    v-text-field(
-                      outlined
-                      v-model='config.pass'
-                      :label='$t(`admin:mail.smtpPwd`)'
-                      required
-                      prepend-icon='mdi-form-textbox-password'
-                      type='password'
-                      )
-
-            v-flex(lg6 xs12)
-              v-card.animated.fadeInUp.wait-p2s
-                v-form
-                  v-toolbar(color='primary', dark, dense, flat)
-                    v-toolbar-title.subtitle-1 {{ $t('admin:mail.dkim') }}
-                  v-card-info
-                    span {{ $t('admin:mail.dkimHint') }}
-                  .pa-4
-                    v-switch(
-                      v-model='config.useDKIM'
-                      :label='$t(`admin:mail.dkimUse`)'
-                      color='primary'
-                      prepend-icon='mdi-key'
-                      inset
-                      )
-                    v-text-field(
-                      outlined
-                      v-model='config.dkimDomainName'
-                      :label='$t(`admin:mail.dkimDomainName`)'
-                      :counter='255'
-                      prepend-icon='mdi-key'
-                      :disabled='!config.useDKIM'
-                      )
-                    v-text-field(
-                      outlined
-                      v-model='config.dkimKeySelector'
-                      :label='$t(`admin:mail.dkimKeySelector`)'
-                      :counter='255'
-                      prepend-icon='mdi-key'
-                      :disabled='!config.useDKIM'
-                      )
-                    v-textarea(
-                      outlined
-                      v-model='config.dkimPrivateKey'
-                      :label='$t(`admin:mail.dkimPrivateKey`)'
-                      prepend-icon='mdi-key'
-                      persistent-hint
-                      :hint='$t(`admin:mail.dkimPrivateKeyHint`)'
-                      :disabled='!config.useDKIM'
-                      )
-
-              v-card.mt-3.animated.fadeInUp.wait-p3s
-                v-form
-                  v-toolbar(color='teal', dark, dense, flat)
-                    v-toolbar-title.subtitle-1 {{ $t('admin:mail.test') }}
-                  .pa-4
-                    .body-2.grey--text.text--darken-2 {{ $t('admin:mail.testHint') }}
-                    v-text-field.mt-3(
-                      outlined
-                      v-model='testEmail'
-                      :label='$t(`admin:mail.testRecipient`)'
-                      :counter='255'
-                      prepend-icon='mdi-email-outline'
-                      :disabled='testLoading'
-                      )
-                  v-card-chin
-                    v-spacer
-                    v-btn.px-4(color='teal', dark, @click='sendTest', :loading='testLoading')
-                      v-icon(left) mdi-send
-                      span {{ $t('admin:mail.testSend') }}
-
-</template>
-
-<script>
-import _ from 'lodash'
-import mailConfigQuery from 'gql/admin/mail/mail-query-config.gql'
-import mailUpdateConfigMutation from 'gql/admin/mail/mail-mutation-save-config.gql'
-import mailTestMutation from 'gql/admin/mail/mail-mutation-sendtest.gql'
-
-export default {
-  data() {
-    return {
-      config: {
-        senderName: '',
-        senderEmail: '',
-        host: '',
-        port: 0,
-        secure: false,
-        verifySSL: false,
-        user: '',
-        pass: '',
-        useDKIM: false,
-        dkimDomainName: '',
-        dkimKeySelector: '',
-        dkimPrivateKey: ''
-      },
-      testEmail: '',
-      testLoading: false
-    }
-  },
-  methods: {
-    async save () {
-      try {
-        await this.$apollo.mutate({
-          mutation: mailUpdateConfigMutation,
-          variables: {
-            senderName: this.config.senderName || '',
-            senderEmail: this.config.senderEmail || '',
-            host: this.config.host || '',
-            port: _.toSafeInteger(this.config.port) || 0,
-            secure: this.config.secure || false,
-            verifySSL: this.config.verifySSL || false,
-            user: this.config.user || '',
-            pass: this.config.pass || '',
-            useDKIM: this.config.useDKIM || false,
-            dkimDomainName: this.config.dkimDomainName || '',
-            dkimKeySelector: this.config.dkimKeySelector || '',
-            dkimPrivateKey: this.config.dkimPrivateKey || ''
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-update')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:mail.saveSuccess'),
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    },
-    async sendTest () {
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: mailTestMutation,
-          variables: {
-            recipientEmail: this.testEmail
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-test')
-          }
-        })
-        if (!_.get(resp, 'data.mail.sendTest.responseResult.succeeded', false)) {
-          throw new Error(_.get(resp, 'data.mail.sendTest.responseResult.message', 'An unexpected error occurred.'))
-        }
-
-        this.testEmail = ''
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:mail.sendTestSuccess'),
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    }
-  },
-  apollo: {
-    config: {
-      query: mailConfigQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.mail.config),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 526
client/components/admin/admin-navigation.vue

@@ -1,526 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-triangle-arrow.svg', alt='Navigation', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('navigation.title')}}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('navigation.subtitle')}}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/navigation', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.mx-3.animated.fadeInDown.wait-p2s.mr-3(icon, outlined, color='grey', @click='refresh')
-            v-icon mdi-refresh
-          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-        v-container.pa-0.mt-3(fluid, grid-list-lg)
-          v-row(dense)
-            v-col(cols='3')
-              v-card.animated.fadeInUp
-                v-toolbar(color='teal', dark, dense, flat, height='56')
-                  v-toolbar-title.subtitle-1 {{$t('admin:navigation.mode')}}
-                v-list(nav, two-line)
-                  v-list-item-group(v-model='config.mode', mandatory, :color='$vuetify.theme.dark ? `teal lighten-3` : `teal`')
-                    v-list-item(value='TREE')
-                      v-list-item-avatar
-                        img(src='/_assets/svg/icon-tree-structure-dotted.svg', alt='Site Tree')
-                      v-list-item-content
-                        v-list-item-title {{$t('admin:navigation.modeSiteTree.title')}}
-                        v-list-item-subtitle {{$t('admin:navigation.modeSiteTree.description')}}
-                      v-list-item-avatar
-                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `TREE` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
-                        v-icon(v-else, :color='config.mode === `TREE` ? `teal` : `grey lighten-3`') mdi-check-circle
-                    v-list-item(value='STATIC')
-                      v-list-item-avatar
-                        img(src='/_assets/svg/icon-features-list.svg', alt='Static Navigation')
-                      v-list-item-content
-                        v-list-item-title {{$t('admin:navigation.modeStatic.title')}}
-                        v-list-item-subtitle {{$t('admin:navigation.modeStatic.description')}}
-                      v-list-item-avatar
-                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `STATIC` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
-                        v-icon(v-else, :color='config.mode === `STATIC` ? `teal` : `grey lighten-3`') mdi-check-circle
-                    v-list-item(value='MIXED')
-                      v-list-item-avatar
-                        img(src='/_assets/svg/icon-user-menu-male-dotted.svg', alt='Custom Navigation')
-                      v-list-item-content
-                        v-list-item-title {{$t('admin:navigation.modeCustom.title')}}
-                        v-list-item-subtitle {{$t('admin:navigation.modeCustom.description')}}
-                      v-list-item-avatar
-                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `MIXED` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
-                        v-icon(v-else, :color='config.mode === `MIXED` ? `teal` : `grey lighten-3`') mdi-check-circle
-                    v-list-item(value='NONE')
-                      v-list-item-avatar
-                        img(src='/_assets/svg/icon-cancel-dotted.svg', alt='None')
-                      v-list-item-content
-                        v-list-item-title {{$t('admin:navigation.modeNone.title')}}
-                        v-list-item-subtitle {{$t('admin:navigation.modeNone.description')}}
-                      v-list-item-avatar
-                        v-icon(v-if='$vuetify.theme.dark', :color='config.mode === `none` ? `teal lighten-3` : `grey darken-2`') mdi-check-circle
-                        v-icon(v-else, :color='config.mode === `none` ? `teal` : `grey lighten-3`') mdi-check-circle
-            v-col(cols='9', v-if='config.mode === `MIXED` || config.mode === `STATIC`')
-              v-card.animated.fadeInUp.wait-p2s
-                v-row(no-gutters, align='stretch')
-                  v-col(style='flex: 0 0 350px;')
-                    v-card.grey(flat, style='height: 100%; border-radius: 4px 0 0 4px;', :class='$vuetify.theme.dark ? `darken-4-l5` : `lighten-3`')
-                      .teal.lighten-1.pa-2.d-flex(style='margin-bottom: 1px; height:56px;')
-                        v-select(
-                          :disabled='locales.length < 2'
-                          label='Locale'
-                          hide-details
-                          solo
-                          flat
-                          background-color='teal darken-2'
-                          dark
-                          dense
-                          v-model='currentLang'
-                          :items='locales'
-                          item-text='nativeName'
-                          item-value='code'
-                        )
-                        v-tooltip(top)
-                          template(v-slot:activator='{ on }')
-                            v-btn.ml-2(icon, tile, color='white', v-on='on', @click='copyFromLocaleDialogIsShown = true')
-                              v-icon mdi-arrange-send-backward
-                          span {{$t('admin:navigation.copyFromLocale')}}
-                      v-list.py-2(dense, nav, dark, class='blue darken-2', style='border-radius: 0;')
-                        v-list-item(v-if='currentTree.length < 1')
-                          v-list-item-avatar(size='24'): v-icon(color='blue lighten-3') mdi-alert
-                          v-list-item-content
-                            em.caption.blue--text.text--lighten-4 {{$t('navigation.emptyList')}}
-                        draggable(v-model='currentTree')
-                          template(v-for='navItem in currentTree')
-                            v-list-item(
-                              v-if='navItem.kind === "link"'
-                              :key='navItem.id'
-                              :class='(navItem === current) ? "blue" : ""'
-                              @click='selectItem(navItem)'
-                              )
-                              v-list-item-avatar(size='24', tile)
-                                v-icon(v-if='navItem.icon.match(/fa[a-z] fa-/)', size='19') {{ navItem.icon }}
-                                v-icon(v-else) {{ navItem.icon }}
-                              v-list-item-title {{navItem.label}}
-                            .py-2.clickable(
-                              v-else-if='navItem.kind === "divider"'
-                              :key='navItem.id'
-                              :class='(navItem === current) ? "blue" : ""'
-                              @click='selectItem(navItem)'
-                              )
-                              v-divider
-                            v-subheader.pl-4.clickable(
-                              v-else-if='navItem.kind === "header"'
-                              :key='navItem.id'
-                              :class='(navItem === current) ? "blue" : ""'
-                              @click='selectItem(navItem)'
-                              ) {{navItem.label}}
-                      v-card-chin
-                        v-menu(offset-y, bottom, min-width='200px', style='flex: 1 1;')
-                          template(v-slot:activator='{ on }')
-                            v-btn(v-on='on', color='primary', depressed, block)
-                              v-icon(left) mdi-plus
-                              span {{$t('common:actions.add')}}
-                          v-list
-                            v-list-item(@click='addItem("link")')
-                              v-list-item-avatar(size='24'): v-icon mdi-link
-                              v-list-item-title {{$t('navigation.link')}}
-                            v-list-item(@click='addItem("header")')
-                              v-list-item-avatar(size='24'): v-icon mdi-format-title
-                              v-list-item-title {{$t('navigation.header')}}
-                            v-list-item(@click='addItem("divider")')
-                              v-list-item-avatar(size='24'): v-icon mdi-minus
-                              v-list-item-title {{$t('navigation.divider')}}
-                  v-col
-                    v-card(flat, style='border-radius: 0 4px 4px 0;')
-                      template(v-if='current.kind === "link"')
-                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
-                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.link') })}}
-                          v-spacer
-                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
-                            v-icon(left) mdi-delete
-                            span {{$t('navigation.delete', { kind: $t('navigation.link') })}}
-                        v-card-text
-                          v-text-field(
-                            outlined
-                            :label='$t("navigation.label")'
-                            prepend-icon='mdi-format-title'
-                            v-model='current.label'
-                            counter='255'
-                          )
-                          v-text-field(
-                            outlined
-                            :label='$t("navigation.icon")'
-                            prepend-icon='mdi-dice-5'
-                            v-model='current.icon'
-                            hide-details
-                          )
-                          .caption.pt-3.pl-5 The default icon set is #[strong Material Design Icons]. In order to use another icon set, you must first select it in the Theme administration section.
-                          .caption.pt-3.pl-5: strong Material Design Icons
-                          .caption.pl-5 Refer to the #[a(href='https://materialdesignicons.com/', target='_blank') Material Design Icons Reference] for the list of all possible values. You must prefix all values with #[code mdi-], e.g. #[code mdi-home]
-                          .caption.pt-3.pl-5: strong Font Awesome 5
-                          .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/icons?d=gallery&m=free', target='_blank') Font Awesome 5 Reference] for the list of all possible values. You must prefix all values with #[code fas fa-], e.g. #[code fas fa-home]. Note that some icons use different prefixes (e.g. #[code fab], #[code fad], #[code fal], #[code far]).
-                          .caption.pt-3.pl-5: strong Font Awesome 4
-                          .caption.pl-5 Refer to the #[a(href='https://fontawesome.com/v4.7.0/icons/', target='_blank') Font Awesome 4 Reference] for the list of all possible values. You must prefix all values with #[code fa fa-], e.g. #[code fa fa-home]
-                        v-divider
-                        v-card-text
-                          v-select(
-                            outlined
-                            :label='$t("navigation.targetType")'
-                            prepend-icon='mdi-near-me'
-                            :items='navTypes'
-                            v-model='current.targetType'
-                            hide-details
-                          )
-                          v-text-field.mt-4(
-                            v-if='current.targetType === `external` || current.targetType === `externalblank`'
-                            outlined
-                            :label='$t("navigation.target")'
-                            prepend-icon='mdi-near-me'
-                            v-model='current.target'
-                            hide-details
-                          )
-                          .d-flex.align-center.mt-4(v-else-if='current.targetType === "page"')
-                            v-btn.ml-8(
-                              color='primary'
-                              dark
-                              @click='selectPage'
-                              )
-                              v-icon(left) mdi-magnify
-                              span {{$t('admin:navigation.selectPageButton')}}
-                            .caption.ml-4.primary--text {{current.target}}
-                          v-text-field(
-                            v-else-if='current.targetType === `search`'
-                            outlined
-                            :label='$t("navigation.navType.searchQuery")'
-                            prepend-icon='search'
-                            v-model='current.target'
-                          )
-                        v-divider
-
-                      template(v-else-if='current.kind === "header"')
-                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
-                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.header') })}}
-                          v-spacer
-                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
-                            v-icon(left) mdi-delete
-                            span {{$t('navigation.delete', { kind: $t('navigation.header') })}}
-                        v-card-text
-                          v-text-field(
-                            outlined
-                            :label='$t("navigation.label")'
-                            prepend-icon='mdi-format-title'
-                            v-model='current.label'
-                          )
-                        v-divider
-
-                      div(v-else-if='current.kind === "divider"')
-                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
-                          .subtitle-1 {{$t('navigation.edit', { kind: $t('navigation.divider') })}}
-                          v-spacer
-                          v-btn.px-5(color='white', outlined, @click='deleteItem(current)')
-                            v-icon(left) mdi-delete
-                            span {{$t('navigation.delete', { kind: $t('navigation.divider') })}}
-
-                      v-card-text(v-if='current.kind')
-                        v-radio-group.pl-8(v-model='current.visibilityMode', mandatory, hide-details)
-                          v-radio(:label='$t("admin:navigation.visibilityMode.all")', value='all', color='primary')
-                          v-radio.mt-3(:label='$t("admin:navigation.visibilityMode.restricted")', value='restricted', color='primary')
-                        .pl-8
-                          v-select.pl-8.mt-3(
-                            item-text='name'
-                            item-value='id'
-                            outlined
-                            prepend-icon='mdi-account-group'
-                            label='Groups'
-                            :disabled='current.visibilityMode !== `restricted`'
-                            v-model='current.visibilityGroups'
-                            :items='groups'
-                            persistent-hint
-                            clearable
-                            multiple
-                          )
-                      template(v-else)
-                        v-toolbar(height='56', color='teal lighten-1', flat, dark)
-                        v-card-text.grey--text(v-if='currentTree.length > 0') {{$t('navigation.noSelectionText')}}
-                        v-card-text.grey--text(v-else) {{$t('navigation.noItemsText')}}
-
-    v-dialog(v-model='copyFromLocaleDialogIsShown', max-width='650', persistent)
-      v-card
-        .dialog-header.is-short.is-teal
-          v-icon.mr-3(color='white') mdi-arrange-send-backward
-          span {{$t('admin:navigation.copyFromLocale')}}
-        v-card-text.pt-5
-          .body-2 {{$t('admin:navigation.copyFromLocaleInfoText')}}
-          v-select.mt-3(
-            :items='locales'
-            item-text='nativeName'
-            item-value='code'
-            outlined
-            prepend-icon='mdi-web'
-            v-model='copyFromLocaleCode'
-            :label='$t(`admin:navigation.sourceLocale`)'
-            :hint='$t(`admin:navigation.sourceLocaleHint`)'
-            persistent-hint
-            )
-        v-card-chin
-          v-spacer
-          v-btn(text, @click='copyFromLocaleDialogIsShown = false') {{$t('common:actions.cancel')}}
-          v-btn.px-3(depressed, color='primary', @click='copyFromLocale')
-            v-icon(left) mdi-chevron-right
-            span {{$t('common:actions.copy')}}
-
-    page-selector(mode='select', v-model='selectPageModal', :open-handler='selectPageHandle', path='home', :locale='currentLang')
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-import { v4 as uuid } from 'uuid'
-
-import groupsQuery from 'gql/admin/users/users-query-groups.gql'
-
-import draggable from 'vuedraggable'
-
-/* global siteConfig, siteLangs */
-
-export default {
-  components: {
-    draggable
-  },
-  data() {
-    return {
-      selectPageModal: false,
-      trees: [],
-      current: {},
-      currentLang: siteConfig.lang,
-      groups: [],
-      copyFromLocaleDialogIsShown: false,
-      config: {
-        mode: 'NONE'
-      },
-      allLocales: [],
-      copyFromLocaleCode: 'en'
-    }
-  },
-  computed: {
-    navTypes () {
-      return [
-        { text: this.$t('navigation.navType.external'), value: 'external' },
-        { text: this.$t('navigation.navType.externalblank'), value: 'externalblank' },
-        { text: this.$t('navigation.navType.home'), value: 'home' },
-        { text: this.$t('navigation.navType.page'), value: 'page' }
-        // { text: this.$t('navigation.navType.searchQuery'), value: 'search' }
-      ]
-    },
-    locales () {
-      return _.intersectionBy(this.allLocales, _.unionBy(siteLangs, [{ code: 'en' }, { code: siteConfig.lang }], 'code'), 'code')
-    },
-    currentTree: {
-      get () {
-        return _.get(_.find(this.trees, ['locale', this.currentLang]), 'items', null) || []
-      },
-      set (val) {
-        const tree = _.find(this.trees, ['locale', this.currentLang])
-        if (tree) {
-          tree.items = val
-        } else {
-          this.trees = [...this.trees, {
-            locale: this.currentLang,
-            items: val
-          }]
-        }
-      }
-    }
-  },
-  watch: {
-    currentLang (newValue, oldValue) {
-      this.$nextTick(() => {
-        if (this.currentTree.length > 0) {
-          this.current = this.currentTree[0]
-        } else {
-          this.current = {}
-        }
-      })
-    }
-  },
-  methods: {
-    addItem(kind) {
-      let newItem = {
-        id: uuid(),
-        kind,
-        visibilityMode: 'all',
-        visibilityGroups: []
-      }
-      switch (kind) {
-        case 'link':
-          newItem = {
-            ...newItem,
-            label: this.$t('navigation.untitled', { kind: this.$t(`navigation.link`) }),
-            icon: 'mdi-chevron-right',
-            targetType: 'home',
-            target: ''
-          }
-          break
-        case 'header':
-          newItem.label = this.$t('navigation.untitled', { kind: this.$t(`navigation.header`) })
-          break
-      }
-      this.currentTree = [...this.currentTree, newItem]
-      this.current = newItem
-    },
-    deleteItem(item) {
-      this.currentTree = _.pull(this.currentTree, item)
-      this.current = {}
-    },
-    selectItem(item) {
-      this.current = item
-    },
-    selectPage() {
-      this.selectPageModal = true
-    },
-    selectPageHandle ({ path, locale }) {
-      this.current.target = `/${locale}/${path}`
-    },
-    copyFromLocale () {
-      this.copyFromLocaleDialogIsShown = false
-      this.currentTree = [...this.currentTree, ..._.get(_.find(this.trees, ['locale', this.copyFromLocaleCode]), 'items', null) || []]
-    },
-    async save() {
-      this.$store.commit(`loadingStart`, 'admin-navigation-save')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($tree: [NavigationTreeInput]!, $mode: NavigationMode!) {
-              navigation{
-                updateTree(tree: $tree) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                },
-                updateConfig(mode: $mode) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            tree: this.trees,
-            mode: this.config.mode
-          }
-        })
-        if (_.get(resp, 'data.navigation.updateTree.responseResult.succeeded', false) && _.get(resp, 'data.navigation.updateConfig.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            message: this.$t('navigation.saveSuccess'),
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(_.get(resp, 'data.navigation.updateTree.responseResult.message', 'An unexpected error occurred.'))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-navigation-save')
-    },
-    async refresh() {
-      await this.$apollo.queries.trees.refetch()
-      this.current = {}
-      this.$store.commit('showNotification', {
-        message: 'Navigation has been refreshed.',
-        style: 'success',
-        icon: 'cached'
-      })
-    }
-  },
-  apollo: {
-    config: {
-      query: gql`
-        {
-          navigation {
-            config {
-              mode
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.navigation.config),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-config')
-      }
-    },
-    trees: {
-      query: gql`
-        {
-          navigation {
-            tree {
-              locale
-              items {
-                id
-                kind
-                label
-                icon
-                targetType
-                target
-                visibilityMode
-                visibilityGroups
-              }
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.navigation.tree),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-tree')
-      }
-    },
-    groups: {
-      query: groupsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.groups.list,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-groups')
-      }
-    },
-    allLocales: {
-      query: gql`
-        {
-          localization {
-            locales {
-              code
-              name
-              nativeName
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => data.localization.locales,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-navigation-locales')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss' scoped>
-
-.clickable {
-  cursor: pointer;
-
-  &:hover {
-    background-color: rgba(mc('blue', '500'), .25);
-  }
-}
-
-</style>

+ 0 - 235
client/components/admin/admin-pages-edit.vue

@@ -1,235 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap, v-if='page.id')
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-view-details.svg', alt='Edit Page', style='width: 80px;')
-          .admin-header-title
-            .headline.blue--text.text--darken-2.animated.fadeInLeft Page Details
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s
-              v-chip.ml-0.mr-2(label, small).caption ID {{page.id}}
-              span /{{page.locale}}/{{page.path}}
-          v-spacer
-          template(v-if='page.isPublished')
-            status-indicator.mr-3(positive, pulse)
-            .caption.green--text {{$t('common:page.published')}}
-          template(v-else)
-            status-indicator.mr-3(negative, pulse)
-            .caption.red--text {{$t('common:page.unpublished')}}
-          template(v-if='page.isPrivate')
-            status-indicator.mr-3.ml-4(intermediary, pulse)
-            .caption.deep-orange--text {{$t('common:page.private')}}
-          template(v-else)
-            status-indicator.mr-3.ml-4(active, pulse)
-            .caption.blue--text {{$t('common:page.global')}}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(color='grey', icon, outlined, to='/pages')
-            v-icon mdi-arrow-left
-          v-menu(offset-y, origin='top right')
-            template(v-slot:activator='{ on }')
-              v-btn.mx-3.animated.fadeInDown.wait-p2s(color='black', v-on='on', depressed, dark)
-                span Actions
-                v-icon(right) mdi-chevron-down
-            v-list(dense, nav)
-              v-list-item(:href='`/` + page.locale + `/` + page.path')
-                v-list-item-icon
-                  v-icon(color='indigo') mdi-text-subject
-                v-list-item-title View
-              v-list-item(:href='`/e/` + page.locale + `/` + page.path')
-                v-list-item-icon
-                  v-icon(color='indigo') mdi-pencil
-                v-list-item-title Edit
-              v-list-item(@click='', disabled)
-                v-list-item-icon
-                  v-icon(color='grey') mdi-cube-scan
-                v-list-item-title Re-Render
-              v-list-item(@click='', disabled)
-                v-list-item-icon
-                  v-icon(color='grey') mdi-earth-remove
-                v-list-item-title Unpublish
-              v-list-item(:href='`/s/` + page.locale + `/` + page.path')
-                v-list-item-icon
-                  v-icon(color='indigo') mdi-code-tags
-                v-list-item-title View Source
-              v-list-item(:href='`/h/` + page.locale + `/` + page.path')
-                v-list-item-icon
-                  v-icon(color='indigo') mdi-history
-                v-list-item-title View History
-              v-list-item(@click='', disabled)
-                v-list-item-icon
-                  v-icon(color='grey') mdi-content-duplicate
-                v-list-item-title Duplicate
-              v-list-item(@click='', disabled)
-                v-list-item-icon
-                  v-icon(color='grey') mdi-content-save-move-outline
-                v-list-item-title Move / Rename
-              v-dialog(v-model='deletePageDialog', max-width='500')
-                template(v-slot:activator='{ on }')
-                  v-list-item(v-on='on')
-                    v-list-item-icon
-                      v-icon(color='red') mdi-trash-can-outline
-                    v-list-item-title Delete
-                v-card
-                  .dialog-header.is-short.is-red
-                    v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
-                    span {{$t('common:page.delete')}}
-                  v-card-text.pt-5
-                    i18next.body-2(path='common:page.deleteTitle', tag='div')
-                      span.red--text.text--darken-2(place='title') {{page.title}}
-                    .caption {{$t('common:page.deleteSubtitle')}}
-                    v-chip.mt-3.ml-0.mr-1(label, color='red lighten-4', disabled, small)
-                      .caption.red--text.text--darken-2 {{page.locale.toUpperCase()}}
-                    v-chip.mt-3.mx-0(label, color='red lighten-5', disabled, small)
-                      span.red--text.text--darken-2 /{{page.path}}
-                  v-card-chin
-                    v-spacer
-                    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.animated.fadeInDown(color='success', large, depressed, disabled)
-            v-icon(left) mdi-check
-            span Save Changes
-      v-flex(xs12, lg6)
-        v-card.animated.fadeInUp
-          v-toolbar(color='primary', dense, dark, flat)
-            v-icon.mr-2 mdi-text-subject
-            span Properties
-          v-list.py-0(two-line, dense)
-            v-list-item
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Title
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.title }}
-            v-divider
-            v-list-item
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Description
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.description || '-' }}
-            v-divider
-            v-list-item
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Locale
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.locale }}
-            v-divider
-            v-list-item
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Path
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.path }}
-            v-divider
-            v-list-item
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Editor
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.editor || '?' }}
-            v-divider
-            v-list-item
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Content Type
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.contentType || '?' }}
-            v-divider
-            v-list-item
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Page Hash
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.hash }}
-
-      v-flex(xs12, lg6)
-        v-card.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, dark, flat)
-            v-icon.mr-2 mdi-account-multiple
-            span Users
-          v-list.py-0(two-line, dense)
-            v-list-item
-              v-list-item-avatar(size='24')
-                v-btn(icon, :to='`/users/` + page.creatorId')
-                  v-icon(color='grey') mdi-account
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Creator
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.creatorName }} #[em.caption ({{ page.creatorEmail }})]
-              v-list-item-action
-                v-list-item-action-text {{ page.createdAt | moment('calendar') }}
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='24')
-                v-btn(icon, :to='`/users/` + page.authorId')
-                  v-icon(color='grey') mdi-account
-              v-list-item-content
-                v-list-item-title: .overline.grey--text Last Editor
-                v-list-item-subtitle.body-2(:class='$vuetify.theme.dark ? `grey--text text--lighten-2` : `grey--text text--darken-3`') {{ page.authorName }} #[em.caption ({{ page.authorEmail }})]
-              v-list-item-action
-                v-list-item-action-text {{ page.updatedAt | moment('calendar') }}
-
-    v-layout(row, align-center, v-else)
-      v-progress-circular(indeterminate, width='2', color='grey')
-      .body-2.pl-3.grey--text {{ $t('common:page.loading') }}
-
-</template>
-<script>
-import _ from 'lodash'
-import { StatusIndicator } from 'vue-status-indicator'
-
-import pageQuery from 'gql/admin/pages/pages-query-single.gql'
-import deletePageMutation from 'gql/common/common-pages-mutation-delete.gql'
-
-export default {
-  components: {
-    StatusIndicator
-  },
-  data() {
-    return {
-      deletePageDialog: false,
-      page: {},
-      loading: false
-    }
-  },
-  methods: {
-    async deletePage() {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'page-delete')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: deletePageMutation,
-          variables: {
-            id: this.page.id
-          }
-        })
-        if (_.get(resp, 'data.pages.delete.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'green',
-            message: `Page deleted successfully.`,
-            icon: 'check'
-          })
-          this.$router.replace('/pages')
-        } else {
-          throw new Error(_.get(resp, 'data.pages.delete.responseResult.message', this.$t('common:error.unexpected')))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'page-delete')
-    },
-    async rerenderPage() {
-      this.$store.commit('showNotification', {
-        style: 'indigo',
-        message: `Coming soon...`,
-        icon: 'directions_boat'
-      })
-    }
-  },
-  apollo: {
-    page: {
-      query: pageQuery,
-      variables() {
-        return {
-          id: _.toSafeInteger(this.$route.params.id)
-        }
-      },
-      fetchPolicy: 'network-only',
-      update: (data) => data.pages.single,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 405
client/components/admin/admin-pages-visualize.vue

@@ -1,405 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-venn-diagram.svg', alt='Visualize Pages', style='width: 80px;')
-          .admin-header-title
-            .headline.blue--text.text--darken-2.animated.fadeInLeft Visualize Pages
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Dendrogram representation of your pages
-          v-spacer
-          v-select.mx-5.animated.fadeInDown.wait-p1s(
-            v-if='locales.length > 0'
-            v-model='currentLocale'
-            :items='locales'
-            style='flex: 0 1 120px;'
-            solo
-            dense
-            hide-details
-            item-value='code'
-            item-text='name'
-          )
-          v-btn-toggle.animated.fadeInDown(v-model='graphMode', color='primary', dense, rounded)
-            v-btn.px-5(value='htree')
-              v-icon(left, :color='graphMode === `htree` ? `primary` : `grey darken-3`') mdi-sitemap
-              span.text-none Hierarchical Tree
-            v-btn.px-5(value='hradial')
-              v-icon(left, :color='graphMode === `hradial` ? `primary` : `grey darken-3`') mdi-chart-donut-variant
-              span.text-none Hierarchical Radial
-            v-btn.px-5(value='rradial')
-              v-icon(left, :color='graphMode === `rradial` ? `primary` : `grey darken-3`') mdi-blur-radial
-              span.text-none Relational Radial
-        .admin-pages-visualize-svg(ref='svgContainer', v-show='pages.length >= 1')
-        v-alert(v-if='pages.length < 1', outlined, type='warning', style='max-width: 650px; margin: 0 auto;') Looks like there's no data yet to graph!
-</template>
-
-<script>
-import _ from 'lodash'
-import * as d3 from 'd3'
-import gql from 'graphql-tag'
-
-/* global siteConfig, siteLangs */
-
-export default {
-  data() {
-    return {
-      graphMode: 'htree',
-      width: 800,
-      radius: 400,
-      pages: [],
-      locales: siteLangs,
-      currentLocale: siteConfig.lang
-    }
-  },
-  watch: {
-    pages () {
-      this.redraw()
-    },
-    graphMode () {
-      this.redraw()
-    }
-  },
-  methods: {
-    goToPage (d) {
-      const id = d.data.id
-      if (id) {
-        if (d3.event.ctrlKey || d3.event.metaKey) {
-          const { href } = this.$router.resolve(String(id))
-          window.open(href, '_blank')
-        } else {
-          this.$router.push(String(id))
-        }
-      }
-    },
-    bilink (root) {
-      const map = new Map(root.descendants().map(d => [d.data.path, d]))
-      for (const d of root.descendants()) {
-        d.incoming = []
-        d.outgoing = []
-        d.data.links.forEach(i => {
-          const relNode = map.get(i)
-          if (relNode) {
-            d.outgoing.push([d, relNode])
-          }
-        })
-      }
-      for (const d of root.descendants()) {
-        for (const o of d.outgoing) {
-          if (o[1]) {
-            o[1].incoming.push(o)
-          }
-        }
-      }
-      return root
-    },
-    hierarchy (pages) {
-      const map = new Map(pages.map(p => [p.path, p]))
-      const getPage = path => map.get(path) || {
-        path: path,
-        title: path.split('/').slice(-1)[0],
-        links: []
-      }
-
-      function recurse (depth, [parent, descendants]) {
-        const truncatePath = path => _.take(path.split('/'), depth).join('/')
-        const descendantsByChild =
-          Object.entries(_.groupBy(descendants, page => truncatePath(page.path)))
-            .map(([childPath, descendantsGroup]) => [getPage(childPath), descendantsGroup])
-            .map(([child, descendantsGroup]) =>
-              [child, _.filter(descendantsGroup, d => d.path !== child.path)])
-        return {
-          ...parent,
-          children: descendantsByChild.map(_.partial(recurse, depth + 1))
-        }
-      }
-      const root = { path: this.currentLocale, title: this.currentLocale, links: [] }
-      // start at depth=2 because we're taking {locale} as the root and
-      // all paths start with {locale}/
-      return recurse(2, [root, pages])
-    },
-    /**
-     * Relational Radial
-     */
-    drawRelations () {
-      const data = this.hierarchy(this.pages)
-
-      const line = d3.lineRadial()
-        .curve(d3.curveBundle.beta(0.85))
-        .radius(d => d.y)
-        .angle(d => d.x)
-
-      const tree = d3.cluster()
-        .size([2 * Math.PI, this.radius - 100])
-
-      const root = tree(this.bilink(d3.hierarchy(data)
-        .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.path, b.data.path))))
-
-      const svg = d3.create('svg')
-        .attr('viewBox', [-this.width / 2, -this.width / 2, this.width, this.width])
-
-      const g = svg.append('g')
-
-      svg.call(d3.zoom().on('zoom', function() {
-        g.attr('transform', d3.event.transform)
-      }))
-
-      const link = g.append('g')
-        .attr('stroke', '#CCC')
-        .attr('fill', 'none')
-        .selectAll('path')
-        .data(root.descendants().flatMap(leaf => leaf.outgoing))
-        .join('path')
-        .style('mix-blend-mode', 'multiply')
-        .attr('d', ([i, o]) => line(i.path(o)))
-        .each(function(d) { d.path = this })
-
-      g.append('g')
-        .attr('font-family', 'sans-serif')
-        .attr('font-size', 10)
-        .selectAll('g')
-        .data(root.descendants())
-        .join('g')
-        .attr('transform', d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y},0)`)
-        .append('text')
-        .attr('dy', '0.31em')
-        .attr('x', d => d.x < Math.PI ? 6 : -6)
-        .attr('text-anchor', d => d.x < Math.PI ? 'start' : 'end')
-        .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
-        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
-        .attr('cursor', 'pointer')
-        .text(d => d.data.title)
-        .each(function(d) { d.text = this })
-        .on('mouseover', overed)
-        .on('mouseout', outed)
-        .on('click', d => this.goToPage(d))
-        .call(text => text.append('title').text(d => `${d.data.path}
-          ${d.outgoing.length} outgoing
-          ${d.incoming.length} incoming`))
-        .clone(true).lower()
-        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
-
-      function overed(d) {
-        link.style('mix-blend-mode', null)
-        d3.select(this).attr('font-weight', 'bold')
-        d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', '#2196F3').raise()
-        d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', '#2196F3').attr('font-weight', 'bold')
-        d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', '#E91E63').raise()
-        d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', '#E91E63').attr('font-weight', 'bold')
-      }
-
-      function outed(d) {
-        link.style('mix-blend-mode', 'multiply')
-        d3.select(this).attr('font-weight', null)
-        d3.selectAll(d.incoming.map(d => d.path)).attr('stroke', null)
-        d3.selectAll(d.incoming.map(([d]) => d.text)).attr('fill', null).attr('font-weight', null)
-        d3.selectAll(d.outgoing.map(d => d.path)).attr('stroke', null)
-        d3.selectAll(d.outgoing.map(([, d]) => d.text)).attr('fill', null).attr('font-weight', null)
-      }
-
-      this.$refs.svgContainer.appendChild(svg.node())
-    },
-    /**
-     * Hierarchical Tree
-     */
-    drawTree () {
-      const data = this.hierarchy(this.pages)
-
-      const treeRoot = d3.hierarchy(data)
-      treeRoot.dx = 10
-      treeRoot.dy = this.width / (treeRoot.height + 1)
-      const root = d3.tree().nodeSize([treeRoot.dx, treeRoot.dy])(treeRoot)
-
-      let x0 = Infinity
-      let x1 = -x0
-      root.each(d => {
-        if (d.x > x1) x1 = d.x
-        if (d.x < x0) x0 = d.x
-      })
-
-      const svg = d3.create('svg')
-        .attr('viewBox', [0, 0, this.width, x1 - x0 + root.dx * 2])
-
-      // this extra level is necessary because the element that we
-      // apply the zoom tranform to must be above the element where
-      // we apply the translation (`g`), or else zoom is wonky
-      const gZoom = svg.append('g')
-
-      svg.call(d3.zoom().on('zoom', function() {
-        gZoom.attr('transform', d3.event.transform)
-      }))
-
-      const g = gZoom.append('g')
-        .attr('font-family', 'sans-serif')
-        .attr('font-size', 10)
-        .attr('transform', `translate(${root.dy / 3},${root.dx - x0})`)
-
-      g.append('g')
-        .attr('fill', 'none')
-        .attr('stroke', this.$vuetify.theme.dark ? '#999' : '#555')
-        .attr('stroke-opacity', 0.4)
-        .attr('stroke-width', 1.5)
-        .selectAll('path')
-        .data(root.links())
-        .join('path')
-        .attr('d', d3.linkHorizontal()
-          .x(d => d.y)
-          .y(d => d.x))
-
-      const node = g.append('g')
-        .attr('stroke-linejoin', 'round')
-        .attr('stroke-width', 3)
-        .selectAll('g')
-        .data(root.descendants())
-        .join('g')
-        .attr('transform', d => `translate(${d.y},${d.x})`)
-
-      node.append('circle')
-        .attr('fill', d => d.children ? '#555' : '#999')
-        .attr('r', 2.5)
-
-      node.append('text')
-        .attr('dy', '0.31em')
-        .attr('x', d => d.children ? -6 : 6)
-        .attr('text-anchor', d => d.children ? 'end' : 'start')
-        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
-        .attr('cursor', 'pointer')
-        .text(d => d.data.title)
-        .on('click', d => this.goToPage(d))
-        .clone(true).lower()
-        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
-
-      this.$refs.svgContainer.appendChild(svg.node())
-    },
-    /**
-     * Hierarchical Radial
-     */
-    drawRadialTree () {
-      const data = this.hierarchy(this.pages)
-
-      const tree = d3.tree()
-        .size([2 * Math.PI, this.radius])
-        .separation((a, b) => (a.parent === b.parent ? 1 : 2) / a.depth)
-
-      const root = tree(d3.hierarchy(data)
-        .sort((a, b) => d3.ascending(a.data.title, b.data.title)))
-
-      const svg = d3.create('svg')
-        .style('font', '10px sans-serif')
-
-      const g = svg.append('g')
-
-      svg.call(d3.zoom().on('zoom', function () {
-        g.attr('transform', d3.event.transform)
-      }))
-
-      // eslint-disable-next-line no-unused-vars
-      const link = g.append('g')
-        .attr('fill', 'none')
-        .attr('stroke', this.$vuetify.theme.dark ? 'white' : '#555')
-        .attr('stroke-opacity', 0.4)
-        .attr('stroke-width', 1.5)
-        .selectAll('path')
-        .data(root.links())
-        .join('path')
-        .attr('d', d3.linkRadial()
-          .angle(d => d.x)
-          .radius(d => d.y))
-
-      const node = g.append('g')
-        .attr('stroke-linejoin', 'round')
-        .attr('stroke-width', 3)
-        .selectAll('g')
-        .data(root.descendants().reverse())
-        .join('g')
-        .attr('transform', d => `
-          rotate(${d.x * 180 / Math.PI - 90})
-          translate(${d.y},0)
-        `)
-
-      node.append('circle')
-        .attr('fill', d => d.children ? '#555' : '#999')
-        .attr('r', 2.5)
-
-      node.append('text')
-        .attr('dy', '0.31em')
-        /* eslint-disable no-mixed-operators */
-        .attr('x', d => d.x < Math.PI === !d.children ? 6 : -6)
-        .attr('text-anchor', d => d.x < Math.PI === !d.children ? 'start' : 'end')
-        .attr('transform', d => d.x >= Math.PI ? 'rotate(180)' : null)
-        /* eslint-enable no-mixed-operators */
-        .attr('fill', this.$vuetify.theme.dark ? 'white' : '')
-        .attr('cursor', 'pointer')
-        .text(d => d.data.title)
-        .on('click', d => this.goToPage(d))
-        .clone(true).lower()
-        .attr('stroke', this.$vuetify.theme.dark ? '#222' : 'white')
-
-      this.$refs.svgContainer.appendChild(svg.node())
-
-      function autoBox() {
-        const {x, y, width, height} = this.getBBox()
-        return [x, y, width, height]
-      }
-
-      svg.attr('viewBox', autoBox)
-    },
-    redraw () {
-      while (this.$refs.svgContainer.firstChild) {
-        this.$refs.svgContainer.firstChild.remove()
-      }
-      if (this.pages.length > 0) {
-        switch (this.graphMode) {
-          case 'rradial':
-            this.drawRelations()
-            break
-          case 'htree':
-            this.drawTree()
-            break
-          case 'hradial':
-            this.drawRadialTree()
-            break
-        }
-      }
-    }
-  },
-  apollo: {
-    pages: {
-      query: gql`
-        query ($locale: String!) {
-          pages {
-            links(locale: $locale) {
-              id
-              path
-              title
-              links
-            }
-          }
-        }
-      `,
-      variables () {
-        return {
-          locale: this.currentLocale
-        }
-      },
-      fetchPolicy: 'network-only',
-      update: (data) => data.pages.links,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-.admin-pages-visualize-svg {
-  text-align: center;
-  // 100vh - header - title section - footer - content padding
-  height: calc(100vh - 64px - 92px - 32px - 16px);
-
-  > svg {
-    height: 100%;
-    width: 100%;
-  }
-}
-</style>

+ 0 - 169
client/components/admin/admin-pages.vue

@@ -1,169 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Page', style='width: 80px;')
-          .admin-header-title
-            .headline.blue--text.text--darken-2.animated.fadeInLeft Pages
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage pages
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p1s(icon, color='grey', outlined, @click='refresh')
-            v-icon.grey--text mdi-refresh
-          v-btn.animated.fadeInDown.mx-3(color='primary', outlined, @click='recyclebin', disabled)
-            v-icon(left) mdi-delete-outline
-            span Recycle Bin
-          v-btn.animated.fadeInDown(color='primary', depressed, large, to='pages/visualize')
-            v-icon(left) mdi-graph
-            span Visualize
-        v-card.mt-3.animated.fadeInUp
-          .pa-2.d-flex.align-center(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-3`')
-            v-text-field(
-              solo
-              flat
-              v-model='search'
-              prepend-inner-icon='mdi-file-search-outline'
-              label='Search Pages...'
-              hide-details
-              dense
-              style='max-width: 400px;'
-              )
-            v-spacer
-            v-select.ml-2(
-              solo
-              flat
-              hide-details
-              dense
-              label='Locale'
-              :items='langs'
-              v-model='selectedLang'
-              style='max-width: 250px;'
-            )
-            v-select.ml-2(
-              solo
-              flat
-              hide-details
-              dense
-              label='Publish State'
-              :items='states'
-              v-model='selectedState'
-              style='max-width: 250px;'
-            )
-          v-divider
-          v-data-table(
-            :items='filteredPages'
-            :headers='headers'
-            :search='search'
-            :page.sync='pagination'
-            :items-per-page='15'
-            :loading='loading'
-            must-sort,
-            sort-by='updatedAt',
-            sort-desc,
-            hide-default-footer
-            @page-count="pageTotal = $event"
-          )
-            template(slot='item', slot-scope='props')
-              tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')
-                td.text-xs-right {{ props.item.id }}
-                td
-                  .body-2: strong {{ props.item.title }}
-                  .caption {{ props.item.description }}
-                td.admin-pages-path
-                  v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
-                  span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
-                td {{ props.item.createdAt | moment('calendar') }}
-                td {{ props.item.updatedAt | moment('calendar') }}
-            template(slot='no-data')
-              v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display.
-          .text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
-            v-pagination(v-model='pagination', :length='pageTotal')
-</template>
-
-<script>
-import _ from 'lodash'
-import pagesQuery from 'gql/admin/pages/pages-query-list.gql'
-
-export default {
-  data() {
-    return {
-      selectedPage: {},
-      pagination: 1,
-      pages: [],
-      pageTotal: 0,
-      headers: [
-        { text: 'ID', value: 'id', width: 80, sortable: true },
-        { text: 'Title', value: 'title' },
-        { text: 'Path', value: 'path' },
-        { text: 'Created', value: 'createdAt', width: 250 },
-        { text: 'Last Updated', value: 'updatedAt', width: 250 }
-      ],
-      search: '',
-      selectedLang: null,
-      selectedState: null,
-      states: [
-        { text: 'All Publishing States', value: null },
-        { text: 'Published', value: true },
-        { text: 'Not Published', value: false }
-      ],
-      loading: false
-    }
-  },
-  computed: {
-    filteredPages () {
-      return _.filter(this.pages, pg => {
-        if (this.selectedLang !== null && this.selectedLang !== pg.locale) {
-          return false
-        }
-        if (this.selectedState !== null && this.selectedState !== pg.isPublished) {
-          return false
-        }
-        return true
-      })
-    },
-    langs () {
-      return _.concat({
-        text: 'All Locales',
-        value: null
-      }, _.uniqBy(this.pages, 'locale').map(pg => ({
-        text: pg.locale,
-        value: pg.locale
-      })))
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.pages.refetch()
-      this.$store.commit('showNotification', {
-        message: 'Page list has been refreshed.',
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    newpage() {
-      this.pageSelectorShown = true
-    },
-    recyclebin () { }
-  },
-  apollo: {
-    pages: {
-      query: pagesQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.pages.list,
-      watchLoading (isLoading) {
-        this.loading = isLoading
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-pages-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-.admin-pages-path {
-  display: flex;
-  justify-content: flex-start;
-  align-items: center;
-  font-family: 'Roboto Mono', monospace;
-}
-</style>

+ 0 - 261
client/components/admin/admin-rendering.vue

@@ -1,261 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-process.svg', alt='Rendering', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:rendering.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:rendering.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/rendering', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
-            v-icon mdi-refresh
-          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-
-      v-flex.animated.fadeInUp(lg3, xs12)
-        v-toolbar(
-          color='blue darken-2'
-          dense
-          flat
-          dark
-          )
-          .subtitle-1 Pipeline
-        v-expansion-panels.adm-rendering-pipeline(
-          v-model='selectedCore'
-          accordion
-          mandatory
-          )
-          v-expansion-panel(
-            v-for='core in renderers'
-            :key='core.key'
-            )
-            v-expansion-panel-header(
-              hide-actions
-              ripple
-            )
-              v-toolbar(
-                color='blue'
-                dense
-                dark
-                flat
-                )
-                v-spacer
-                .body-2 {{core.input}}
-                v-icon.mx-2 mdi-arrow-right-circle
-                .caption {{core.output}}
-                v-spacer
-            v-expansion-panel-content
-              v-list.py-0(two-line, dense)
-                template(v-for='(rdr, n) in core.children')
-                  v-list-item(
-                    :key='rdr.key'
-                    @click='selectRenderer(rdr.key)'
-                    :class='currentRenderer.key === rdr.key ? ($vuetify.theme.dark ? `grey darken-4-l4` : `blue lighten-5`) : ``'
-                    )
-                    v-list-item-avatar(size='24', tile)
-                      v-icon(:color='currentRenderer.key === rdr.key ? "primary" : "grey"') {{rdr.icon}}
-                    v-list-item-content
-                      v-list-item-title {{rdr.title}}
-                      v-list-item-subtitle: .caption {{rdr.description}}
-                    v-list-item-avatar(size='24')
-                      status-indicator(v-if='rdr.isEnabled', positive, pulse)
-                      status-indicator(v-else, negative, pulse)
-                  v-divider.my-0(v-if='n < core.children.length - 1')
-
-      v-flex(lg9, xs12)
-        v-card.wiki-form.animated.fadeInUp
-          v-toolbar(
-            color='indigo'
-            dark
-            flat
-            dense
-            )
-            v-icon.mr-2 {{currentRenderer.icon}}
-            .subtitle-1 {{currentRenderer.title}}
-            v-spacer
-            v-switch(
-              dark
-              color='white'
-              label='Enabled'
-              v-model='currentRenderer.isEnabled'
-              hide-details
-              inset
-              )
-          v-card-info(color='blue')
-            div
-              div {{currentRenderer.description}}
-              span.caption: a(href='https://docs.requarks.io/en/rendering', target='_blank') Documentation
-          v-card-text.pb-4.pl-4
-            .overline.mb-5 Rendering Module Configuration
-            .body-2.ml-3(v-if='!currentRenderer.config || currentRenderer.config.length < 1'): em This rendering module has no configuration options you can modify.
-            template(v-else, v-for='(cfg, idx) in currentRenderer.config')
-              v-select(
-                v-if='cfg.value.type === "string" && cfg.value.enum'
-                outlined
-                :items='cfg.value.enum'
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-                color='indigo'
-              )
-              v-switch(
-                v-else-if='cfg.value.type === "boolean"'
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                color='indigo'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                inset
-                )
-              v-text-field(
-                v-else
-                outlined
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-                color='indigo'
-                )
-              v-divider.my-5(v-if='idx < currentRenderer.config.length - 1')
-          v-card-chin
-            v-spacer
-            .caption.pr-3.grey--text Module: {{ currentRenderer.key }}
-</template>
-
-<script>
-import _ from 'lodash'
-import { DepGraph } from 'dependency-graph'
-
-import { StatusIndicator } from 'vue-status-indicator'
-
-import renderersQuery from 'gql/admin/rendering/rendering-query-renderers.gql'
-import renderersSaveMutation from 'gql/admin/rendering/rendering-mutation-save-renderers.gql'
-
-export default {
-  components: {
-    StatusIndicator
-  },
-  data() {
-    return {
-      selectedCore: -1,
-      renderers: [],
-      currentRenderer: {}
-    }
-  },
-  watch: {
-    renderers(newValue, oldValue) {
-      _.delay(() => {
-        this.selectedCore = _.findIndex(newValue, ['key', 'markdownCore'])
-        this.selectRenderer('markdownCore')
-      }, 500)
-    }
-  },
-  methods: {
-    selectRenderer (key) {
-      this.renderers.map(rdr => {
-        if (_.some(rdr.children, ['key', key])) {
-          this.currentRenderer = _.find(rdr.children, ['key', key])
-        }
-      })
-    },
-    async refresh () {
-      await this.$apollo.queries.renderers.refetch()
-      this.$store.commit('showNotification', {
-        message: 'Rendering active configuration has been reloaded.',
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save () {
-      this.$store.commit(`loadingStart`, 'admin-rendering-saverenderers')
-      await this.$apollo.mutate({
-        mutation: renderersSaveMutation,
-        variables: {
-          renderers: _.reduce(this.renderers, (result, core) => {
-            result = _.concat(result, core.children.map(rd => ({
-              key: rd.key,
-              isEnabled: rd.isEnabled,
-              config: rd.config.map(cfg => ({ key: cfg.key, value: JSON.stringify({ v: cfg.value.value }) }))
-            })))
-            return result
-          }, [])
-        }
-      })
-      this.$store.commit('showNotification', {
-        message: 'Rendering configuration saved successfully.',
-        style: 'success',
-        icon: 'check'
-      })
-      this.$store.commit(`loadingStop`, 'admin-rendering-saverenderers')
-    }
-  },
-  apollo: {
-    renderers: {
-      query: renderersQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => {
-        let renderers = _.cloneDeep(data.rendering.renderers).map(str => ({
-          ...str,
-          config: _.sortBy(str.config.map(cfg => ({
-            ...cfg,
-            value: JSON.parse(cfg.value)
-          })), [t => t.value.order])
-        }))
-        // Build tree
-        const graph = new DepGraph({ circular: true })
-        const rawCores = _.filter(renderers, ['dependsOn', null]).map(core => {
-          core.children = _.concat([_.cloneDeep(core)], _.filter(renderers, ['dependsOn', core.key]))
-          return core
-        })
-        // Build dependency graph
-        rawCores.map(core => { graph.addNode(core.key) })
-        rawCores.map(core => {
-          rawCores.map(coreTarget => {
-            if (core.key !== coreTarget.key) {
-              if (core.output === coreTarget.input) {
-                graph.addDependency(core.key, coreTarget.key)
-              }
-            }
-          })
-        })
-        // Reorder cores in reverse dependency order
-        let orderedCores = []
-        _.reverse(graph.overallOrder()).map(coreKey => {
-          orderedCores.push(_.find(rawCores, ['key', coreKey]))
-        })
-        return orderedCores
-      },
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-rendering-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-.adm-rendering-pipeline {
-  .v-expansion-panel--active .v-expansion-panel-header {
-    min-height: 0;
-  }
-
-  .v-expansion-panel-header {
-    padding: 0;
-    margin-top: 1px;
-  }
-
-  .v-expansion-panel-content__wrap {
-    padding: 0;
-  }
-}
-</style>

+ 0 - 217
client/components/admin/admin-search.vue

@@ -1,217 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-search.svg', alt='Search Engine', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('admin:search.title')}}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:search.subtitle')}}
-          v-spacer
-          v-btn.mr-3.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/search', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
-            v-icon mdi-refresh
-          v-btn.mx-3.animated.fadeInDown.wait-p1s(color='black', dark, depressed, @click='rebuild')
-            v-icon(left) mdi-cached
-            span {{$t('admin:search.rebuildIndex')}}
-          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-
-      v-flex(lg3, xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(flat, color='primary', dark, dense)
-            .subtitle-1 {{$t('admin:search.searchEngine')}}
-          v-list.py-0(two-line, dense)
-            template(v-for='(eng, idx) in engines')
-              v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
-                v-list-item-avatar(size='24')
-                  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-circle-outline
-                  v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline
-                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-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}
-                v-list-item-avatar(v-if='selectedEngine === eng.key', size='24')
-                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
-              v-divider(v-if='idx < engines.length - 1')
-
-      v-flex(lg9, xs12)
-        v-card.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, flat, dark)
-            .subtitle-1 {{engine.title}}
-          v-card-info(color='blue')
-            div
-              div {{engine.description}}
-              span.caption: a(:href='engine.website') {{engine.website}}
-            v-spacer
-            .admin-providerlogo
-              img(:src='engine.logo', :alt='engine.title')
-          v-card-text
-            .overline.mb-5 {{$t('admin:search.engineConfig')}}
-            .body-2.ml-3(v-if='!engine.config || engine.config.length < 1'): em {{$t('admin:search.engineNoConfig')}}
-            template(v-else, v-for='cfg in engine.config')
-              v-select(
-                v-if='cfg.value.type === "string" && cfg.value.enum'
-                outlined
-                :items='cfg.value.enum'
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-              )
-              v-switch.mb-3(
-                v-else-if='cfg.value.type === "boolean"'
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                color='primary'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                inset
-                )
-              v-textarea(
-                v-else-if='cfg.value.type === "string" && cfg.value.multiline'
-                outlined
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-                )
-              v-text-field(
-                v-else
-                outlined
-                :key='cfg.key'
-                :label='cfg.value.title'
-                v-model='cfg.value.value'
-                prepend-icon='mdi-cog-box'
-                :hint='cfg.value.hint ? cfg.value.hint : ""'
-                persistent-hint
-                :class='cfg.value.hint ? "mb-2" : ""'
-                )
-</template>
-
-<script>
-import _ from 'lodash'
-
-import enginesQuery from 'gql/admin/search/search-query-engines.gql'
-import enginesSaveMutation from 'gql/admin/search/search-mutation-save-engines.gql'
-import enginesRebuildMutation from 'gql/admin/search/search-mutation-rebuild-index.gql'
-
-export default {
-  data() {
-    return {
-      engines: [],
-      selectedEngine: '',
-      engine: {}
-    }
-  },
-  watch: {
-    selectedEngine(newValue, oldValue) {
-      this.engine = _.find(this.engines, ['key', newValue]) || {}
-    },
-    engines(newValue, oldValue) {
-      this.selectedEngine = _.get(_.find(this.engines, 'isEnabled'), 'key', 'db')
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.engines.refetch()
-      this.$store.commit('showNotification', {
-        message: this.$t('admin:search.listRefreshSuccess'),
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save() {
-      this.$store.commit(`loadingStart`, 'admin-search-saveengines')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: enginesSaveMutation,
-          variables: {
-            engines: this.engines.map(tgt => ({
-              isEnabled: tgt.key === this.selectedEngine,
-              key: tgt.key,
-              config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))
-            }))
-          }
-        })
-        if (_.get(resp, 'data.search.updateSearchEngines.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            message: this.$t('admin:search.configSaveSuccess'),
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(_.get(resp, 'data.search.updateSearchEngines.responseResult.message', this.$t('common:error.unexpected')))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-search-saveengines')
-    },
-    async rebuild () {
-      this.$store.commit(`loadingStart`, 'admin-search-rebuildindex')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: enginesRebuildMutation
-        })
-        if (_.get(resp, 'data.search.rebuildIndex.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            message: this.$t('admin:search.indexRebuildSuccess'),
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(_.get(resp, 'data.search.rebuildIndex.responseResult.message', this.$t('common:error.unexpected')))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-search-rebuildindex')
-    }
-  },
-  apollo: {
-    engines: {
-      query: enginesQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.search.searchEngines).map(str => ({
-        ...str,
-        config: _.sortBy(str.config.map(cfg => ({
-          ...cfg,
-          value: JSON.parse(cfg.value)
-        })), [t => t.value.order])
-      })),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-search-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss' scoped>
-
-.enginelogo {
-  width: 250px;
-  height: 85px;
-  float:right;
-  display: flex;
-  justify-content: flex-end;
-  align-items: center;
-
-  img {
-    max-width: 100%;
-    max-height: 50px;
-  }
-}
-
-</style>

+ 0 - 444
client/components/admin/admin-security.vue

@@ -1,444 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-private.svg', alt='Security', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:security.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:security.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(lg6 xs12)
-              v-card.animated.fadeInUp
-                v-toolbar(color='red darken-2', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 Security
-                v-card-info(color='red')
-                  span Make sure to understand the implications before turning on / off a security feature.
-                v-card-text
-                  v-switch(
-                    inset
-                    label='Block Open Redirect'
-                    color='red darken-2'
-                    v-model='config.securityOpenRedirect'
-                    persistent-hint
-                    hint='Prevents user controlled URLs from directing to websites outside of your wiki. This provides Open Redirect protection.'
-                    )
-
-                  v-divider.mt-3
-                  v-switch.mt-3(
-                    inset
-                    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(
-                    inset
-                    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(
-                    inset
-                    label='Trust X-Forwarded-* Proxy Headers'
-                    color='red darken-2'
-                    v-model='config.securityTrustProxy'
-                    persistent-hint
-                    hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.'
-                    )
-
-                  //- v-divider.mt-3
-                  //- v-switch(
-                  //-   inset
-                  //-   label='Subresource Integrity (SRI)'
-                  //-   color='red darken-2'
-                  //-   v-model='config.securitySRI'
-                  //-   persistent-hint
-                  //-   hint='This ensure that resources such as CSS and JS files are not altered during delivery.'
-                  //-   disabled
-                  //-   )
-
-                  v-divider.mt-3
-                  v-switch(
-                    inset
-                    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(
-                  //-   inset
-                  //-   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
-                  //- )
-
-            v-flex(lg6 xs12)
-              v-card.animated.fadeInUp.wait-p2s
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{ $t('admin:security.uploads') }}
-                v-card-info(color='blue')
-                  span {{$t('admin:security.uploadsInfo')}}
-                v-card-text
-                  v-text-field.mt-3(
-                    outlined
-                    :label='$t(`admin:security.maxUploadSize`)'
-                    required
-                    v-model='config.uploadMaxFileSize'
-                    prepend-icon='mdi-progress-upload'
-                    :hint='$t(`admin:security.maxUploadSizeHint`)'
-                    persistent-hint
-                    :suffix='$t(`admin:security.maxUploadSizeSuffix`)'
-                    style='max-width: 450px;'
-                    )
-                  v-text-field.mt-3(
-                    outlined
-                    :label='$t(`admin:security.maxUploadBatch`)'
-                    required
-                    v-model='config.uploadMaxFiles'
-                    prepend-icon='mdi-upload-lock'
-                    :hint='$t(`admin:security.maxUploadBatchHint`)'
-                    persistent-hint
-                    :suffix='$t(`admin:security.maxUploadBatchSuffix`)'
-                    style='max-width: 450px;'
-                    )
-                  v-divider.mt-3
-                  v-switch(
-                    inset
-                    label='Scan and Sanitize SVG Uploads'
-                    color='primary'
-                    v-model='config.uploadScanSVG'
-                    persistent-hint
-                    hint='Should SVG uploads be scanned for vulnerabilities and stripped of any potentially unsafe content.'
-                    )
-                  v-divider.mt-3
-                  v-switch(
-                    inset
-                    label='Force Download of Unsafe Extensions'
-                    color='primary'
-                    v-model='config.uploadForceDownload'
-                    persistent-hint
-                    hint='Should non-image files be forced as downloads when accessed directly. This prevents potential XSS attacks via unsafe file extensions uploads.'
-                    )
-
-              v-card.mt-3.animated.fadeInUp.wait-p2s
-                v-toolbar(flat, color='primary', dark, dense)
-                  .subtitle-1 {{$t('admin:security.login')}}
-                //- v-card-info(color='blue')
-                //-   span {{$t('admin:security.loginInfo')}}
-                .overline.grey--text.pa-4 {{$t('admin:security.loginScreen')}}
-                .px-4.pb-3
-                  v-text-field(
-                    outlined
-                    :label='$t(`admin:security.loginBgUrl`)'
-                    v-model='config.authLoginBgUrl'
-                    :hint='$t(`admin:security.loginBgUrlHint`)'
-                    persistent-hint
-                    prepend-icon='mdi-image-area'
-                    append-icon='mdi-folder-image'
-                    @click:append='browseLoginBg'
-                  )
-                  v-switch(
-                    inset
-                    :label='$t(`admin:security.bypassLogin`)'
-                    color='primary'
-                    v-model='config.authAutoLogin'
-                    prepend-icon='mdi-fast-forward'
-                    persistent-hint
-                    :hint='$t(`admin:security.bypassLoginHint`)'
-                    )
-                  v-switch(
-                    inset
-                    :label='$t(`admin:security.hideLocalLogin`)'
-                    color='primary'
-                    v-model='config.authHideLocal'
-                    prepend-icon='mdi-eye-off-outline'
-                    persistent-hint
-                    :hint='$t(`admin:security.hideLocalLoginHint`)'
-                    )
-                v-divider.mt-3
-                .overline.grey--text.pa-4 {{$t('admin:security.loginSecurity')}}
-                .px-4.pb-3
-                  v-switch.mt-0(
-                    inset
-                    :label='$t(`admin:security.enforce2fa`)'
-                    color='primary'
-                    v-model='config.authEnforce2FA'
-                    prepend-icon='mdi-two-factor-authentication'
-                    :hint='$t(`admin:security.enforce2faHint`)'
-                    persistent-hint
-                  )
-                v-divider.mt-3
-                .overline.grey--text.pa-4 {{$t('admin:security.jwt')}}
-                .px-4.pb-3
-                  v-text-field(
-                    v-model='config.authJwtAudience'
-                    outlined
-                    prepend-icon='mdi-account-group-outline'
-                    :label='$t(`admin:auth.jwtAudience`)'
-                    :hint='$t(`admin:auth.jwtAudienceHint`)'
-                    persistent-hint
-                  )
-                  v-text-field.mt-3(
-                    v-model='config.authJwtExpiration'
-                    outlined
-                    prepend-icon='mdi-clock-outline'
-                    :label='$t(`admin:auth.tokenExpiration`)'
-                    :hint='$t(`admin:auth.tokenExpirationHint`)'
-                    persistent-hint
-                  )
-                  v-text-field.mt-3(
-                    v-model='config.authJwtRenewablePeriod'
-                    outlined
-                    prepend-icon='mdi-update'
-                    :label='$t(`admin:auth.tokenRenewalPeriod`)'
-                    :hint='$t(`admin:auth.tokenRenewalPeriodHint`)'
-                    persistent-hint
-                  )
-
-    component(:is='activeModal')
-</template>
-
-<script>
-import _ from 'lodash'
-import { sync } from 'vuex-pathify'
-import gql from 'graphql-tag'
-
-import editorStore from '../../store/editor'
-
-/* global WIKI */
-
-WIKI.$store.registerModule('editor', editorStore)
-
-export default {
-  i18nOptions: { namespaces: 'editor' },
-  components: {
-    editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
-  },
-  data() {
-    return {
-      config: {
-        uploadMaxFileSize: 0,
-        uploadMaxFiles: 0,
-        uploadScanSVG: true,
-        uploadForceDownload: true,
-        securityOpenRedirect: true,
-        securityIframe: true,
-        securityReferrerPolicy: true,
-        securityTrustProxy: true,
-        securitySRI: true,
-        securityHSTS: false,
-        securityHSTSDuration: 0,
-        securityCSP: false,
-        securityCSPDirectives: '',
-        authAutoLogin: false,
-        authHideLocal: false,
-        authLoginBgUrl: '',
-        authJwtAudience: 'urn:wiki.js',
-        authJwtExpiration: '30m',
-        authJwtRenewablePeriod: '14d'
-      },
-      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' }
-      ]
-    }
-  },
-  computed: {
-    activeModal: sync('editor/activeModal')
-  },
-  methods: {
-    async save () {
-      try {
-        await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $authAutoLogin: Boolean
-              $authEnforce2FA: Boolean
-              $authHideLocal: Boolean
-              $authLoginBgUrl: String
-              $authJwtAudience: String
-              $authJwtExpiration: String
-              $authJwtRenewablePeriod: String
-              $uploadMaxFileSize: Int
-              $uploadMaxFiles: Int
-              $uploadScanSVG: Boolean
-              $uploadForceDownload: Boolean
-              $securityOpenRedirect: Boolean
-              $securityIframe: Boolean
-              $securityReferrerPolicy: Boolean
-              $securityTrustProxy: Boolean
-              $securitySRI: Boolean
-              $securityHSTS: Boolean
-              $securityHSTSDuration: Int
-              $securityCSP: Boolean
-              $securityCSPDirectives: String
-            ) {
-              site {
-                updateConfig(
-                  authAutoLogin: $authAutoLogin,
-                  authEnforce2FA: $authEnforce2FA,
-                  authHideLocal: $authHideLocal,
-                  authLoginBgUrl: $authLoginBgUrl,
-                  authJwtAudience: $authJwtAudience,
-                  authJwtExpiration: $authJwtExpiration,
-                  authJwtRenewablePeriod: $authJwtRenewablePeriod,
-                  uploadMaxFileSize: $uploadMaxFileSize,
-                  uploadMaxFiles: $uploadMaxFiles,
-                  uploadScanSVG: $uploadScanSVG
-                  uploadForceDownload: $uploadForceDownload,
-                  securityOpenRedirect: $securityOpenRedirect,
-                  securityIframe: $securityIframe,
-                  securityReferrerPolicy: $securityReferrerPolicy,
-                  securityTrustProxy: $securityTrustProxy,
-                  securitySRI: $securitySRI,
-                  securityHSTS: $securityHSTS,
-                  securityHSTSDuration: $securityHSTSDuration,
-                  securityCSP: $securityCSP,
-                  securityCSPDirectives: $securityCSPDirectives
-                ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            authAutoLogin: _.get(this.config, 'authAutoLogin', false),
-            authEnforce2FA: _.get(this.config, 'authEnforce2FA', false),
-            authHideLocal: _.get(this.config, 'authHideLocal', false),
-            authLoginBgUrl: _.get(this.config, 'authLoginBgUrl', ''),
-            authJwtAudience: _.get(this.config, 'authJwtAudience', ''),
-            authJwtExpiration: _.get(this.config, 'authJwtExpiration', ''),
-            authJwtRenewablePeriod: _.get(this.config, 'authJwtRenewablePeriod', ''),
-            uploadMaxFileSize: _.toSafeInteger(_.get(this.config, 'uploadMaxFileSize', 0)),
-            uploadMaxFiles: _.toSafeInteger(_.get(this.config, 'uploadMaxFiles', 0)),
-            uploadScanSVG: _.get(this.config, 'uploadScanSVG', false),
-            uploadForceDownload: _.get(this.config, 'uploadForceDownload', false),
-            securityOpenRedirect: _.get(this.config, 'securityOpenRedirect', false),
-            securityIframe: _.get(this.config, 'securityIframe', false),
-            securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
-            securityTrustProxy: _.get(this.config, 'securityTrustProxy', false),
-            securitySRI: _.get(this.config, 'securitySRI', 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) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: 'Configuration saved successfully.',
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    },
-    browseLoginBg () {
-      this.$store.set('editor/editorKey', 'common')
-      this.activeModal = 'editorModalMedia'
-    }
-  },
-  mounted () {
-    this.$root.$on('editorInsert', opts => {
-      this.config.authLoginBgUrl = opts.path
-    })
-  },
-  beforeDestroy() {
-    this.$root.$off('editorInsert')
-  },
-  apollo: {
-    config: {
-      query: gql`
-        {
-          site {
-            config {
-              authAutoLogin
-              authEnforce2FA
-              authHideLocal
-              authLoginBgUrl
-              authJwtAudience
-              authJwtExpiration
-              authJwtRenewablePeriod
-              uploadMaxFileSize
-              uploadMaxFiles
-              uploadScanSVG
-              uploadForceDownload
-              securityOpenRedirect
-              securityIframe
-              securityReferrerPolicy
-              securityTrustProxy
-              securitySRI
-              securityHSTS
-              securityHSTSDuration
-              securityCSP
-              securityCSPDirectives
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.site.config),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-security-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 269
client/components/admin/admin-ssl.vue

@@ -1,269 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-validation.svg', alt='SSL', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:ssl.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:ssl.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown(
-            v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0'
-            color='black'
-            dark
-            depressed
-            @click='renewCertificate'
-            large
-            :loading='loadingRenew'
-            )
-            v-icon(left) mdi-cached
-            span {{$t('admin:ssl.renewCertificate')}}
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(lg6 xs12)
-              v-card.animated.fadeInUp
-                v-subheader {{ $t('admin:ssl.currentState') }}
-                v-list(two-line, dense)
-                  v-list-item
-                    v-list-item-avatar
-                      v-icon.indigo.white--text mdi-handshake
-                    v-list-item-content
-                      v-list-item-title {{ $t(`admin:ssl.provider`) }}
-                      v-list-item-subtitle {{ providerTitle }}
-                  template(v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0')
-                    v-list-item
-                      v-list-item-avatar
-                        v-icon.indigo.white--text mdi-application
-                      v-list-item-content
-                        v-list-item-title {{ $t(`admin:ssl.domain`) }}
-                        v-list-item-subtitle {{ info.sslDomain }}
-                    v-list-item
-                      v-list-item-avatar
-                        v-icon.indigo.white--text mdi-at
-                      v-list-item-content
-                        v-list-item-title {{ $t('admin:ssl.subscriberEmail') }}
-                        v-list-item-subtitle {{ info.sslSubscriberEmail }}
-                    v-list-item
-                      v-list-item-avatar
-                        v-icon.indigo.white--text mdi-calendar-remove-outline
-                      v-list-item-content
-                        v-list-item-title {{ $t('admin:ssl.expiration') }}
-                        v-list-item-subtitle {{ info.sslExpirationDate | moment('calendar') }}
-                    v-list-item
-                      v-list-item-avatar
-                        v-icon.indigo.white--text mdi-traffic-light
-                      v-list-item-content
-                        v-list-item-title {{ $t(`admin:ssl.status`) }}
-                        v-list-item-subtitle {{ info.sslStatus }}
-
-            v-flex(lg6 xs12)
-              v-card.animated.fadeInUp.wait-p2s
-                v-subheader {{ $t('admin:ssl.ports') }}
-                v-list(two-line, dense)
-                  v-list-item
-                    v-list-item-avatar
-                      v-icon.blue.white--text mdi-lock-open-variant
-                    v-list-item-content
-                      v-list-item-title {{ $t(`admin:ssl.httpPort`) }}
-                      v-list-item-subtitle {{ info.httpPort }}
-                  template(v-if='info.httpsPort > 0')
-                    v-divider
-                    v-list-item
-                      v-list-item-avatar
-                        v-icon.green.white--text mdi-lock
-                      v-list-item-content
-                        v-list-item-title {{ $t(`admin:ssl.httpsPort`) }}
-                        v-list-item-subtitle {{ info.httpsPort }}
-                    v-divider
-                    v-list-item
-                      v-list-item-avatar
-                        v-icon.indigo.white--text mdi-sign-direction
-                      v-list-item-content
-                        v-list-item-title {{ $t(`admin:ssl.httpPortRedirect`) }}
-                        v-list-item-subtitle {{ info.httpRedirection }}
-                      v-list-item-action
-                        v-btn.red--text(
-                          v-if='info.httpRedirection'
-                          depressed
-                          :color='$vuetify.theme.dark ? `red darken-4` : `red lighten-5`'
-                          :class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'
-                          @click='toggleRedir'
-                          :loading='loadingRedir'
-                          )
-                          v-icon(left) mdi-power
-                          span {{$t('admin:ssl.httpPortRedirectTurnOff')}}
-                        v-btn.green--text(
-                          v-else
-                          depressed
-                          :color='$vuetify.theme.dark ? `green darken-4` : `green lighten-5`'
-                          :class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'
-                          @click='toggleRedir'
-                          :loading='loadingRedir'
-                          )
-                          v-icon(left) mdi-power
-                          span {{$t('admin:ssl.httpPortRedirectTurnOn')}}
-
-    v-dialog(
-      v-model='loadingRenew'
-      persistent
-      max-width='450'
-      )
-      v-card(color='black', dark)
-        v-card-text.pa-10.text-center
-          semipolar-spinner.animated.fadeIn(
-            :animation-duration='1500'
-            :size='65'
-            color='#FFF'
-            style='margin: 0 auto;'
-          )
-          .mt-5.body-1.white--text {{$t('admin:ssl.renewCertificateLoadingTitle')}}
-          .caption.mt-4 {{$t('admin:ssl.renewCertificateLoadingSubtitle')}}
-
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-
-import { SemipolarSpinner } from 'epic-spinners'
-
-export default {
-  components: {
-    SemipolarSpinner
-  },
-  data() {
-    return {
-      loadingRenew: false,
-      loadingRedir: false,
-      info: {
-        sslDomain: '',
-        sslProvider: '',
-        sslSubscriberEmail: '',
-        sslExpirationDate: false,
-        sslStatus: '',
-        httpPort: 0,
-        httpRedirection: false,
-        httpsPort: 0
-      }
-    }
-  },
-  computed: {
-    providerTitle () {
-      switch (this.info.sslProvider) {
-        case 'custom':
-          return this.$t('admin:ssl.providerCustomCertificate')
-        case 'letsencrypt':
-          return this.$t('admin:ssl.providerLetsEncrypt')
-        default:
-          return this.$t('admin:ssl.providerDisabled')
-      }
-    }
-  },
-  methods: {
-    async toggleRedir () {
-      this.loadingRedir = true
-      try {
-        this.info.httpRedirection = !this.info.httpRedirection
-        await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($enabled: Boolean!) {
-              system {
-                setHTTPSRedirection(enabled: $enabled) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            enabled: _.get(this.info, 'httpRedirection', false)
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-toggleRedirection')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:ssl.httpPortRedirectSaveSuccess'),
-          icon: 'check'
-        })
-      } catch (err) {
-        this.info.httpRedirection = !this.info.httpRedirection
-        this.$store.commit('pushGraphError', err)
-      }
-      this.loadingRedir = false
-    },
-    async renewCertificate () {
-      this.loadingRenew = true
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: gql`
-            mutation {
-              system {
-                renewHTTPSCertificate {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-renew')
-          }
-        })
-        const resp = _.get(respRaw, 'data.system.renewHTTPSCertificate.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.$t('admin:ssl.renewCertificateSuccess'),
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.loadingRenew = false
-    }
-  },
-  apollo: {
-    info: {
-      query: gql`
-        {
-          system {
-            info {
-              httpPort
-              httpRedirection
-              httpsPort
-              sslDomain
-              sslExpirationDate
-              sslProvider
-              sslStatus
-              sslSubscriberEmail
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.system.info),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 32
client/components/admin/admin-stats.vue

@@ -1,32 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, fill-height)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header-icon: v-icon(size='80', color='grey lighten-2') show_chart
-        .headline.primary--text Statistics
-        .subtitle-1.grey--text Useful information about your wiki
-        .pa-3
-          fingerprint-spinner(
-            :animation-duration='1500'
-            :size='128'
-            color='#e91e63'
-            )
-          .caption.pink--text.mt-3 Compiling latest data...
-</template>
-
-<script>
-import { FingerprintSpinner } from 'epic-spinners'
-
-export default {
-  components: {
-    FingerprintSpinner
-  },
-  data() {
-    return {}
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 372
client/components/admin/admin-storage.vue

@@ -1,372 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-cloud-storage.svg', alt='Storage', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('admin:storage.title')}}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('admin:storage.subtitle')}}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p3s(icon, outlined, color='grey', href='https://docs.requarks.io/storage', target='_blank')
-            v-icon mdi-help-circle
-          v-btn.mx-3.animated.fadeInDown.wait-p2s(icon, outlined, color='grey', @click='refresh')
-            v-icon mdi-refresh
-          v-btn.animated.fadeInDown(color='success', @click='save', depressed, large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-
-      v-flex(lg3, xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(flat, color='primary', dark, dense)
-            .subtitle-1 {{$t('admin:storage.targets')}}
-          v-list(two-line, dense).py-0
-            template(v-for='(tgt, idx) in targets')
-              v-list-item(:key='tgt.key', @click='selectedTarget = tgt.key', :disabled='!tgt.isAvailable')
-                v-list-item-avatar(size='24')
-                  v-icon(color='grey', v-if='!tgt.isAvailable') mdi-minus-box-outline
-                  v-icon(color='primary', v-else-if='tgt.isEnabled', v-ripple, @click='tgt.key !== `local` && (tgt.isEnabled = false)') mdi-checkbox-marked-outline
-                  v-icon(color='grey', v-else, v-ripple, @click='tgt.isEnabled = true') mdi-checkbox-blank-outline
-                v-list-item-content
-                  v-list-item-title.body-2(:class='!tgt.isAvailable ? `grey--text` : (selectedTarget === tgt.key ? `primary--text` : ``)') {{ tgt.title }}
-                  v-list-item-subtitle: .caption(:class='!tgt.isAvailable ? `grey--text text--lighten-1` : (selectedTarget === tgt.key ? `blue--text ` : ``)') {{ tgt.description }}
-                v-list-item-avatar(v-if='selectedTarget === tgt.key', size='24')
-                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
-              v-divider(v-if='idx < targets.length - 1')
-
-        v-card.mt-3.animated.fadeInUp.wait-p2s
-          v-toolbar(flat, :color='$vuetify.theme.dark ? `grey darken-3-l5` : `grey darken-3`', dark, dense)
-            .subtitle-1 {{$t('admin:storage.status')}}
-            v-spacer
-            looping-rhombuses-spinner(
-              :animation-duration='5000'
-              :rhombus-size='10'
-              color='#FFF'
-            )
-          v-list.py-0(two-line, dense)
-            template(v-for='(tgt, n) in status')
-              v-list-item(:key='tgt.key')
-                template(v-if='tgt.status === `pending`')
-                  v-list-item-avatar(color='purple')
-                    v-icon(color='white') mdi-clock-outline
-                  v-list-item-content
-                    v-list-item-title.body-2 {{tgt.title}}
-                    v-list-item-subtitle.purple--text.caption {{tgt.status}}
-                  v-list-item-action
-                    v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
-                template(v-else-if='tgt.status === `operational`')
-                  v-list-item-avatar(color='green')
-                    v-icon(color='white') mdi-check-circle
-                  v-list-item-content
-                    v-list-item-title.body-2 {{tgt.title}}
-                    v-list-item-subtitle.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
-                template(v-else)
-                  v-list-item-avatar(color='red')
-                    v-icon(color='white') mdi-close-circle-outline
-                  v-list-item-content
-                    v-list-item-title.body-2 {{tgt.title}}
-                    v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
-                  v-list-item-action
-                    v-menu
-                      template(v-slot:activator='{ on }')
-                        v-btn(icon, v-on='on')
-                          v-icon(color='red') mdi-information
-                      v-card(width='450')
-                        v-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}}
-                        v-card-text {{tgt.message}}
-
-              v-divider(v-if='n < status.length - 1')
-            v-list-item(v-if='status.length < 1')
-              em {{$t('admin:storage.noTarget')}}
-
-      v-flex(xs12, lg9)
-        v-card.wiki-form.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, flat, dark)
-            .subtitle-1 {{target.title}}
-            v-spacer
-            v-switch(
-              dark
-              color='blue lighten-5'
-              label='Active'
-              v-model='target.isEnabled'
-              hide-details
-              inset
-              )
-          v-card-info(color='blue')
-            div
-              div {{target.description}}
-              span.caption: a(:href='target.website') {{target.website}}
-            v-spacer
-            .admin-providerlogo
-              img(:src='target.logo', :alt='target.title')
-          v-card-text
-            v-form
-              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
-              .overline.my-5 {{$t('admin:storage.targetConfig')}}
-              .body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}}
-              template(v-else, v-for='cfg in target.config')
-                v-select(
-                  v-if='cfg.value.type === "string" && cfg.value.enum'
-                  outlined
-                  :items='cfg.value.enum'
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  :class='cfg.value.hint ? "mb-2" : ""'
-                )
-                v-switch.mb-3(
-                  v-else-if='cfg.value.type === "boolean"'
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  color='primary'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  inset
-                  )
-                v-textarea(
-                  v-else-if='cfg.value.type === "string" && cfg.value.multiline'
-                  outlined
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  :class='cfg.value.hint ? "mb-2" : ""'
-                  )
-                v-text-field(
-                  v-else
-                  outlined
-                  :key='cfg.key'
-                  :label='cfg.value.title'
-                  v-model='cfg.value.value'
-                  prepend-icon='mdi-cog-box'
-                  :hint='cfg.value.hint ? cfg.value.hint : ""'
-                  persistent-hint
-                  :class='cfg.value.hint ? "mb-2" : ""'
-                  )
-              v-divider.mt-3
-              .overline.my-5 {{$t('admin:storage.syncDirection')}}
-              .body-2.ml-3 {{$t('admin:storage.syncDirectionSubtitle')}}
-              .pr-3.pt-3
-                v-radio-group.ml-3.py-0(v-model='target.mode')
-                  v-radio(
-                    :label='$t(`admin:storage.syncDirBi`)'
-                    color='primary'
-                    value='sync'
-                    :disabled='target.supportedModes.indexOf(`sync`) < 0'
-                  )
-                  v-radio(
-                    :label='$t(`admin:storage.syncDirPush`)'
-                    color='primary'
-                    value='push'
-                    :disabled='target.supportedModes.indexOf(`push`) < 0'
-                  )
-                  v-radio(
-                    :label='$t(`admin:storage.syncDirPull`)'
-                    color='primary'
-                    value='pull'
-                    :disabled='target.supportedModes.indexOf(`pull`) < 0'
-                  )
-              .body-2.ml-3
-                strong {{$t('admin:storage.syncDirBi')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') {{$t('admin:storage.unsupported')}}]
-                .pb-3 {{$t('admin:storage.syncDirBiHint')}}
-                strong {{$t('admin:storage.syncDirPush')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') {{$t('admin:storage.unsupported')}}]
-                .pb-3 {{$t('admin:storage.syncDirPushHint')}}
-                strong {{$t('admin:storage.syncDirPull')}} #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') {{$t('admin:storage.unsupported')}}]
-                .pb-3 {{$t('admin:storage.syncDirPullHint')}}
-
-              template(v-if='target.hasSchedule')
-                v-divider.mt-3
-                .overline.my-5 {{$t('admin:storage.syncSchedule')}}
-                .body-2.ml-3 {{$t('admin:storage.syncScheduleHint')}}
-                .pa-3
-                  duration-picker(v-model='target.syncInterval')
-                  i18next.caption.mt-3(path='admin:storage.syncScheduleCurrent', tag='div')
-                    strong(place='schedule') {{getDefaultSchedule(target.syncInterval)}}
-                  i18next.caption(path='admin:storage.syncScheduleDefault', tag='div')
-                    strong(place='schedule') {{getDefaultSchedule(target.syncIntervalDefault)}}
-
-              template(v-if='target.actions && target.actions.length > 0')
-                v-divider.mt-3
-                .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-layout(row, wrap, fill-height)
-                    v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
-                      v-card.radius-7.grey(flat, :class='$vuetify.theme.dark ? `darken-3-d5` : `lighten-3`', height='100%')
-                        v-card-text
-                          .subtitle-1(v-html='act.label')
-                          .body-2.mt-4(v-html='act.hint')
-                          v-btn.mx-0.mt-5(
-                            @click='executeAction(target.key, act.handler)'
-                            outlined
-                            :color='$vuetify.theme.dark ? `blue` : `primary`'
-                            :disabled='runningAction || !target.isEnabled'
-                            :loading='runningActionHandler === act.handler'
-                            ) {{$t('admin:storage.actionRun')}}
-
-</template>
-
-<script>
-import _ from 'lodash'
-import moment from 'moment'
-import momentDurationFormatSetup from 'moment-duration-format'
-
-import DurationPicker from '../common/duration-picker.vue'
-import { LoopingRhombusesSpinner } from 'epic-spinners'
-
-import statusQuery from 'gql/admin/storage/storage-query-status.gql'
-import targetsQuery from 'gql/admin/storage/storage-query-targets.gql'
-import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
-import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
-
-momentDurationFormatSetup(moment)
-
-export default {
-  components: {
-    DurationPicker,
-    LoopingRhombusesSpinner
-  },
-  filters: {
-    startCase(val) { return _.startCase(val) }
-  },
-  data() {
-    return {
-      runningAction: false,
-      runningActionHandler: '',
-      selectedTarget: '',
-      target: {
-        supportedModes: []
-      },
-      targets: [],
-      status: []
-    }
-  },
-  computed: {
-    activeTargets() {
-      return _.filter(this.targets, 'isEnabled')
-    }
-  },
-  watch: {
-    selectedTarget(newValue, oldValue) {
-      this.target = _.find(this.targets, ['key', newValue]) || {}
-    },
-    targets(newValue, oldValue) {
-      this.selectedTarget = _.get(_.find(this.targets, ['isEnabled', true]), 'key', 'disk')
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.targets.refetch()
-      this.$store.commit('showNotification', {
-        message: 'List of storage targets has been refreshed.',
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async save() {
-      this.$store.commit(`loadingStart`, 'admin-storage-savetargets')
-      await this.$apollo.mutate({
-        mutation: targetsSaveMutation,
-        variables: {
-          targets: this.targets.map(tgt => _.pick(tgt, [
-            'isEnabled',
-            'key',
-            'config',
-            'mode',
-            'syncInterval'
-          ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
-        }
-      })
-      this.$store.commit('showNotification', {
-        message: 'Storage configuration saved successfully.',
-        style: 'success',
-        icon: 'check'
-      })
-      this.$store.commit(`loadingStop`, 'admin-storage-savetargets')
-    },
-    getDefaultSchedule(val) {
-      if (!val) { return 'N/A' }
-      return moment.duration(val).format('y [years], M [months], d [days], h [hours], m [minutes]')
-    },
-    async executeAction(targetKey, handler) {
-      this.$store.commit(`loadingStart`, 'admin-storage-executeaction')
-      this.runningAction = true
-      this.runningActionHandler = handler
-      try {
-        await this.$apollo.mutate({
-          mutation: targetExecuteActionMutation,
-          variables: {
-            targetKey,
-            handler
-          }
-        })
-        this.$store.commit('showNotification', {
-          message: 'Action completed.',
-          style: 'success',
-          icon: 'check'
-        })
-      } catch (err) {
-        console.warn(err)
-      }
-      this.runningAction = false
-      this.runningActionHandler = ''
-      this.$store.commit(`loadingStop`, 'admin-storage-executeaction')
-    }
-  },
-  apollo: {
-    targets: {
-      query: targetsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.storage.targets).map(str => ({
-        ...str,
-        config: _.sortBy(str.config.map(cfg => ({
-          ...cfg,
-          value: JSON.parse(cfg.value)
-        })), [t => t.value.order])
-      })),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-targets-refresh')
-      }
-    },
-    status: {
-      query: statusQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.storage.status,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-status-refresh')
-      },
-      pollInterval: 3000
-    }
-  }
-}
-</script>
-
-<style lang='scss' scoped>
-
-.targetlogo {
-  width: 250px;
-  height: 85px;
-  float:right;
-  display: flex;
-  justify-content: flex-end;
-  align-items: center;
-
-  img {
-    max-width: 100%;
-    max-height: 50px;
-  }
-}
-
-</style>

+ 0 - 238
client/components/admin/admin-system.vue

@@ -1,238 +0,0 @@
-<template lang='pug'>
-  v-container.admin-system(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-tune.svg', alt='System Info', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:system.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{ $t('admin:system.subtitle') }}
-        v-layout.mt-3(row wrap)
-          v-flex(lg6 xs12)
-            v-card.animated.fadeInUp
-              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-list(two-line, dense)
-                v-list-item
-                  v-list-item-avatar
-                    v-icon.blue.white--text mdi-application-export
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.currentVersion') }}
-                    v-list-item-subtitle {{ info.currentVersion }}
-                v-list-item
-                  v-list-item-avatar
-                    v-icon.blue.white--text mdi-inbox-arrow-up
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.latestVersion') }}
-                    v-list-item-subtitle {{ info.latestVersion }}
-                  v-list-item-action
-                    v-list-item-action-text {{ $t('admin:system.published') }} {{ info.latestVersionReleaseDate | moment('from') }}
-              v-card-actions(v-if='info.upgradeCapable && !isLatestVersion && info.platform === `docker`', :class='$vuetify.theme.dark ? `grey darken-3-d5` : `indigo lighten-5`')
-                .caption.indigo--text.pl-3(:class='$vuetify.theme.dark ? `text--lighten-4` : ``') Wiki.js can perform the upgrade to the latest version for you.
-                v-spacer
-                v-btn.px-3(
-                  color='indigo'
-                  dark
-                  @click='performUpgrade'
-                  )
-                  v-icon(left) mdi-upload
-                  span Perform Upgrade
-
-            v-card.mt-4.animated.fadeInUp.wait-p2s
-              v-subheader {{ $t('admin:system.hostInfo') }}
-              v-list(two-line, dense)
-                v-list-item
-                  v-list-item-avatar
-                    v-avatar.blue-grey(size='40')
-                      v-icon(color='white') {{platformLogo}}
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.os') }}
-                    v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}
-                v-list-item
-                  v-list-item-avatar
-                    v-icon.blue-grey.white--text mdi-desktop-classic
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.hostname') }}
-                    v-list-item-subtitle {{ info.hostname }}
-                v-list-item
-                  v-list-item-avatar
-                    v-icon.blue-grey.white--text mdi-cpu-64-bit
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.cpuCores') }}
-                    v-list-item-subtitle {{ info.cpuCores }}
-                v-list-item
-                  v-list-item-avatar
-                    v-icon.blue-grey.white--text mdi-memory
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.totalRAM') }}
-                    v-list-item-subtitle {{ info.ramTotal }}
-                v-list-item
-                  v-list-item-avatar
-                    v-icon.blue-grey.white--text mdi-iframe-outline
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.workingDirectory') }}
-                    v-list-item-subtitle {{ info.workingDirectory }}
-                v-list-item
-                  v-list-item-avatar
-                    v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline
-                  v-list-item-content
-                    v-list-item-title {{ $t('admin:system.configFile') }}
-                    v-list-item-subtitle {{ info.configFile }}
-
-          v-flex(lg6 xs12)
-            v-card.pb-3.animated.fadeInUp.wait-p4s
-              v-subheader Node.js
-              v-list(dense)
-                v-list-item
-                  v-list-item-avatar
-                    v-avatar.light-green(size='40')
-                      v-icon(color='white') mdi-nodejs
-                  v-list-item-content
-                    v-list-item-title {{ info.nodeVersion }}
-
-              v-divider.mt-3
-              v-subheader {{ info.dbType }}
-              v-list(dense)
-                v-list-item
-                  v-list-item-avatar
-                    v-avatar.indigo.darken-1(size='40')
-                      v-icon(color='white') mdi-database
-                  v-list-item-content
-                    v-list-item-title(v-html='dbVersion')
-                    v-list-item-subtitle {{ info.dbHost }}
-
-                v-alert.mt-3.mx-4(:value='isDbLimited', color='deep-orange darken-2', icon='mdi-alert', dark) {{ $t('admin:system.dbPartialSupport') }}
-
-    v-dialog(
-      v-model='isUpgrading'
-      persistent
-      width='450'
-      )
-      v-card.blue.darken-5(dark)
-        v-card-text.text-center.pa-10
-          self-building-square-spinner(
-            :animation-duration='4000'
-            :size='40'
-            color='#FFF'
-            style='margin: 0 auto;'
-            )
-          .body-2.mt-5.blue--text.text--lighten-4 Your Wiki.js container is being upgraded...
-          .caption.blue--text.text--lighten-2 Please wait
-          v-progress-linear.mt-5(
-            color='blue lighten-2'
-            :value='upgradeProgress'
-            :buffer-value='upgradeProgress'
-            rounded
-            :stream='isUpgradingStarted'
-            query
-            :indeterminate='!isUpgradingStarted'
-          )
-</template>
-
-<script>
-import _ from 'lodash'
-
-import { SelfBuildingSquareSpinner } from 'epic-spinners'
-
-import systemInfoQuery from 'gql/admin/system/system-query-info.gql'
-import performUpgradeMutation from 'gql/admin/system/system-mutation-upgrade.gql'
-
-export default {
-  components: {
-    SelfBuildingSquareSpinner
-  },
-  data () {
-    return {
-      isUpgrading: false,
-      isUpgradingStarted: false,
-      upgradeProgress: 0,
-      info: {}
-    }
-  },
-  computed: {
-    dbVersion () {
-      return _.get(this.info, 'dbVersion', '').replace(/(?:\r\n|\r|\n)/g, '<br />')
-    },
-    platformLogo () {
-      switch (this.info.platform) {
-        case 'docker':
-          return 'mdi-docker'
-        case 'darwin':
-          return 'mdi-apple'
-        case 'linux':
-          if (this.info.operatingSystem.indexOf('Ubuntu')) {
-            return 'mdi-ubuntu'
-          } else {
-            return 'mdi-linux'
-          }
-        case 'win32':
-          return 'mdi-microsoft-windows'
-        default:
-          return ''
-      }
-    },
-    isDbLimited () {
-      return this.info.dbType === 'MySQL' && this.dbVersion.indexOf('5.') === 0
-    },
-    isLatestVersion () {
-      return this.info.currentVersion === this.info.latestVersion
-    }
-  },
-  methods: {
-    async refresh () {
-      await this.$apollo.queries.info.refetch()
-      this.$store.commit('showNotification', {
-        message: this.$t('admin:system.refreshSuccess'),
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    async performUpgrade () {
-      this.isUpgrading = true
-      this.isUpgradingStarted = false
-      this.upgradeProgress = 0
-      this.$store.commit(`loadingStart`, 'admin-system-upgrade')
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: performUpgradeMutation
-        })
-        const resp = _.get(respRaw, 'data.system.performUpgrade.responseResult', {})
-        if (resp.succeeded) {
-          this.isUpgradingStarted = true
-          let progressInterval = setInterval(() => {
-            this.upgradeProgress += 0.83
-          }, 500)
-          _.delay(() => {
-            clearInterval(progressInterval)
-            window.location.reload(true)
-          }, 60000)
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-        this.$store.commit(`loadingStop`, 'admin-system-upgrade')
-        this.isUpgrading = false
-      }
-    }
-  },
-  apollo: {
-    info: {
-      query: systemInfoQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.system.info,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-system-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-.admin-system {
-  .v-list-item-title, .v-list-item__subtitle {
-    user-select: text;
-  }
-}
-</style>

+ 0 - 248
client/components/admin/admin-tags.vue

@@ -1,248 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-tags.svg', alt='Tags', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('tags.title')}}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{$t('tags.subtitle')}}
-          v-spacer
-          v-btn.animated.fadeInDown(outlined, color='grey', @click='refresh', icon)
-            v-icon mdi-refresh
-        v-container.pa-0.mt-3(fluid, grid-list-lg)
-          v-layout(row)
-            v-flex(style='flex: 0 0 350px;')
-              v-card.animated.fadeInUp
-                v-toolbar(:color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', flat)
-                  v-text-field(
-                    v-model='filter'
-                    :label='$t(`admin:tags.filter`)'
-                    hide-details
-                    single-line
-                    solo
-                    flat
-                    dense
-                    color='teal'
-                    :background-color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-2`'
-                    prepend-inner-icon='mdi-magnify'
-                  )
-                v-divider
-                v-list.py-2(dense, nav)
-                  v-list-item(v-if='tags.length < 1')
-                    v-list-item-avatar(size='24'): v-icon(color='grey') mdi-compass-off
-                    v-list-item-content
-                      .caption.grey--text {{$t('tags.emptyList')}}
-                  v-list-item(
-                    v-for='tag of filteredTags'
-                    :key='tag.id'
-                    :class='(tag.id === current.id) ? "teal" : ""'
-                    @click='selectTag(tag)'
-                    )
-                    v-list-item-avatar(size='24', tile): v-icon(size='18', :color='tag.id === current.id ? `white` : `teal`') mdi-tag
-                    v-list-item-title(:class='tag.id === current.id ? `white--text` : ``') {{tag.tag}}
-            v-flex.animated.fadeInUp.wait-p2s
-              template(v-if='current.id')
-                v-card
-                  v-toolbar(dense, color='teal', flat, dark)
-                    .subtitle-1 {{$t('tags.edit')}}
-                    v-spacer
-                    v-btn.pl-4(
-                      color='white'
-                      dark
-                      outlined
-                      small
-                      :href='`/t/` + current.tag'
-                      )
-                      span.text-none {{$t('admin:tags.viewLinkedPages')}}
-                      v-icon(right) mdi-chevron-right
-                  v-card-text
-                    v-text-field(
-                      outlined
-                      :label='$t("tags.tag")'
-                      prepend-icon='mdi-tag'
-                      v-model='current.tag'
-                      counter='255'
-                    )
-                    v-text-field(
-                      outlined
-                      :label='$t("tags.label")'
-                      prepend-icon='mdi-format-title'
-                      v-model='current.title'
-                      hide-details
-                    )
-                  v-card-chin
-                    i18next.caption.pl-3(path='admin:tags.date', tag='div')
-                      strong(place='created') {{current.createdAt | moment('from')}}
-                      strong(place='updated') {{current.updatedAt | moment('from')}}
-                    v-spacer
-                    v-dialog(v-model='deleteTagDialog', max-width='500')
-                      template(v-slot:activator='{ on }')
-                        v-btn(color='red', outlined, v-on='on')
-                          v-icon(color='red') mdi-trash-can-outline
-                      v-card
-                        .dialog-header.is-red {{$t('admin:tags.deleteConfirm')}}
-                        v-card-text.pa-4
-                          i18next(tag='span', path='admin:tags.deleteConfirmText')
-                            strong(place='tag') {{ current.tag }}
-                        v-card-actions
-                          v-spacer
-                          v-btn(text, @click='deleteTagDialog = false') {{$t('common:actions.cancel')}}
-                          v-btn(color='red', dark, @click='deleteTag(current)') {{$t('common:actions.delete')}}
-                    v-btn.px-5.mr-2(color='success', depressed, dark, @click='saveTag(current)')
-                      v-icon(left) mdi-content-save
-                      span {{$t('common:actions.save')}}
-              v-card(v-else)
-                v-card-text.grey--text(v-if='tags.length > 0') {{$t('tags.noSelectionText')}}
-                v-card-text.grey--text(v-else) {{$t('tags.noItemsText')}}
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-
-export default {
-  data() {
-    return {
-      tags: [],
-      current: {},
-      filter: '',
-      deleteTagDialog: false
-    }
-  },
-  computed: {
-    filteredTags () {
-      if (this.filter.length > 0) {
-        return _.filter(this.tags, t => t.tag.indexOf(this.filter) >= 0 || t.title.indexOf(this.filter) >= 0)
-      } else {
-        return this.tags
-      }
-    }
-  },
-  methods: {
-    selectTag(tag) {
-      this.current = tag
-    },
-    async deleteTag(tag) {
-      this.$store.commit(`loadingStart`, 'admin-tags-delete')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($id: Int!) {
-              pages {
-                deleteTag (id: $id) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            id: tag.id
-          }
-        })
-        if (_.get(resp, 'data.pages.deleteTag.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            message: this.$t('tags.deleteSuccess'),
-            style: 'success',
-            icon: 'check'
-          })
-          this.refresh()
-        } else {
-          throw new Error(_.get(resp, 'data.pages.deleteTag.responseResult.message', 'An unexpected error occurred.'))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.deleteTagDialog = false
-      this.$store.commit(`loadingStop`, 'admin-tags-delete')
-    },
-    async saveTag(tag) {
-      this.$store.commit(`loadingStart`, 'admin-tags-save')
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($id: Int!, $tag: String!, $title: String!) {
-              pages {
-                updateTag (id: $id, tag: $tag, title: $title) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            id: tag.id,
-            tag: tag.tag,
-            title: tag.title
-          }
-        })
-        if (_.get(resp, 'data.pages.updateTag.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            message: this.$t('tags.saveSuccess'),
-            style: 'success',
-            icon: 'check'
-          })
-          this.current.updatedAt = new Date()
-        } else {
-          throw new Error(_.get(resp, 'data.pages.updateTag.responseResult.message', 'An unexpected error occurred.'))
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-tags-save')
-    },
-    async refresh() {
-      await this.$apollo.queries.tags.refetch()
-      this.current = {}
-      this.$store.commit('showNotification', {
-        message: this.$t('tags.refreshSuccess'),
-        style: 'success',
-        icon: 'cached'
-      })
-    }
-  },
-  apollo: {
-    tags: {
-      query: gql`
-        {
-          pages {
-            tags {
-              id
-              tag
-              title
-              createdAt
-              updatedAt
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.pages.tags),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-tags-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss' scoped>
-
-.clickable {
-  cursor: pointer;
-
-  &:hover {
-    background-color: rgba(mc('blue', '500'), .25);
-  }
-}
-
-</style>

+ 0 - 255
client/components/admin/admin-theme.vue

@@ -1,255 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-paint-palette.svg', alt='Theme', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('admin:theme.title')}}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:theme.subtitle')}}
-          v-spacer
-          v-btn.animated.fadeInRight(color='success', depressed, @click='save', large, :loading='loading')
-            v-icon(left) mdi-check
-            span {{$t('common:actions.apply')}}
-        v-form.pt-3
-          v-layout(row wrap)
-            v-flex(lg6 xs12)
-              v-card.animated.fadeInUp
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{$t('admin:theme.title')}}
-                v-card-text
-                  v-select(
-                    :items='themes'
-                    outlined
-                    prepend-icon='mdi-palette'
-                    v-model='config.theme'
-                    :label='$t(`admin:theme.siteTheme`)'
-                    persistent-hint
-                    :hint='$t(`admin:theme.siteThemeHint`)'
-                    )
-                    template(slot='item', slot-scope='data')
-                      v-list-item-avatar
-                        v-icon.blue--text(dark) mdi-image-filter-frames
-                      v-list-item-content
-                        v-list-item-title(v-html='data.item.text')
-                        v-list-item-sub-title(v-html='data.item.author')
-                  v-select.mt-3(
-                    :items='iconsets'
-                    outlined
-                    prepend-icon='mdi-paw'
-                    v-model='config.iconset'
-                    :label='$t(`admin:theme.iconset`)'
-                    persistent-hint
-                    :hint='$t(`admin:theme.iconsetHint`)'
-                    )
-                  v-divider.mt-3
-                  v-switch(
-                    inset
-                    v-model='darkMode'
-                    :label='$t(`admin:theme.darkMode`)'
-                    color='primary'
-                    persistent-hint
-                    :hint='$t(`admin:theme.darkModeHint`)'
-                    )
-
-              v-card.mt-3.animated.fadeInUp.wait-p1s
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{$t(`admin:theme.options`)}}
-                  v-spacer
-                  v-chip(label, color='white', small).primary--text coming soon
-                v-card-text
-                  v-select(
-                    :items='[]'
-                    outlined
-                    prepend-icon='mdi-border-vertical'
-                    v-model='config.iconset'
-                    label='Table of Contents Position'
-                    persistent-hint
-                    hint='Select whether the table of contents is shown on the left, right or not at all.'
-                    disabled
-                    )
-
-            v-flex(lg6 xs12)
-              //- v-card.animated.fadeInUp.wait-p2s
-              //-   v-toolbar(color='teal', dark, dense, flat)
-              //-     v-toolbar-title.subtitle-1 {{$t('admin:theme.downloadThemes')}}
-              //-     v-spacer
-              //-     v-chip(label, color='white', small).teal--text coming soon
-              //-   v-data-table(
-              //-     :headers='headers',
-              //-     :items='themes',
-              //-     hide-default-footer,
-              //-     item-key='value',
-              //-     :items-per-page='1000'
-              //-   )
-              //-     template(v-slot:item='thm')
-              //-       td
-              //-         strong {{thm.item.text}}
-              //-       td
-              //-         span {{ thm.item.author }}
-              //-       td.text-xs-center
-              //-         v-progress-circular(v-if='thm.item.isDownloading', indeterminate, color='blue', size='20', :width='2')
-              //-         v-btn(v-else-if='thm.item.isInstalled && thm.item.installDate < thm.item.updatedAt', icon)
-              //-           v-icon.blue--text mdi-cached
-              //-         v-btn(v-else-if='thm.item.isInstalled', icon)
-              //-           v-icon.green--text mdi-check-bold
-              //-         v-btn(v-else, icon)
-              //-           v-icon.grey--text mdi-cloud-download
-
-              v-card.animated.fadeInUp.wait-p2s
-                v-toolbar(color='primary', dark, dense, flat)
-                  v-toolbar-title.subtitle-1 {{$t(`admin:theme.codeInjection`)}}
-                v-card-text
-                  v-textarea.is-monospaced(
-                    v-model='config.injectCSS'
-                    :label='$t(`admin:theme.cssOverride`)'
-                    outlined
-                    color='primary'
-                    persistent-hint
-                    :hint='$t(`admin:theme.cssOverrideHint`)'
-                    auto-grow
-                    )
-                  i18next.caption.pl-2.ml-1(path='admin:theme.cssOverrideWarning', tag='div')
-                    strong.red--text(place='caution') {{$t('admin:theme.cssOverrideWarningCaution')}}
-                    code(place='cssClass') .contents
-                  v-textarea.is-monospaced.mt-3(
-                    v-model='config.injectHead'
-                    :label='$t(`admin:theme.headHtmlInjection`)'
-                    outlined
-                    color='primary'
-                    persistent-hint
-                    :hint='$t(`admin:theme.headHtmlInjectionHint`)'
-                    auto-grow
-                    )
-                  v-textarea.is-monospaced.mt-2(
-                    v-model='config.injectBody'
-                    :label='$t(`admin:theme.bodyHtmlInjection`)'
-                    outlined
-                    color='primary'
-                    persistent-hint
-                    :hint='$t(`admin:theme.bodyHtmlInjectionHint`)'
-                    auto-grow
-                    )
-</template>
-
-<script>
-import _ from 'lodash'
-import { sync } from 'vuex-pathify'
-
-import themeConfigQuery from 'gql/admin/theme/theme-query-config.gql'
-import themeSaveMutation from 'gql/admin/theme/theme-mutation-save.gql'
-
-export default {
-  data() {
-    return {
-      loading: false,
-      themes: [
-        { text: 'Default', author: 'requarks.io', value: 'default', isInstalled: true, installDate: '', updatedAt: '' }
-      ],
-      iconsets: [
-        { text: 'Material Design Icons (default)', value: 'mdi' },
-        { text: 'Font Awesome 5', value: 'fa' },
-        { text: 'Font Awesome 4', value: 'fa4' }
-      ],
-      config: {
-        theme: 'default',
-        darkMode: false,
-        iconset: '',
-        injectCSS: '',
-        injectHead: '',
-        injectBody: ''
-      },
-      darkModeInitial: false
-    }
-  },
-  computed: {
-    darkMode: sync('site/dark'),
-    headers() {
-      return [
-        {
-          text: this.$t('admin:theme.downloadName'),
-          align: 'left',
-          value: 'text'
-        },
-        {
-          text: this.$t('admin:theme.downloadAuthor'),
-          align: 'left',
-          value: 'author'
-        },
-        {
-          text: this.$t('admin:theme.downloadDownload'),
-          align: 'center',
-          value: 'value',
-          sortable: false,
-          width: 100
-        }
-      ]
-    }
-  },
-  watch: {
-    'darkMode' (newValue, oldValue) {
-      this.$vuetify.theme.dark = newValue
-    }
-  },
-  mounted() {
-    this.darkModeInitial = this.darkMode
-  },
-  beforeDestroy() {
-    this.darkMode = this.darkModeInitial
-    this.$vuetify.theme.dark = this.darkModeInitial
-  },
-  methods: {
-    async save () {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-theme-save')
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: themeSaveMutation,
-          variables: {
-            theme: this.config.theme,
-            iconset: this.config.iconset,
-            darkMode: this.darkMode,
-            injectCSS: this.config.injectCSS,
-            injectHead: this.config.injectHead,
-            injectBody: this.config.injectBody
-          }
-        })
-        const resp = _.get(respRaw, 'data.theming.setConfig.responseResult', {})
-        if (resp.succeeded) {
-          this.darkModeInitial = this.darkMode
-          this.$store.commit('showNotification', {
-            message: 'Theme settings updated successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-      this.$store.commit(`loadingStop`, 'admin-theme-save')
-      this.loading = false
-    }
-  },
-  apollo: {
-    config: {
-      query: themeConfigQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.theming.config,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-theme-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-.v-textarea.is-monospaced textarea {
-  font-family: 'Roboto Mono', 'Courier New', Courier, monospace;
-  font-size: 13px;
-  font-weight: 600;
-  line-height: 1.4;
-}
-</style>

+ 0 - 256
client/components/admin/admin-users-create.vue

@@ -1,256 +0,0 @@
-<template lang="pug">
-  v-dialog(v-model='isShown', max-width='650', persistent)
-    v-card
-      .dialog-header.is-short
-        v-icon.mr-3(color='white') mdi-plus
-        span New User
-        v-spacer
-        v-btn.mx-0(color='white', outlined, disabled, dark)
-          v-icon(left) mdi-database-import
-          span Bulk Import
-      v-card-text.pt-5
-        v-select(
-          :items='providers'
-          item-text='displayName'
-          item-value='key'
-          outlined
-          prepend-icon='mdi-domain'
-          v-model='provider'
-          label='Provider'
-          )
-        v-text-field(
-          outlined
-          prepend-icon='mdi-at'
-          v-model='email'
-          label='Email Address'
-          key='newUserEmail'
-          persistent-hint
-          ref='emailInput'
-          )
-        v-text-field(
-          v-if='provider === `local`'
-          outlined
-          prepend-icon='mdi-lock-outline'
-          append-icon='mdi-dice-5'
-          v-model='password'
-          :label='mustChangePwd ? `Temporary Password` : `Password`'
-          counter='255'
-          @click:append='generatePwd'
-          key='newUserPassword'
-          persistent-hint
-          )
-        v-text-field(
-          outlined
-          prepend-icon='mdi-account-outline'
-          v-model='name'
-          label='Name'
-          :hint='provider === `local` ? `Can be changed by the user.` : `May be overwritten by the provider during login.`'
-          key='newUserName'
-          persistent-hint
-          )
-        v-select.mt-2(
-          :items='groups'
-          item-text='name'
-          item-value='id'
-          item-disabled='isSystem'
-          outlined
-          prepend-icon='mdi-account-group'
-          v-model='group'
-          label='Assign to Group(s)...'
-          hint='Note that you cannot assign users to the Administrators or Guests groups from this dialog.'
-          persistent-hint
-          clearable
-          multiple
-          )
-        v-divider
-        v-checkbox(
-          color='primary'
-          label='Require password change on first login'
-          v-if='provider === `local`'
-          v-model='mustChangePwd'
-          hide-details
-        )
-        v-checkbox(
-          color='primary'
-          label='Send a welcome email'
-          hide-details
-          v-model='sendWelcomeEmail'
-          disabled
-        )
-      v-card-chin
-        v-spacer
-        v-btn(text, @click='isShown = false') Cancel
-        v-btn.px-3(depressed, color='primary', @click='newUser(false)')
-          v-icon(left) mdi-chevron-right
-          span Create
-        v-btn.px-3(depressed, color='primary', @click='newUser(true)')
-          v-icon(left) mdi-chevron-double-right
-          span Create and Close
-</template>
-
-<script>
-import _ from 'lodash'
-import validate from 'validate.js'
-import gql from 'graphql-tag'
-
-import createUserMutation from 'gql/admin/users/users-mutation-create.gql'
-import groupsQuery from 'gql/admin/users/users-query-groups.gql'
-
-export default {
-  props: {
-    value: {
-      type: Boolean,
-      default: false
-    }
-  },
-  data() {
-    return {
-      providers: [],
-      provider: 'local',
-      email: '',
-      password: '',
-      name: '',
-      groups: [],
-      group: [],
-      mustChangePwd: false,
-      sendWelcomeEmail: false
-    }
-  },
-  computed: {
-    isShown: {
-      get() { return this.value },
-      set(val) { this.$emit('input', val) }
-    }
-  },
-  watch: {
-    value(newValue, oldValue) {
-      if (newValue) {
-        this.$nextTick(() => {
-          this.$refs.emailInput.focus()
-        })
-      }
-    }
-  },
-  methods: {
-    async newUser(close = false) {
-      let rules = {
-        email: {
-          presence: {
-            allowEmpty: false
-          },
-          email: true
-        },
-        name: {
-          presence: {
-            allowEmpty: false
-          },
-          length: {
-            minimum: 2,
-            maximum: 255
-          }
-        }
-      }
-      if (this.provider === `local`) {
-        rules.password = {
-          presence: {
-            allowEmpty: false
-          },
-          length: {
-            minimum: 6,
-            maximum: 255
-          }
-        }
-      }
-      const validationResults = validate({
-        email: this.email,
-        password: this.password,
-        name: this.name
-      }, rules, { format: 'flat' })
-
-      if (validationResults) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: validationResults[0],
-          icon: 'alert'
-        })
-        return
-      }
-
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: createUserMutation,
-          variables: {
-            providerKey: this.provider,
-            email: this.email,
-            passwordRaw: this.password,
-            name: this.name,
-            groups: this.group,
-            mustChangePassword: this.mustChangePwd,
-            sendWelcomeEmail: this.sendWelcomeEmail
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-create')
-          }
-        })
-        if (_.get(resp, 'data.users.create.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: 'New user created successfully.',
-            icon: 'check'
-          })
-
-          this.email = ''
-          this.password = ''
-          this.name = ''
-
-          if (close) {
-            this.isShown = false
-            this.$emit('refresh')
-          } else {
-            this.$refs.emailInput.focus()
-          }
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _.get(resp, 'data.users.create.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'alert'
-          })
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    },
-    generatePwd() {
-      const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
-      this.password = _.sampleSize(pwdChars, 12).join('')
-    }
-  },
-  apollo: {
-    providers: {
-      query: gql`
-        query {
-          authentication {
-            activeStrategies {
-              key
-              displayName
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => data.authentication.activeStrategies,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
-      }
-    },
-    groups: {
-      query: groupsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.groups.list,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-groups-refresh')
-      }
-    }
-  }
-}
-</script>

+ 0 - 1077
client/components/admin/admin-users-edit.vue

@@ -1,1077 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-male-user.svg', :alt='$t(`admin:users.edit`)', style='width: 80px;')
-          .admin-header-title
-            .headline.blue--text.text--darken-2.animated.fadeInLeft {{$t('admin:users.edit')}}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{user.name}}
-          v-spacer
-          i18next.pr-4.caption.grey--text.animated.fadeInDown(path='admin:users.id', tag='div')
-            strong(place='id') {{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
-          v-btn.ml-3.animated.fadeInDown.wait-p3s(color='grey', icon, outlined, to='/users')
-            v-icon mdi-arrow-left
-          v-menu(offset-y, origin='top right')
-            template(v-slot:activator='{ on }')
-              v-btn.ml-3.animated.fadeInDown.wait-p2s(color='black', v-on='on', depressed, dark)
-                span Actions
-                v-icon(right) mdi-chevron-down
-            v-list(dense, nav)
-              v-list-item(v-if='!user.isActive', @click='activateUser')
-                v-list-item-icon
-                  v-icon(color='purple') mdi-account-key
-                v-list-item-title Activate
-              v-list-item(v-else, @click='deactivateUser', :disabled='user.id == currentUserId || user.isSystem')
-                v-list-item-icon
-                  v-icon(color='purple') mdi-account-cancel
-                v-list-item-title Deactivate
-              v-list-item(@click='verifyUser', :disabled='user.isVerified')
-                v-list-item-icon
-                  v-icon(color='blue') mdi-account-check
-                v-list-item-title Set as Verified
-              v-list-item(@click='deleteUserConfirm', :disabled='user.id == currentUserId || user.isSystem')
-                v-list-item-icon
-                  v-icon(color='red') mdi-trash-can-outline
-                v-list-item-title Delete
-          v-btn.ml-3.animated.fadeInDown(color='primary', large, depressed, @click='updateUser')
-            v-icon(left) mdi-check
-            span {{$t('admin:users.updateUser')}}
-      v-flex(xs6)
-        v-card.animated.fadeInUp
-          v-toolbar(color='primary', dense, dark, flat)
-            v-icon.mr-2 mdi-information-variant
-            span {{$t('admin:users.basicInfo')}}
-          v-list.py-0(two-line, dense)
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-email-variant
-              v-list-item-content
-                v-list-item-title {{$t('admin:users.email')}}
-                v-list-item-subtitle {{ user.email }}
-              v-list-item-action(v-if='!user.isSystem && user.providerKey === `local`')
-                v-menu(
-                  v-model='editPop.email'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptEmail`)')
-                      v-icon mdi-pencil
-                  v-card
-                    v-text-field(
-                      ref='iptEmail'
-                      v-model='user.email'
-                      :label='$t(`admin:users.email`)'
-                      solo
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.email = false'
-                      @keydown.enter='editPop.email = false'
-                      @keydown.esc='editPop.email = false'
-                    )
-
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-account
-              v-list-item-content
-                v-list-item-title {{$t('admin:users.displayName')}}
-                v-list-item-subtitle {{ user.name }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.name'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptDisplayName`)')
-                      v-icon mdi-pencil
-                  v-card
-                    v-text-field(
-                      ref='iptDisplayName'
-                      v-model='user.name'
-                      :label='$t(`admin:users.displayName`)'
-                      solo
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.name = false'
-                      @keydown.enter='editPop.name = false'
-                      @keydown.esc='editPop.name = false'
-                    )
-
-        v-card.mt-3.animated.fadeInUp.wait-p2s(v-if='!user.isSystem')
-          v-toolbar(color='primary', dense, dark, flat)
-            v-icon.mr-2 mdi-lock-outline
-            span {{$t('admin:users.authentication')}}
-          v-list.py-0(two-line, dense)
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-domain
-              v-list-item-content
-                v-list-item-title {{$t('admin:users.authProvider')}}
-                v-list-item-subtitle {{ user.providerName }} #[em.caption ({{ user.providerKey }})]
-            template(v-if='user.providerKey === `local`')
-              v-divider
-              v-list-item
-                v-list-item-avatar(size='32')
-                  v-icon mdi-form-textbox-password
-                v-list-item-content
-                  v-list-item-title {{$t('admin:users.password')}}
-                  v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
-                v-list-item-action
-                  v-menu(
-                    v-model='editPop.newPassword'
-                    :close-on-content-click='false'
-                    min-width='350'
-                    left
-                    )
-                    template(v-slot:activator='{ on: menu }')
-                      v-tooltip(top)
-                        template(v-slot:activator='{ on: tooltip }')
-                          v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)')
-                            v-icon mdi-pencil
-                        span {{$t('admin:users.changePassword')}}
-                    v-card
-                      v-text-field(
-                        ref='iptNewPassword'
-                        v-model='newPassword'
-                        :label='$t(`admin:users.newPassword`)'
-                        solo
-                        hide-details
-                        append-icon='mdi-check'
-                        type='password'
-                        @click:append='editPop.newPassword = false'
-                        @keydown.enter='editPop.newPassword = false'
-                        @keydown.esc='editPop.newPassword = false'
-                      )
-                v-list-item-action
-                  v-tooltip(top)
-                    template(v-slot:activator='{ on }')
-                      v-btn(icon, color='grey', x-small, v-on='on', disabled)
-                        v-icon mdi-email
-                    span Send Password Reset Email
-            template(v-if='user.providerIs2FACapable')
-              v-divider
-              v-list-item
-                v-list-item-avatar(size='32')
-                  v-icon mdi-two-factor-authentication
-                v-list-item-content
-                  v-list-item-title {{$t('admin:users.tfa')}}
-                  v-list-item-subtitle.green--text(v-if='user.tfaIsActive') Active
-                  v-list-item-subtitle.red--text(v-else) Inactive
-                v-list-item-action
-                  v-tooltip(top)
-                    template(v-slot:activator='{ on }')
-                      v-btn(icon, color='grey', x-small, v-on='on', @click='toggle2FA')
-                        v-icon mdi-power
-                    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-toolbar(color='primary', dense, dark, flat)
-            v-icon.mr-2 mdi-account-group
-            span {{$t('admin:users.groups')}}
-          v-list(dense)
-            template(v-for='(group, idx) in user.groups')
-              v-list-item(:key='`group-` + group.id')
-                v-list-item-avatar(size='32')
-                  v-icon mdi-account-group-outline
-                v-list-item-content
-                  v-list-item-title {{group.name}}
-                v-list-item-action(v-if='!user.isSystem')
-                  v-btn(icon, color='red', x-small, @click='unassignGroup(group.id)')
-                    v-icon mdi-close
-              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')
-            .caption {{$t('admin:users.noGroupAssigned')}}
-          v-card-chin(v-if='!user.isSystem')
-            v-spacer
-            v-select(
-              ref='iptAssignGroup'
-              :items='groups'
-              v-model='newGroup'
-              :label='$t(`admin:users.selectGroup`)'
-              item-value='id'
-              item-text='name'
-              item-disabled='isSystem'
-              solo
-              flat
-              hide-details
-              @keydown.esc='editPop.assignGroup = false'
-              style='max-width: 300px;'
-              dense
-            )
-            v-btn.ml-2.px-4(depressed, color='primary', @click='assignGroup', :disabled='newGroup === 0')
-              v-icon(left) mdi-clipboard-account-outline
-              span {{$t('admin:users.groupAssign')}}
-          v-system-bar(window, :color='$vuetify.theme.dark ? `grey darken-4-l3` : `grey lighten-3`')
-            v-spacer
-            .caption {{$t('admin:users.groupAssignNotice')}}
-
-      v-flex(xs6)
-        v-card.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, dark, flat)
-            v-icon.mr-2 mdi-account-badge-outline
-            span {{$t('admin:users.extendedMetadata')}}
-          v-list.py-0(two-line, dense)
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-map-marker
-              v-list-item-content
-                v-list-item-title {{$t('admin:users.location')}}
-                v-list-item-subtitle {{ user.location }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.location'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptLocation`)')
-                      v-icon mdi-pencil
-                  v-card
-                    v-text-field(
-                      ref='iptLocation'
-                      v-model='user.location'
-                      :label='$t(`admin:users.location`)'
-                      solo
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.location = false'
-                      @keydown.enter='editPop.location = false'
-                      @keydown.esc='editPop.location = false'
-                    )
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-briefcase
-              v-list-item-content
-                v-list-item-title {{$t('admin:users.jobTitle')}}
-                v-list-item-subtitle {{ user.jobTitle }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.jobTitle'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptJobTitle`)')
-                      v-icon mdi-pencil
-                  v-card
-                    v-text-field(
-                      ref='iptJobTitle'
-                      v-model='user.jobTitle'
-                      :label='$t(`admin:users.jobTitle`)'
-                      solo
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.jobTitle = false'
-                      @keydown.enter='editPop.jobTitle = false'
-                      @keydown.esc='editPop.jobTitle = false'
-                    )
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-map-clock-outline
-              v-list-item-content
-                v-list-item-title {{$t('admin:users.timezone')}}
-                v-list-item-subtitle {{ user.timezone }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.timezone'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(icon, color='grey', x-small, v-on='on', @click='focusField(`iptTimezone`)')
-                      v-icon mdi-pencil
-                  v-card
-                    v-select(
-                      ref='iptTimezone'
-                      :items='timezones'
-                      v-model='user.timezone'
-                      :label='$t(`admin:users.timezone`)'
-                      solo
-                      dense
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.timezone = false'
-                      @keydown.enter='editPop.timezone = false'
-                      @keydown.esc='editPop.timezone = false'
-                    )
-
-        v-card.mt-3.animated.fadeInUp.wait-p4s
-          v-toolbar(color='teal', dark, dense, flat)
-            v-toolbar-title
-              .subtitle-1 {{$t('profile:activity.title')}}
-          v-card-text.grey--text.text--darken-2
-            .caption.grey--text {{$t('profile:activity.joinedOn')}}
-            .body-2: strong {{ user.createdAt | moment('LLLL') }}
-            .caption.grey--text.mt-3 {{$t('profile:activity.lastUpdatedOn')}}
-            .body-2: strong {{ user.updatedAt | moment('LLLL') }}
-            .caption.grey--text.mt-3 {{$t('profile:activity.lastLoginOn')}}
-            .body-2: strong {{ user.lastLoginAt | moment('LLLL') }}
-
-        v-card.mt-3.animated.fadeInUp.wait-p6s
-          v-toolbar(color='teal', dense, dark, flat)
-            v-icon.mr-2 mdi-file-document-box-multiple-outline
-            span Content
-          v-card-text
-            em.caption.grey--text Coming soon
-
-    v-dialog(v-model='deleteUserDialog', max-width='500')
-      v-card
-        .dialog-header.is-red {{$t('admin:users.deleteConfirmTitle')}}
-        v-card-text.pt-5
-          i18next(path='admin:users.deleteConfirmText', tag='span')
-            strong(place='username') {{ user.email }}
-          .mt-3 {{$t('admin:users.deleteConfirmReplaceWarn')}}
-          v-divider.my-3
-          .d-flex.align-center.mt-3
-            v-btn.text-none(color='primary', depressed, @click='deleteSearchUserDialog = true')
-              v-icon(left) mdi-clipboard-account
-              | Select User...
-            .caption.pl-3
-              strong ID {{deleteReplaceUser.id}}
-              .caption {{deleteReplaceUser.name}}
-              em {{deleteReplaceUser.email}}
-        v-card-chin
-          v-spacer
-          v-btn(text, @click='deleteUserDialog = false') {{$t('common:actions.cancel')}}
-          v-btn(color='red', dark, @click='deleteUser') {{$t('common:actions.delete')}}
-
-        user-search(v-model='deleteSearchUserDialog', @select='assignDeleteUser')
-
-</template>
-<script>
-import _ from 'lodash'
-import { get } from 'vuex-pathify'
-import gql from 'graphql-tag'
-import { StatusIndicator } from 'vue-status-indicator'
-
-import UserSearch from '../common/user-search.vue'
-
-import groupsQuery from 'gql/admin/users/users-query-groups.gql'
-
-export default {
-  i18nOptions: {
-    namespaces: ['admin', 'profile']
-  },
-  components: {
-    StatusIndicator,
-    UserSearch
-  },
-  data () {
-    return {
-      deleteUserDialog: false,
-      deleteSearchUserDialog: false,
-      deleteReplaceUser: {
-        id: 1,
-        name: '',
-        email: ''
-      },
-      editPop: {
-        email: false,
-        name: false,
-        pwd: false,
-        location: false,
-        jobTitle: false,
-        timezone: false,
-        newPassword: false,
-        assignGroup: false
-      },
-      newGroup: 0,
-      newPassword: '',
-      user: {
-        email: '',
-        name: '',
-        location: '',
-        jobTitle: '',
-        timezone: '',
-        groups: [],
-        isActive: false,
-        isVerified: false
-      },
-      timezones: [
-        { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
-        { text: '(GMT-11:00) Pago Pago', value: 'Pacific/Pago_Pago' },
-        { text: '(GMT-10:00) Hawaii Time', value: 'Pacific/Honolulu' },
-        { text: '(GMT-10:00) Rarotonga', value: 'Pacific/Rarotonga' },
-        { text: '(GMT-10:00) Tahiti', value: 'Pacific/Tahiti' },
-        { text: '(GMT-09:30) Marquesas', value: 'Pacific/Marquesas' },
-        { text: '(GMT-09:00) Alaska Time', value: 'America/Anchorage' },
-        { text: '(GMT-09:00) Gambier', value: 'Pacific/Gambier' },
-        { text: '(GMT-08:00) Pacific Time', value: 'America/Los_Angeles' },
-        { text: '(GMT-08:00) Pacific Time - Tijuana', value: 'America/Tijuana' },
-        { text: '(GMT-08:00) Pacific Time - Vancouver', value: 'America/Vancouver' },
-        { text: '(GMT-08:00) Pacific Time - Whitehorse', value: 'America/Whitehorse' },
-        { text: '(GMT-08:00) Pitcairn', value: 'Pacific/Pitcairn' },
-        { text: '(GMT-07:00) Mountain Time', value: 'America/Denver' },
-        { text: '(GMT-07:00) Mountain Time - Arizona', value: 'America/Phoenix' },
-        { text: '(GMT-07:00) Mountain Time - Chihuahua, Mazatlan', value: 'America/Mazatlan' },
-        { text: '(GMT-07:00) Mountain Time - Dawson Creek', value: 'America/Dawson_Creek' },
-        { text: '(GMT-07:00) Mountain Time - Edmonton', value: 'America/Edmonton' },
-        { text: '(GMT-07:00) Mountain Time - Hermosillo', value: 'America/Hermosillo' },
-        { text: '(GMT-07:00) Mountain Time - Yellowknife', value: 'America/Yellowknife' },
-        { text: '(GMT-06:00) Belize', value: 'America/Belize' },
-        { text: '(GMT-06:00) Central Time', value: 'America/Chicago' },
-        { text: '(GMT-06:00) Central Time - Mexico City', value: 'America/Mexico_City' },
-        { text: '(GMT-06:00) Central Time - Regina', value: 'America/Regina' },
-        { text: '(GMT-06:00) Central Time - Tegucigalpa', value: 'America/Tegucigalpa' },
-        { text: '(GMT-06:00) Central Time - Winnipeg', value: 'America/Winnipeg' },
-        { text: '(GMT-06:00) Costa Rica', value: 'America/Costa_Rica' },
-        { text: '(GMT-06:00) El Salvador', value: 'America/El_Salvador' },
-        { text: '(GMT-06:00) Galapagos', value: 'Pacific/Galapagos' },
-        { text: '(GMT-06:00) Guatemala', value: 'America/Guatemala' },
-        { text: '(GMT-06:00) Managua', value: 'America/Managua' },
-        { text: '(GMT-05:00) America Cancun', value: 'America/Cancun' },
-        { text: '(GMT-05:00) Bogota', value: 'America/Bogota' },
-        { text: '(GMT-05:00) Easter Island', value: 'Pacific/Easter' },
-        { text: '(GMT-05:00) Eastern Time', value: 'America/New_York' },
-        { text: '(GMT-05:00) Eastern Time - Iqaluit', value: 'America/Iqaluit' },
-        { text: '(GMT-05:00) Eastern Time - Toronto', value: 'America/Toronto' },
-        { text: '(GMT-05:00) Guayaquil', value: 'America/Guayaquil' },
-        { text: '(GMT-05:00) Havana', value: 'America/Havana' },
-        { text: '(GMT-05:00) Jamaica', value: 'America/Jamaica' },
-        { text: '(GMT-05:00) Lima', value: 'America/Lima' },
-        { text: '(GMT-05:00) Nassau', value: 'America/Nassau' },
-        { text: '(GMT-05:00) Panama', value: 'America/Panama' },
-        { text: '(GMT-05:00) Port-au-Prince', value: 'America/Port-au-Prince' },
-        { text: '(GMT-05:00) Rio Branco', value: 'America/Rio_Branco' },
-        { text: '(GMT-04:00) Atlantic Time - Halifax', value: 'America/Halifax' },
-        { text: '(GMT-04:00) Barbados', value: 'America/Barbados' },
-        { text: '(GMT-04:00) Bermuda', value: 'Atlantic/Bermuda' },
-        { text: '(GMT-04:00) Boa Vista', value: 'America/Boa_Vista' },
-        { text: '(GMT-04:00) Caracas', value: 'America/Caracas' },
-        { text: '(GMT-04:00) Curacao', value: 'America/Curacao' },
-        { text: '(GMT-04:00) Grand Turk', value: 'America/Grand_Turk' },
-        { text: '(GMT-04:00) Guyana', value: 'America/Guyana' },
-        { text: '(GMT-04:00) La Paz', value: 'America/La_Paz' },
-        { text: '(GMT-04:00) Manaus', value: 'America/Manaus' },
-        { text: '(GMT-04:00) Martinique', value: 'America/Martinique' },
-        { text: '(GMT-04:00) Port of Spain', value: 'America/Port_of_Spain' },
-        { text: '(GMT-04:00) Porto Velho', value: 'America/Porto_Velho' },
-        { text: '(GMT-04:00) Puerto Rico', value: 'America/Puerto_Rico' },
-        { text: '(GMT-04:00) Santo Domingo', value: 'America/Santo_Domingo' },
-        { text: '(GMT-04:00) Thule', value: 'America/Thule' },
-        { text: '(GMT-03:30) Newfoundland Time - St. Johns', value: 'America/St_Johns' },
-        { text: '(GMT-03:00) Araguaina', value: 'America/Araguaina' },
-        { text: '(GMT-03:00) Asuncion', value: 'America/Asuncion' },
-        { text: '(GMT-03:00) Belem', value: 'America/Belem' },
-        { text: '(GMT-03:00) Buenos Aires', value: 'America/Argentina/Buenos_Aires' },
-        { text: '(GMT-03:00) Campo Grande', value: 'America/Campo_Grande' },
-        { text: '(GMT-03:00) Cayenne', value: 'America/Cayenne' },
-        { text: '(GMT-03:00) Cuiaba', value: 'America/Cuiaba' },
-        { text: '(GMT-03:00) Fortaleza', value: 'America/Fortaleza' },
-        { text: '(GMT-03:00) Godthab', value: 'America/Godthab' },
-        { text: '(GMT-03:00) Maceio', value: 'America/Maceio' },
-        { text: '(GMT-03:00) Miquelon', value: 'America/Miquelon' },
-        { text: '(GMT-03:00) Montevideo', value: 'America/Montevideo' },
-        { text: '(GMT-03:00) Palmer', value: 'Antarctica/Palmer' },
-        { text: '(GMT-03:00) Paramaribo', value: 'America/Paramaribo' },
-        { text: '(GMT-03:00) Punta Arenas', value: 'America/Punta_Arenas' },
-        { text: '(GMT-03:00) Recife', value: 'America/Recife' },
-        { text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
-        { text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
-        { text: '(GMT-03:00) Santiago', value: 'America/Santiago' },
-        { text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },
-        { text: '(GMT-02:00) Noronha', value: 'America/Noronha' },
-        { text: '(GMT-02:00) Sao Paulo', value: 'America/Sao_Paulo' },
-        { text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
-        { text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
-        { text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },
-        { text: '(GMT-01:00) Scoresbysund', value: 'America/Scoresbysund' },
-        { text: '(GMT+00:00) Abidjan', value: 'Africa/Abidjan' },
-        { text: '(GMT+00:00) Accra', value: 'Africa/Accra' },
-        { text: '(GMT+00:00) Bissau', value: 'Africa/Bissau' },
-        { text: '(GMT+00:00) Canary Islands', value: 'Atlantic/Canary' },
-        { text: '(GMT+00:00) Casablanca', value: 'Africa/Casablanca' },
-        { text: '(GMT+00:00) Danmarkshavn', value: 'America/Danmarkshavn' },
-        { text: '(GMT+00:00) Dublin', value: 'Europe/Dublin' },
-        { text: '(GMT+00:00) El Aaiun', value: 'Africa/El_Aaiun' },
-        { text: '(GMT+00:00) Faeroe', value: 'Atlantic/Faroe' },
-        { text: '(GMT+00:00) GMT (no daylight saving)', value: 'Etc/GMT' },
-        { text: '(GMT+00:00) Lisbon', value: 'Europe/Lisbon' },
-        { text: '(GMT+00:00) London', value: 'Europe/London' },
-        { text: '(GMT+00:00) Monrovia', value: 'Africa/Monrovia' },
-        { text: '(GMT+00:00) Reykjavik', value: 'Atlantic/Reykjavik' },
-        { text: '(GMT+01:00) Algiers', value: 'Africa/Algiers' },
-        { text: '(GMT+01:00) Amsterdam', value: 'Europe/Amsterdam' },
-        { text: '(GMT+01:00) Andorra', value: 'Europe/Andorra' },
-        { text: '(GMT+01:00) Berlin', value: 'Europe/Berlin' },
-        { text: '(GMT+01:00) Brussels', value: 'Europe/Brussels' },
-        { text: '(GMT+01:00) Budapest', value: 'Europe/Budapest' },
-        { text: '(GMT+01:00) Central European Time - Belgrade', value: 'Europe/Belgrade' },
-        { text: '(GMT+01:00) Central European Time - Prague', value: 'Europe/Prague' },
-        { text: '(GMT+01:00) Ceuta', value: 'Africa/Ceuta' },
-        { text: '(GMT+01:00) Copenhagen', value: 'Europe/Copenhagen' },
-        { text: '(GMT+01:00) Gibraltar', value: 'Europe/Gibraltar' },
-        { text: '(GMT+01:00) Lagos', value: 'Africa/Lagos' },
-        { text: '(GMT+01:00) Luxembourg', value: 'Europe/Luxembourg' },
-        { text: '(GMT+01:00) Madrid', value: 'Europe/Madrid' },
-        { text: '(GMT+01:00) Malta', value: 'Europe/Malta' },
-        { text: '(GMT+01:00) Monaco', value: 'Europe/Monaco' },
-        { text: '(GMT+01:00) Ndjamena', value: 'Africa/Ndjamena' },
-        { text: '(GMT+01:00) Oslo', value: 'Europe/Oslo' },
-        { text: '(GMT+01:00) Paris', value: 'Europe/Paris' },
-        { text: '(GMT+01:00) Rome', value: 'Europe/Rome' },
-        { text: '(GMT+01:00) Stockholm', value: 'Europe/Stockholm' },
-        { text: '(GMT+01:00) Tirane', value: 'Europe/Tirane' },
-        { text: '(GMT+01:00) Tunis', value: 'Africa/Tunis' },
-        { text: '(GMT+01:00) Vienna', value: 'Europe/Vienna' },
-        { text: '(GMT+01:00) Warsaw', value: 'Europe/Warsaw' },
-        { text: '(GMT+01:00) Zurich', value: 'Europe/Zurich' },
-        { text: '(GMT+02:00) Amman', value: 'Asia/Amman' },
-        { text: '(GMT+02:00) Athens', value: 'Europe/Athens' },
-        { text: '(GMT+02:00) Beirut', value: 'Asia/Beirut' },
-        { text: '(GMT+02:00) Bucharest', value: 'Europe/Bucharest' },
-        { text: '(GMT+02:00) Cairo', value: 'Africa/Cairo' },
-        { text: '(GMT+02:00) Chisinau', value: 'Europe/Chisinau' },
-        { text: '(GMT+02:00) Damascus', value: 'Asia/Damascus' },
-        { text: '(GMT+02:00) Gaza', value: 'Asia/Gaza' },
-        { text: '(GMT+02:00) Helsinki', value: 'Europe/Helsinki' },
-        { text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
-        { text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
-        { text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },
-        { text: '(GMT+02:00) Kyiv', value: 'Europe/Kyiv' },
-        { text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },
-        { text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
-        { text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },
-        { text: '(GMT+02:00) Riga', value: 'Europe/Riga' },
-        { text: '(GMT+02:00) Sofia', value: 'Europe/Sofia' },
-        { text: '(GMT+02:00) Tallinn', value: 'Europe/Tallinn' },
-        { text: '(GMT+02:00) Tripoli', value: 'Africa/Tripoli' },
-        { text: '(GMT+02:00) Vilnius', value: 'Europe/Vilnius' },
-        { text: '(GMT+02:00) Windhoek', value: 'Africa/Windhoek' },
-        { text: '(GMT+03:00) Baghdad', value: 'Asia/Baghdad' },
-        { text: '(GMT+03:00) Istanbul', value: 'Europe/Istanbul' },
-        { text: '(GMT+03:00) Minsk', value: 'Europe/Minsk' },
-        { text: '(GMT+03:00) Moscow+00 - Moscow', value: 'Europe/Moscow' },
-        { text: '(GMT+03:00) Nairobi', value: 'Africa/Nairobi' },
-        { text: '(GMT+03:00) Qatar', value: 'Asia/Qatar' },
-        { text: '(GMT+03:00) Riyadh', value: 'Asia/Riyadh' },
-        { text: '(GMT+03:00) Syowa', value: 'Antarctica/Syowa' },
-        { text: '(GMT+03:30) Tehran', value: 'Asia/Tehran' },
-        { text: '(GMT+04:00) Baku', value: 'Asia/Baku' },
-        { text: '(GMT+04:00) Dubai', value: 'Asia/Dubai' },
-        { text: '(GMT+04:00) Mahe', value: 'Indian/Mahe' },
-        { text: '(GMT+04:00) Mauritius', value: 'Indian/Mauritius' },
-        { text: '(GMT+04:00) Moscow+01 - Samara', value: 'Europe/Samara' },
-        { text: '(GMT+04:00) Reunion', value: 'Indian/Reunion' },
-        { text: '(GMT+04:00) Tbilisi', value: 'Asia/Tbilisi' },
-        { text: '(GMT+04:00) Yerevan', value: 'Asia/Yerevan' },
-        { text: '(GMT+04:30) Kabul', value: 'Asia/Kabul' },
-        { text: '(GMT+05:00) Aqtau', value: 'Asia/Aqtau' },
-        { text: '(GMT+05:00) Aqtobe', value: 'Asia/Aqtobe' },
-        { text: '(GMT+05:00) Ashgabat', value: 'Asia/Ashgabat' },
-        { text: '(GMT+05:00) Dushanbe', value: 'Asia/Dushanbe' },
-        { text: '(GMT+05:00) Karachi', value: 'Asia/Karachi' },
-        { text: '(GMT+05:00) Kerguelen', value: 'Indian/Kerguelen' },
-        { text: '(GMT+05:00) Maldives', value: 'Indian/Maldives' },
-        { text: '(GMT+05:00) Mawson', value: 'Antarctica/Mawson' },
-        { text: '(GMT+05:00) Moscow+02 - Yekaterinburg', value: 'Asia/Yekaterinburg' },
-        { text: '(GMT+05:00) Tashkent', value: 'Asia/Tashkent' },
-        { text: '(GMT+05:30) Colombo', value: 'Asia/Colombo' },
-        { text: '(GMT+05:30) India Standard Time', value: 'Asia/Kolkata' },
-        { text: '(GMT+05:45) Kathmandu', value: 'Asia/Kathmandu' },
-        { text: '(GMT+06:00) Almaty', value: 'Asia/Almaty' },
-        { text: '(GMT+06:00) Bishkek', value: 'Asia/Bishkek' },
-        { text: '(GMT+06:00) Chagos', value: 'Indian/Chagos' },
-        { text: '(GMT+06:00) Dhaka', value: 'Asia/Dhaka' },
-        { text: '(GMT+06:00) Moscow+03 - Omsk', value: 'Asia/Omsk' },
-        { text: '(GMT+06:00) Thimphu', value: 'Asia/Thimphu' },
-        { text: '(GMT+06:00) Vostok', value: 'Antarctica/Vostok' },
-        { text: '(GMT+06:30) Cocos', value: 'Indian/Cocos' },
-        { text: '(GMT+06:30) Rangoon', value: 'Asia/Yangon' },
-        { text: '(GMT+07:00) Bangkok', value: 'Asia/Bangkok' },
-        { text: '(GMT+07:00) Christmas', value: 'Indian/Christmas' },
-        { text: '(GMT+07:00) Davis', value: 'Antarctica/Davis' },
-        { text: '(GMT+07:00) Hanoi', value: 'Asia/Saigon' },
-        { text: '(GMT+07:00) Hovd', value: 'Asia/Hovd' },
-        { text: '(GMT+07:00) Jakarta', value: 'Asia/Jakarta' },
-        { text: '(GMT+07:00) Moscow+04 - Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
-        { text: '(GMT+08:00) Brunei', value: 'Asia/Brunei' },
-        { text: '(GMT+08:00) China Time - Beijing', value: 'Asia/Shanghai' },
-        { text: '(GMT+08:00) Choibalsan', value: 'Asia/Choibalsan' },
-        { text: '(GMT+08:00) Hong Kong', value: 'Asia/Hong_Kong' },
-        { text: '(GMT+08:00) Kuala Lumpur', value: 'Asia/Kuala_Lumpur' },
-        { text: '(GMT+08:00) Macau', value: 'Asia/Macau' },
-        { text: '(GMT+08:00) Makassar', value: 'Asia/Makassar' },
-        { text: '(GMT+08:00) Manila', value: 'Asia/Manila' },
-        { text: '(GMT+08:00) Moscow+05 - Irkutsk', value: 'Asia/Irkutsk' },
-        { text: '(GMT+08:00) Singapore', value: 'Asia/Singapore' },
-        { text: '(GMT+08:00) Taipei', value: 'Asia/Taipei' },
-        { text: '(GMT+08:00) Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
-        { text: '(GMT+08:00) Western Time - Perth', value: 'Australia/Perth' },
-        { text: '(GMT+08:30) Pyongyang', value: 'Asia/Pyongyang' },
-        { text: '(GMT+09:00) Dili', value: 'Asia/Dili' },
-        { text: '(GMT+09:00) Jayapura', value: 'Asia/Jayapura' },
-        { text: '(GMT+09:00) Moscow+06 - Yakutsk', value: 'Asia/Yakutsk' },
-        { text: '(GMT+09:00) Palau', value: 'Pacific/Palau' },
-        { text: '(GMT+09:00) Seoul', value: 'Asia/Seoul' },
-        { text: '(GMT+09:00) Tokyo', value: 'Asia/Tokyo' },
-        { text: '(GMT+09:30) Central Time - Darwin', value: 'Australia/Darwin' },
-        { text: '(GMT+10:00) Dumont D\'Urville', value: 'Antarctica/DumontDUrville' },
-        { text: '(GMT+10:00) Eastern Time - Brisbane', value: 'Australia/Brisbane' },
-        { text: '(GMT+10:00) Guam', value: 'Pacific/Guam' },
-        { text: '(GMT+10:00) Moscow+07 - Vladivostok', value: 'Asia/Vladivostok' },
-        { text: '(GMT+10:00) Port Moresby', value: 'Pacific/Port_Moresby' },
-        { text: '(GMT+10:00) Truk', value: 'Pacific/Chuuk' },
-        { text: '(GMT+10:30) Central Time - Adelaide', value: 'Australia/Adelaide' },
-        { text: '(GMT+11:00) Casey', value: 'Antarctica/Casey' },
-        { text: '(GMT+11:00) Eastern Time - Hobart', value: 'Australia/Hobart' },
-        { text: '(GMT+11:00) Eastern Time - Melbourne, Sydney', value: 'Australia/Sydney' },
-        { text: '(GMT+11:00) Efate', value: 'Pacific/Efate' },
-        { text: '(GMT+11:00) Guadalcanal', value: 'Pacific/Guadalcanal' },
-        { text: '(GMT+11:00) Kosrae', value: 'Pacific/Kosrae' },
-        { text: '(GMT+11:00) Moscow+08 - Magadan', value: 'Asia/Magadan' },
-        { text: '(GMT+11:00) Norfolk', value: 'Pacific/Norfolk' },
-        { text: '(GMT+11:00) Noumea', value: 'Pacific/Noumea' },
-        { text: '(GMT+11:00) Ponape', value: 'Pacific/Pohnpei' },
-        { text: '(GMT+12:00) Funafuti', value: 'Pacific/Funafuti' },
-        { text: '(GMT+12:00) Kwajalein', value: 'Pacific/Kwajalein' },
-        { text: '(GMT+12:00) Majuro', value: 'Pacific/Majuro' },
-        { text: '(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy', value: 'Asia/Kamchatka' },
-        { text: '(GMT+12:00) Nauru', value: 'Pacific/Nauru' },
-        { text: '(GMT+12:00) Tarawa', value: 'Pacific/Tarawa' },
-        { text: '(GMT+12:00) Wake', value: 'Pacific/Wake' },
-        { text: '(GMT+12:00) Wallis', value: 'Pacific/Wallis' },
-        { text: '(GMT+13:00) Auckland', value: 'Pacific/Auckland' },
-        { text: '(GMT+13:00) Enderbury', value: 'Pacific/Enderbury' },
-        { text: '(GMT+13:00) Fakaofo', value: 'Pacific/Fakaofo' },
-        { text: '(GMT+13:00) Fiji', value: 'Pacific/Fiji' },
-        { text: '(GMT+13:00) Tongatapu', value: 'Pacific/Tongatapu' },
-        { text: '(GMT+14:00) Apia', value: 'Pacific/Apia' },
-        { text: '(GMT+14:00) Kiritimati', value: 'Pacific/Kiritimati' }
-      ]
-    }
-  },
-  computed: {
-    currentUserId: get('user/id')
-  },
-  methods: {
-    /**
-     * Activate a user (if previously deactivated)
-     */
-    async activateUser () {
-      this.$store.commit(`loadingStart`, 'admin-users-activate')
-      const resp = await this.$apollo.mutate({
-        mutation: gql`
-          mutation ($id: Int!) {
-            users {
-              activate(id: $id) {
-                responseResult {
-                  succeeded
-                  errorCode
-                  slug
-                  message
-                }
-              }
-            }
-          }
-        `,
-        variables: {
-          id: this.user.id
-        }
-      })
-      if (_.get(resp, 'data.users.activate.responseResult.succeeded', false)) {
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:users.userActivateSuccess'),
-          icon: 'check'
-        })
-        this.user.isActive = true
-      } else {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: _.get(resp, 'data.users.activate.responseResult.message', 'An unexpected error occurred.'),
-          icon: 'warning'
-        })
-      }
-      this.$store.commit(`loadingStop`, 'admin-users-activate')
-    },
-    /**
-     * Deactivate a currently active user
-     */
-    async deactivateUser () {
-      this.$store.commit(`loadingStart`, 'admin-users-deactivate')
-      const resp = await this.$apollo.mutate({
-        mutation: gql`
-          mutation ($id: Int!) {
-            users {
-              deactivate(id: $id) {
-                responseResult {
-                  succeeded
-                  errorCode
-                  slug
-                  message
-                }
-              }
-            }
-          }
-        `,
-        variables: {
-          id: this.user.id
-        }
-      })
-      if (_.get(resp, 'data.users.deactivate.responseResult.succeeded', false)) {
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:users.userDeactivateSuccess'),
-          icon: 'check'
-        })
-        this.user.isActive = false
-      } else {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: _.get(resp, 'data.users.deactivate.responseResult.message', 'An unexpected error occurred.'),
-          icon: 'warning'
-        })
-      }
-      this.$store.commit(`loadingStop`, 'admin-users-deactivate')
-    },
-    /**
-     * Delete a user
-     */
-    deleteUserConfirm () {
-      this.deleteUserDialog = true
-      this.deleteReplaceUser = {
-        id: this.currentUserId,
-        name: this.$store.get('user/name'),
-        email: this.$store.get('user/email')
-      }
-    },
-    async deleteUser () {
-      this.$store.commit(`loadingStart`, 'admin-users-delete')
-      const resp = await this.$apollo.mutate({
-        mutation: gql`
-          mutation ($id: Int!, $replaceId: Int!) {
-            users {
-              delete(id: $id, replaceId: $replaceId) {
-                responseResult {
-                  succeeded
-                  errorCode
-                  slug
-                  message
-                }
-              }
-            }
-          }
-        `,
-        variables: {
-          id: this.user.id,
-          replaceId: this.deleteReplaceUser.id
-        }
-      })
-      if (_.get(resp, 'data.users.delete.responseResult.succeeded', false)) {
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:users.userDeleteSuccess'),
-          icon: 'check'
-        })
-        this.$router.push('/users')
-      } else {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: _.get(resp, 'data.users.delete.responseResult.message', 'An unexpected error occurred.'),
-          icon: 'warning'
-        })
-      }
-      this.deleteUserDialog = false
-      this.$store.commit(`loadingStop`, 'admin-users-delete')
-    },
-    assignDeleteUser (selUsr) {
-      if (selUsr.id === this.user.id) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: 'You cannot select the account you\'re about to delete!',
-          icon: 'warning'
-        })
-      } else if (selUsr.id === 2) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: 'You cannot use the guest account for this operation.',
-          icon: 'warning'
-        })
-      } else {
-        this.deleteReplaceUser = selUsr
-      }
-    },
-    /**
-     * Update a user
-     */
-    async updateUser() {
-      this.$store.commit(`loadingStart`, 'admin-users-update')
-      const resp = await this.$apollo.mutate({
-        mutation: gql`
-          mutation ($id: Int!, $email: String, $name: String, $newPassword: String, $groups: [Int], $location: String, $jobTitle: String, $timezone: String) {
-            users {
-              update(id: $id, email: $email, name: $name, newPassword: $newPassword, groups: $groups, location: $location, jobTitle: $jobTitle, timezone: $timezone) {
-                responseResult {
-                  succeeded
-                  errorCode
-                  slug
-                  message
-                }
-              }
-            }
-          }
-        `,
-        variables: {
-          id: this.user.id,
-          email: this.user.email,
-          name: this.user.name,
-          newPassword: this.newPassword,
-          groups: _.map(this.user.groups, 'id'),
-          location: this.user.location,
-          jobTitle: this.user.jobTitle,
-          timezone: this.user.timezone
-        }
-      })
-      this.newPassword = ''
-      if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) {
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:users.userUpdateSuccess'),
-          icon: 'check'
-        })
-        this.$router.push('/users')
-      } else {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: _.get(resp, 'data.users.update.responseResult.message', 'An unexpected error occurred.'),
-          icon: 'warning'
-        })
-      }
-      this.$store.commit(`loadingStop`, 'admin-users-update')
-    },
-    /**
-     * Focus an input after delay
-     */
-    focusField (ipt) {
-      this.$nextTick(() => {
-        _.delay(() => {
-          this.$refs[ipt].focus()
-        }, 200)
-      })
-    },
-    /**
-     * Assign group to user
-     */
-    assignGroup() {
-      if (_.some(this.user.groups, ['id', this.newGroup])) {
-        this.$store.commit('showNotification', {
-          message: this.$t('admin:users.userAlreadyAssignedToGroup'),
-          style: 'error',
-          icon: 'alert'
-        })
-      } else {
-        this.user.groups.push(_.find(this.groups, ['id', this.newGroup]))
-        this.newGroup = 0
-      }
-    },
-    /**
-     * Unassign group from user
-     */
-    unassignGroup(gid) {
-      this.user.groups = _.reject(this.user.groups, ['id', gid])
-    },
-    /**
-     * Manually set user as verified
-     */
-    async verifyUser () {
-      this.$store.commit(`loadingStart`, 'admin-users-verify')
-      const resp = await this.$apollo.mutate({
-        mutation: gql`
-          mutation ($id: Int!) {
-            users {
-              verify(id: $id) {
-                responseResult {
-                  succeeded
-                  errorCode
-                  slug
-                  message
-                }
-              }
-            }
-          }
-        `,
-        variables: {
-          id: this.user.id
-        }
-      })
-      if (_.get(resp, 'data.users.verify.responseResult.succeeded', false)) {
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: this.$t('admin:users.userVerifySuccess'),
-          icon: 'check'
-        })
-        this.user.isVerified = true
-      } else {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: _.get(resp, 'data.users.verify.responseResult.message', 'An unexpected error occurred.'),
-          icon: 'warning'
-        })
-      }
-      this.$store.commit(`loadingStop`, 'admin-users-verify')
-    },
-    /**
-     * Toggle 2FA State
-     */
-    async toggle2FA () {
-      this.$store.commit(`loadingStart`, 'admin-users-toggle2fa')
-      if (this.user.tfaIsActive) {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($id: Int!) {
-              users {
-                disableTFA(id: $id) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            id: this.user.id
-          }
-        })
-        if (_.get(resp, 'data.users.disableTFA.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.$t('admin:users.userTFADisableSuccess'),
-            icon: 'check'
-          })
-          this.user.tfaIsActive = false
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _.get(resp, 'data.users.disableTFA.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'warning'
-          })
-        }
-      } else {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($id: Int!) {
-              users {
-                enableTFA(id: $id) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            id: this.user.id
-          }
-        })
-        if (_.get(resp, 'data.users.enableTFA.responseResult.succeeded', false)) {
-          this.$store.commit('showNotification', {
-            style: 'success',
-            message: this.$t('admin:users.userTFAEnableSuccess'),
-            icon: 'check'
-          })
-          this.user.tfaIsActive = true
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: _.get(resp, 'data.users.enableTFA.responseResult.message', 'An unexpected error occurred.'),
-            icon: 'warning'
-          })
-        }
-      }
-      this.$store.commit(`loadingStop`, 'admin-users-toggle2fa')
-    }
-  },
-  apollo: {
-    user: {
-      query: gql`
-        query ($id: Int!) {
-          users {
-            single(id: $id) {
-              id
-              name
-              email
-              providerKey
-              providerName
-              providerId
-              providerIs2FACapable
-              location
-              jobTitle
-              timezone
-              isSystem
-              isActive
-              isVerified
-              createdAt
-              updatedAt
-              lastLoginAt
-              tfaIsActive
-              groups {
-                id
-                name
-              }
-            }
-          }
-        }
-      `,
-      variables() {
-        return {
-          id: _.toSafeInteger(this.$route.params.id)
-        }
-      },
-      fetchPolicy: 'network-only',
-      update: (data) => data.users.single,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh')
-      }
-    },
-    groups: {
-      query: groupsQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => data.groups.list,
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 192
client/components/admin/admin-users.vue

@@ -1,192 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-customer.svg', alt='Users', style='width: 80px;')
-          .admin-header-title
-            .headline.blue--text.text--darken-2.animated.fadeInLeft Users
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s Manage users
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p2s.mr-3(outlined, color='grey', icon, @click='refresh')
-            v-icon mdi-refresh
-          v-btn.animated.fadeInDown(color='primary', large, depressed, @click='createUser')
-            v-icon(left) mdi-plus
-            span New User
-        v-card.mt-3.animated.fadeInUp
-          .pa-2.d-flex.align-center(:class='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-3`')
-            v-text-field(
-              solo
-              flat
-              v-model='search'
-              prepend-inner-icon='mdi-account-search-outline'
-              label='Search Users...'
-              hide-details
-              style='max-width: 400px;'
-              dense
-              )
-            v-spacer
-            v-select(
-              solo
-              flat
-              hide-details
-              label='Identity Provider'
-              :items='strategies'
-              v-model='filterStrategy'
-              item-text='displayName'
-              item-value='key'
-              style='max-width: 300px;'
-              dense
-            )
-          v-divider
-          v-data-table(
-            v-model='selected'
-            :items='usersFiltered',
-            :headers='headers',
-            :search='search',
-            :page.sync='pagination'
-            :items-per-page='15'
-            :loading='loading'
-            @page-count='pageCount = $event'
-            hide-default-footer
-            )
-            template(slot='item', slot-scope='props')
-              tr.is-clickable(:active='props.selected', @click='$router.push("/users/" + props.item.id)')
-                //- td
-                  v-checkbox(hide-details, :input-value='props.selected', color='blue darken-2', @click='props.selected = !props.selected')
-                td {{ props.item.id }}
-                td: strong {{ props.item.name }}
-                td {{ props.item.email }}
-                td {{ getStrategyName(props.item.providerKey) }}
-                td {{ props.item.createdAt | moment('from') }}
-                td
-                  span(v-if='props.item.lastLoginAt') {{ props.item.lastLoginAt | moment('from') }}
-                  em.grey--text(v-else) Never
-                td.text-right
-                  v-icon.mr-3(v-if='props.item.isSystem') mdi-lock-outline
-                  status-indicator(positive, pulse, v-if='props.item.isActive')
-                  status-indicator(negative, pulse, v-else)
-            template(slot='no-data')
-              .pa-3
-                v-alert.text-left(icon='mdi-alert', outlined, color='grey')
-                  em.body-2 No users to display!
-          v-card-chin(v-if='pageCount > 1')
-            v-spacer
-            v-pagination(v-model='pagination', :length='pageCount')
-            v-spacer
-
-    user-create(v-model='isCreateDialogShown', @refresh='refresh(false)')
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-
-import { StatusIndicator } from 'vue-status-indicator'
-import UserCreate from './admin-users-create.vue'
-
-export default {
-  components: {
-    StatusIndicator,
-    UserCreate
-  },
-  data() {
-    return {
-      selected: [],
-      pagination: 1,
-      pageCount: 0,
-      users: [],
-      headers: [
-        { text: 'ID', value: 'id', width: 80, sortable: true },
-        { text: 'Name', value: 'name', sortable: true },
-        { text: 'Email', value: 'email', sortable: true },
-        { text: 'Provider', value: 'provider', sortable: true },
-        { text: 'Created', value: 'createdAt', sortable: true },
-        { text: 'Last Login', value: 'lastLoginAt', sortable: true },
-        { text: '', value: 'actions', sortable: false, width: 80 }
-      ],
-      strategies: [],
-      filterStrategy: 'all',
-      search: '',
-      loading: false,
-      isCreateDialogShown: false
-    }
-  },
-  computed: {
-    usersFiltered () {
-      const all = this.filterStrategy === 'all' || this.filterStrategy === ''
-      return _.filter(this.users, u => all || u.providerKey === this.filterStrategy)
-    }
-  },
-  methods: {
-    createUser() {
-      this.isCreateDialogShown = true
-    },
-    async refresh(notify = true) {
-      await this.$apollo.queries.users.refetch()
-      if (notify) {
-        this.$store.commit('showNotification', {
-          message: 'Users list has been refreshed.',
-          style: 'success',
-          icon: 'cached'
-        })
-      }
-    },
-    getStrategyName(key) {
-      return (_.find(this.strategies, ['key', key]) || {}).displayName || key
-    }
-  },
-  apollo: {
-    users: {
-      query: gql`
-        query {
-          users {
-            list {
-              id
-              name
-              email
-              providerKey
-              isSystem
-              isActive
-              createdAt
-              lastLoginAt
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => data.users.list,
-      watchLoading (isLoading) {
-        this.loading = isLoading
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-refresh')
-      }
-    },
-    strategies: {
-      query: gql`
-        query {
-          authentication {
-            activeStrategies {
-              key
-              displayName
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => {
-        return _.concat({
-          key: 'all',
-          displayName: 'All Providers'
-        }, data.authentication.activeStrategies)
-      },
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-users-strategies-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 93
client/components/admin/admin-utilities-auth.vue

@@ -1,93 +0,0 @@
-<template lang='pug'>
-  v-card
-    v-toolbar(flat, color='primary', dark, dense)
-      .subtitle-1 {{ $t('admin:utilities.authTitle') }}
-    v-card-text
-      .subtitle-1.pb-3.primary--text Generate New Authentication Public / Private Key Certificates
-      .body-2 This will invalidate all current session tokens and cause all users to be logged out.
-      .body-2.red--text You will need to log back in after the operation.
-      v-btn(outlined, color='primary', @click='regenCerts', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-      v-divider.my-5
-      .subtitle-1.pb-3.primary--text Reset Guest User
-      .body-2 This will reset the guest user to its default parameters and permissions.
-      v-btn(outlined, color='primary', @click='resetGuest', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-</template>
-
-<script>
-import _ from 'lodash'
-import Cookies from 'js-cookie'
-import utilityAuthRegencertsMutation from 'gql/admin/utilities/utilities-mutation-auth-regencerts.gql'
-import utilityAuthResetguestMutation from 'gql/admin/utilities/utilities-mutation-auth-resetguest.gql'
-
-export default {
-  data: () => {
-    return {
-      loading: false
-    }
-  },
-  methods: {
-    async regenCerts() {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-auth-regencerts')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityAuthRegencertsMutation
-        })
-        const resp = _.get(respRaw, 'data.authentication.regenerateCertificates.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: 'New Certificates generated successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-          Cookies.remove('jwt')
-          _.delay(() => {
-            window.location.assign('/login')
-          }, 1000)
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-auth-regencerts')
-      this.loading = false
-    },
-    async resetGuest() {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-auth-resetguest')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityAuthResetguestMutation
-        })
-        const resp = _.get(respRaw, 'data.authentication.resetGuestUser.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: 'Guest user was reset successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-auth-resetguest')
-      this.loading = false
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 108
client/components/admin/admin-utilities-cache.vue

@@ -1,108 +0,0 @@
-<template lang='pug'>
-  v-card
-    v-toolbar(flat, color='primary', dark, dense)
-      .subtitle-1 {{ $t('admin:utilities.cacheTitle') }}
-    v-card-text
-      .subtitle-1.pb-3.primary--text Flush Pages and Assets Cache
-      .body-2 Pages and Assets are cached to disk for better performance. You can flush the cache to force all content to be fetched from the DB again.
-      v-btn(outlined, color='primary', @click='flushCache', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-      v-divider.my-5
-      .subtitle-1.pb-3.primary--text Flush Temporary Uploads
-      .body-2 New uploads are temporarily saved to disk while they are being processed. They are automatically deleted after processing, but you can force an immediate cleanup using this tool.
-      .body-2.red--text Note that performing this action while an upload is in progress can result in a failed upload.
-      v-btn(outlined, color='primary', @click='flushUploads', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-      v-divider.my-5
-      .subtitle-1.pb-3.primary--text Flush Client-Side Locale Cache
-      .body-2 Locale strings are cached in the browser local storage for 24h. You can delete your current cache in order to fetch the latest data during the next page load.
-      .body-2 Note that this affects only #[strong your own browser] and not everyone.
-      v-btn(outlined, color='primary', @click='flushClientLocaleCache', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-</template>
-
-<script>
-import _ from 'lodash'
-import utilityCacheFlushCacheMutation from 'gql/admin/utilities/utilities-mutation-cache-flushcache.gql'
-import utilityCacheFlushUploadsMutation from 'gql/admin/utilities/utilities-mutation-cache-flushuploads.gql'
-
-export default {
-  data() {
-    return {
-      loading: false
-    }
-  },
-  methods: {
-    async flushCache() {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-cache-flushCache')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityCacheFlushCacheMutation
-        })
-        const resp = _.get(respRaw, 'data.pages.flushCache.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: 'Cache flushed successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-cache-flushCache')
-      this.loading = false
-    },
-    async flushUploads() {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-cache-flushUploads')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityCacheFlushUploadsMutation
-        })
-        const resp = _.get(respRaw, 'data.assets.flushTempUploads.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: 'Temporary Uploads flushed successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-cache-flushUploads')
-      this.loading = false
-    },
-    async flushClientLocaleCache () {
-      for (let i = 0; i < window.localStorage.length; i++) {
-        const lsKey = window.localStorage.key(i)
-        if (_.startsWith(lsKey, 'i18next_res')) {
-          window.localStorage.removeItem(lsKey)
-        }
-      }
-      this.$store.commit('showNotification', {
-        message: 'Locale Client-Side Cache flushed successfully.',
-        style: 'success',
-        icon: 'check'
-      })
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 318
client/components/admin/admin-utilities-content.vue

@@ -1,318 +0,0 @@
-<template lang='pug'>
-  v-card
-    v-toolbar(flat, color='primary', dark, dense)
-      .subtitle-1 {{ $t('admin:utilities.contentTitle') }}
-    v-card-text
-      .subtitle-1.pb-3.primary--text Rebuild Page Tree
-      .body-2 The virtual structure of your wiki is automatically inferred from all page paths. You can trigger a full rebuild of the tree if some virtual folders are missing or not valid anymore.
-      v-btn(outlined, color='primary', @click='rebuildTree', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-
-      v-divider.my-5
-
-      .subtitle-1.pb-3.primary--text Rerender All Pages
-      .body-2 All pages will be rendered again. Useful if internal links are broken or the rendering pipeline has changed.
-      v-btn(outlined, color='primary', @click='rerenderPages', :disabled='loading', :loading='isRerendering').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-      v-dialog(
-        v-model='isRerendering'
-        persistent
-        max-width='450'
-        )
-        v-card(color='blue darken-2', dark)
-          v-card-text.pa-10.text-center
-            semipolar-spinner.animated.fadeIn(
-              :animation-duration='1500'
-              :size='65'
-              color='#FFF'
-              style='margin: 0 auto;'
-            )
-            .mt-5.body-1.white--text Rendering all pages...
-            .caption(v-if='renderIndex > 0') Rendering {{renderCurrentPath}}... ({{renderIndex}}/{{renderTotal}}, {{renderProgress}}%)
-            .caption.mt-4 Do not leave this page.
-            v-progress-linear.mt-5(
-              color='white'
-              :value='renderProgress'
-              stream
-              rounded
-              :buffer-value='0'
-            )
-
-      v-divider.my-5
-
-      .subtitle-1.pb-3.pl-0.primary--text Migrate all pages to target locale
-      .body-2 If you created content before selecting a different locale and activating the namespacing capabilities, you may want to transfer all content to the base locale.
-      .body-2.red--text: strong This operation is destructive and cannot be reversed! Make sure you have proper backups!
-      v-toolbar.radius-7.mt-5(flat, :color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', height='80')
-        v-select(
-          label='Source Locale'
-          outlined
-          hide-details
-          :items='locales'
-          item-text='name'
-          item-value='code'
-          v-model='sourceLocale'
-        )
-        v-icon.mx-3(large) mdi-chevron-right-box-outline
-        v-select(
-          label='Target Locale'
-          outlined
-          hide-details
-          :items='locales'
-          item-text='name'
-          item-value='code'
-          v-model='targetLocale'
-        )
-      .body-2.mt-5 Pages that are already in the target locale will not be touched. If a page already exists at the target, the source page will not be modified as it would create a conflict. If you want to overwrite the target page, you must first delete it.
-      v-btn(outlined, color='primary', @click='migrateToLocale', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-
-      v-divider.my-5
-
-      .subtitle-1.pb-3.pl-0.primary--text Purge Page History
-      .body-2 You may want to purge old history for pages to reduce database usage.
-      .body-2 This operation only affects the database and not any history saved by a storage module (e.g. git version history)
-      v-toolbar.radius-7.mt-5(flat, :color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-4`', height='80')
-        v-select(
-          label='Delete history older than...'
-          outlined
-          hide-details
-          :items='purgeHistoryOptions'
-          item-text='title'
-          item-value='key'
-          v-model='purgeHistorySelection'
-        )
-      v-btn(outlined, color='primary', @click='purgeHistory', :disabled='loading').ml-0.mt-3
-        v-icon(left) mdi-gesture-double-tap
-        span Proceed
-</template>
-
-<script>
-import _ from 'lodash'
-import gql from 'graphql-tag'
-import utilityContentMigrateLocaleMutation from 'gql/admin/utilities/utilities-mutation-content-migratelocale.gql'
-import utilityContentRebuildTreeMutation from 'gql/admin/utilities/utilities-mutation-content-rebuildtree.gql'
-
-import { SemipolarSpinner } from 'epic-spinners'
-
-/* global siteLangs, siteConfig */
-
-export default {
-  components: {
-    SemipolarSpinner
-  },
-  data: () => {
-    return {
-      isRerendering: false,
-      loading: false,
-      renderProgress: 0,
-      renderIndex: 0,
-      renderTotal: 0,
-      renderCurrentPath: '',
-      sourceLocale: '',
-      targetLocale: '',
-      purgeHistorySelection: 'P1Y',
-      purgeHistoryOptions: [
-        { key: 'P1D', title: 'Today' },
-        { key: 'P1M', title: '1 month' },
-        { key: 'P3M', title: '3 months' },
-        { key: 'P6M', title: '6 months' },
-        { key: 'P1Y', title: '1 year' },
-        { key: 'P2Y', title: '2 years' },
-        { key: 'P3Y', title: '3 years' },
-        { key: 'P5Y', title: '5 years' }
-      ]
-    }
-  },
-  computed: {
-    currentLocale () {
-      return siteConfig.lang
-    },
-    locales () {
-      return siteLangs
-    }
-  },
-  methods: {
-    async rebuildTree () {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-content-rebuildtree')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityContentRebuildTreeMutation
-        })
-        const resp = _.get(respRaw, 'data.pages.rebuildTree.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: 'Page Tree rebuilt successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-content-rebuildtree')
-      this.loading = false
-    },
-    async rerenderPages () {
-      this.loading = true
-      this.isRerendering = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-content-rerender')
-
-      try {
-        const pagesRaw = await this.$apollo.query({
-          query: gql`
-            {
-              pages {
-                list {
-                  id
-                  path
-                  locale
-                }
-              }
-            }
-          `,
-          fetchPolicy: 'network-only'
-        })
-        if (_.get(pagesRaw, 'data.pages.list', []).length < 1) {
-          throw new Error('Could not find any page to render!')
-        }
-
-        this.renderIndex = 0
-        this.renderTotal = pagesRaw.data.pages.list.length
-        let failed = 0
-        for (const page of pagesRaw.data.pages.list) {
-          this.renderCurrentPath = `${page.locale}/${page.path}`
-          this.renderIndex++
-          this.renderProgress = Math.round(this.renderIndex / this.renderTotal * 100)
-          const respRaw = await this.$apollo.mutate({
-            mutation: gql`
-              mutation($id: Int!) {
-                pages {
-                  render(id: $id) {
-                    responseResult {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                  }
-                }
-              }
-            `,
-            variables: {
-              id: page.id
-            }
-          })
-          const resp = _.get(respRaw, 'data.pages.render.responseResult', {})
-          if (!resp.succeeded) {
-            failed++
-          }
-        }
-        if (failed > 0) {
-          this.$store.commit('showNotification', {
-            message: `Completed with ${failed} pages that failed to render. Check server logs for details.`,
-            style: 'error',
-            icon: 'alert'
-          })
-        } else {
-          this.$store.commit('showNotification', {
-            message: 'All pages have been rendered successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-content-rerender')
-      this.isRerendering = false
-      this.loading = false
-    },
-    async migrateToLocale () {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-content-migratelocale')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityContentMigrateLocaleMutation,
-          variables: {
-            sourceLocale: this.sourceLocale,
-            targetLocale: this.targetLocale
-          }
-        })
-        const resp = _.get(respRaw, 'data.pages.migrateToLocale.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: `Migrated ${_.get(respRaw, 'data.pages.migrateToLocale.count', 0)} page(s) to target locale successfully.`,
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-content-migratelocale')
-      this.loading = false
-    },
-    async purgeHistory () {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-content-purgehistory')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($olderThan: String!) {
-              pages {
-                purgeHistory (
-                  olderThan: $olderThan
-                ) {
-                  responseResult {
-                    errorCode
-                    message
-                    slug
-                    succeeded
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            olderThan: this.purgeHistorySelection
-          }
-        })
-        const resp = _.get(respRaw, 'data.pages.purgeHistory.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: `Purged history successfully.`,
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-content-purgehistory')
-      this.loading = false
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 507
client/components/admin/admin-utilities-importv1.vue

@@ -1,507 +0,0 @@
-<template lang='pug'>
-  v-card
-    v-toolbar(flat, color='primary', dark, dense)
-      .subtitle-1 {{ $t('admin:utilities.importv1Title') }}
-    v-card-text
-      .text-center
-        img.animated.fadeInUp.wait-p1s(src='/_assets/svg/icon-software.svg')
-        .body-2 Import from Wiki.js 1.x
-      v-divider.my-4
-      .body-2 Data from a Wiki.js 1.x installation can easily be imported using this tool. What do you want to import?
-      v-checkbox(
-        label='Content + Uploads'
-        value='content'
-        color='deep-orange darken-2'
-        v-model='importFilters'
-        hide-details
-        )
-        template(v-slot:label)
-          strong.deep-orange--text.text--darken-2 Content + Uploads
-      .pl-8(v-if='wantContent')
-        v-radio-group(v-model='contentMode', hide-details)
-          v-radio(
-            value='git'
-            color='primary'
-            )
-            template(v-slot:label)
-              div
-                span Import from Git Connection
-                .caption: em #[strong.primary--text Recommended] | The Git storage module will also be configured for you.
-        .pl-8.mt-5(v-if='needGit')
-          v-row
-            v-col(cols='8')
-              v-select(
-                label='Authentication Mode'
-                :items='gitAuthModes'
-                v-model='gitAuthMode'
-                outlined
-                hide-details
-              )
-            v-col(cols='4')
-              v-switch(
-                label='Verify SSL Certificate'
-                v-model='gitVerifySSL'
-                hide-details
-                color='primary'
-              )
-            v-col(cols='8')
-              v-text-field(
-                outlined
-                label='Repository URL'
-                :placeholder='(gitAuthMode === `ssh`) ? `e.g. git@github.com:orgname/repo.git` : `e.g. https://github.com/orgname/repo.git`'
-                hide-details
-                v-model='gitRepoUrl'
-              )
-            v-col(cols='4')
-              v-text-field(
-                label='Branch'
-                placeholder='e.g. master'
-                v-model='gitRepoBranch'
-                outlined
-                hide-details
-              )
-            v-col(v-if='gitAuthMode === `ssh`', cols='12')
-              v-textarea(
-                outlined
-                label='Private Key Contents'
-                placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----'
-                hide-details
-                v-model='gitPrivKey'
-              )
-            template(v-else-if='gitAuthMode === `basic`')
-              v-col(cols='6')
-                v-text-field(
-                  label='Username'
-                  v-model='gitUsername'
-                  outlined
-                  hide-details
-                )
-              v-col(cols='6')
-                v-text-field(
-                  type='password'
-                  label='Password / PAT'
-                  v-model='gitPassword'
-                  outlined
-                  hide-details
-                )
-            v-col(cols='6')
-              v-text-field(
-                label='Default Author Email'
-                placeholder='e.g. name@company.com'
-                v-model='gitUserEmail'
-                outlined
-                hide-details
-              )
-            v-col(cols='6')
-              v-text-field(
-                label='Default Author Name'
-                placeholder='e.g. John Smith'
-                v-model='gitUserName'
-                outlined
-                hide-details
-              )
-            v-col(cols='12')
-              v-text-field(
-                label='Local Repository Path'
-                placeholder='e.g. ./data/repo'
-                v-model='gitRepoPath'
-                outlined
-                hide-details
-              )
-              .caption.mt-2 This folder should be empty or not exist yet. #[strong.deep-orange--text.text--darken-2 DO NOT] point to your existing Wiki.js 1.x repository folder. In most cases, it should be left to the default value.
-          v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent)
-            .body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above.
-            .body-2 - Although both v1 and v2 installations can use the same remote git repository, you shouldn't make edits to the same pages simultaneously.
-        v-radio-group(v-model='contentMode', hide-details)
-          v-divider
-          v-radio.mt-3(
-            value='disk'
-            color='primary'
-            )
-            template(v-slot:label)
-              div
-                span Import from local folder
-                .caption: em Choose this option only if you didn't have git configured in your Wiki.js 1.x installation.
-        .pl-8.mt-5(v-if='needDisk')
-          v-text-field(
-            outlined
-            label='Content Repo Path'
-            hint='The absolute path to where the Wiki.js 1.x content is stored on disk.'
-            persistent-hint
-            v-model='contentPath'
-          )
-
-      v-checkbox(
-        label='Users'
-        value='users'
-        color='deep-orange darken-2'
-        v-model='importFilters'
-        hide-details
-        )
-        template(v-slot:label)
-          strong.deep-orange--text.text--darken-2 Users
-      .pl-8.mt-5(v-if='wantUsers')
-        v-text-field(
-          outlined
-          label='MongoDB Connection String'
-          hint='The connection string to connect to the Wiki.js 1.x MongoDB database.'
-          persistent-hint
-          v-model='dbConnStr'
-        )
-        v-radio-group(v-model='groupMode', hide-details, mandatory)
-          v-radio(
-            value='MULTI'
-            color='primary'
-            )
-            template(v-slot:label)
-              div
-                span Create groups for each unique user permissions configuration
-                .caption: em #[strong.primary--text Recommended] | Users having identical permission sets will be assigned to the same group. Note that this can potentially result in a large amount of groups being created.
-          v-divider
-          v-radio.mt-3(
-            value='SINGLE'
-            color='primary'
-            )
-            template(v-slot:label)
-              div
-                span Create a single group with all imported users
-                .caption: em The new group will have read permissions enabled by default.
-          v-divider
-          v-radio.mt-3(
-            value='NONE'
-            color='primary'
-            )
-            template(v-slot:label)
-              div
-                span Don't create any group
-                .caption: em Users will not be able to access your wiki until they are assigned to a group.
-
-        v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent)
-          .body-2 Note that any user that already exists in this installation will not be imported. A list of skipped users will be displayed upon completion.
-          .caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation.
-
-    v-card-chin
-      v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0
-        v-icon(left, color='white') mdi-database-import
-        span.white--text Start Import
-    v-dialog(
-      v-model='isLoading'
-      persistent
-      max-width='350'
-      )
-      v-card(color='deep-orange darken-2', dark)
-        v-card-text.pa-10.text-center
-          semipolar-spinner.animated.fadeIn(
-            :animation-duration='1500'
-            :size='65'
-            color='#FFF'
-            style='margin: 0 auto;'
-          )
-          .mt-5.body-1.white--text Importing from Wiki.js 1.x...
-          .caption Please wait
-          v-progress-linear.mt-5(
-            color='white'
-            :value='progress'
-            stream
-            rounded
-            :buffer-value='0'
-          )
-    v-dialog(
-      v-model='isSuccess'
-      persistent
-      max-width='350'
-      )
-      v-card(color='green darken-2', dark)
-        v-card-text.pa-10.text-center
-          v-icon(size='60') mdi-check-circle-outline
-          .my-5.body-1.white--text Import completed
-          template(v-if='wantUsers')
-            .body-2
-              span #[strong {{successUsers}}] users imported
-              v-btn.text-none.ml-3(
-                v-if='failedUsers.length > 0'
-                text
-                color='white'
-                dark
-                @click='showFailedUsers = true'
-                )
-                v-icon(left) mdi-alert
-                span {{failedUsers.length}} failed
-            .body-2 #[strong {{successGroups}}] groups created
-        v-card-actions.green.darken-1
-          v-spacer
-          v-btn.px-5(
-            color='white'
-            outlined
-            @click='isSuccess = false'
-          ) Close
-          v-spacer
-    v-dialog(
-      v-model='showFailedUsers'
-      persistent
-      max-width='800'
-      )
-      v-card(color='red darken-2', dark)
-        v-toolbar(color='red darken-2', dense)
-          v-icon mdi-alert
-          .body-2.pl-3 Failed User Imports
-          v-spacer
-          v-btn.px-5(
-            color='white'
-            text
-            @click='showFailedUsers = false'
-            ) Close
-        v-simple-table(dense, fixed-header, height='300px')
-          template(v-slot:default)
-            thead
-              tr
-                th Provider
-                th Email
-                th Error
-            tbody
-              tr(v-for='(fusr, idx) in failedUsers', :key='`fusr-` + idx')
-                td {{fusr.provider}}
-                td {{fusr.email}}
-                td {{fusr.error}}
-</template>
-
-<script>
-import _ from 'lodash'
-
-import { SemipolarSpinner } from 'epic-spinners'
-
-import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql'
-import storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql'
-import storageStatusQuery from 'gql/admin/storage/storage-query-status.gql'
-import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql'
-import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql'
-
-export default {
-  components: {
-    SemipolarSpinner
-  },
-  data() {
-    return {
-      importFilters: ['content', 'users'],
-      groupMode: 'MULTI',
-      contentMode: 'git',
-      dbConnStr: 'mongodb://',
-      contentPath: '/wiki-v1/repo',
-      isLoading: false,
-      isSuccess: false,
-      gitAuthMode: 'ssh',
-      gitAuthModes: [
-        { text: 'SSH', value: 'ssh' },
-        { text: 'Basic', value: 'basic' }
-      ],
-      gitVerifySSL: true,
-      gitRepoUrl: '',
-      gitRepoBranch: 'master',
-      gitPrivKey: '',
-      gitUsername: '',
-      gitPassword: '',
-      gitUserEmail: '',
-      gitUserName: '',
-      gitRepoPath: './data/repo',
-      progress: 0,
-      successGroups: 0,
-      successUsers: 0,
-      successPages: 0,
-      showFailedUsers: false,
-      failedUsers: []
-    }
-  },
-  computed: {
-    wantContent () {
-      return this.importFilters.indexOf('content') >= 0
-    },
-    wantUsers () {
-      return this.importFilters.indexOf('users') >= 0
-    },
-    needDisk () {
-      return this.contentMode === `disk`
-    },
-    needGit () {
-      return this.contentMode === `git`
-    }
-  },
-  methods: {
-    async startImport () {
-      this.isLoading = true
-      this.progress = 0
-      this.failedUsers = []
-
-      _.delay(async () => {
-        // -> Import Users
-
-        if (this.wantUsers) {
-          try {
-            const resp = await this.$apollo.mutate({
-              mutation: utilityImportv1UsersMutation,
-              variables: {
-                mongoDbConnString: this.dbConnStr,
-                groupMode: this.groupMode
-              }
-            })
-            const respObj = _.get(resp, 'data.system.importUsersFromV1', {})
-            if (!_.get(respObj, 'responseResult.succeeded', false)) {
-              throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))
-            }
-            this.successUsers = _.get(respObj, 'usersCount', 0)
-            this.successGroups = _.get(respObj, 'groupsCount', 0)
-            this.failedUsers = _.get(respObj, 'failed', [])
-            this.progress += 50
-          } catch (err) {
-            this.$store.commit('pushGraphError', err)
-            this.isLoading = false
-            return
-          }
-        }
-
-        // -> Import Content
-
-        if (this.wantContent) {
-          try {
-            const resp = await this.$apollo.query({
-              query: storageTargetsQuery,
-              fetchPolicy: 'network-only'
-            })
-            if (_.has(resp, 'data.storage.targets')) {
-              this.progress += 10
-              let targets = resp.data.storage.targets.map(str => {
-                let nStr = {
-                  ...str,
-                  config: _.sortBy(str.config.map(cfg => ({
-                    ...cfg,
-                    value: JSON.parse(cfg.value)
-                  })), [t => t.value.order])
-                }
-
-                // -> Setup Git Module
-
-                if (this.contentMode === 'git' && nStr.key === 'git') {
-                  nStr.isEnabled = true
-                  nStr.mode = 'sync'
-                  nStr.syncInterval = 'PT5M'
-                  nStr.config = [
-                    { key: 'authType', value: { value: this.gitAuthMode } },
-                    { key: 'repoUrl', value: { value: this.gitRepoUrl } },
-                    { key: 'branch', value: { value: this.gitRepoBranch } },
-                    { key: 'sshPrivateKeyMode', value: { value: 'contents' } },
-                    { key: 'sshPrivateKeyPath', value: { value: '' } },
-                    { key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } },
-                    { key: 'verifySSL', value: { value: this.gitVerifySSL } },
-                    { key: 'basicUsername', value: { value: this.gitUsername } },
-                    { key: 'basicPassword', value: { value: this.gitPassword } },
-                    { key: 'defaultEmail', value: { value: this.gitUserEmail } },
-                    { key: 'defaultName', value: { value: this.gitUserName } },
-                    { key: 'localRepoPath', value: { value: this.gitRepoPath } },
-                    { key: 'gitBinaryPath', value: { value: '' } }
-                  ]
-                }
-
-                // -> Setup Disk Module
-                if (this.contentMode === 'disk' && nStr.key === 'disk') {
-                  nStr.isEnabled = true
-                  nStr.mode = 'push'
-                  nStr.syncInterval = 'P0D'
-                  nStr.config = [
-                    { key: 'path', value: { value: this.contentPath } },
-                    { key: 'createDailyBackups', value: { value: false } }
-                  ]
-                }
-                return nStr
-              })
-
-              // -> Save storage modules configuration
-
-              const respSv = await this.$apollo.mutate({
-                mutation: targetsSaveMutation,
-                variables: {
-                  targets: targets.map(tgt => _.pick(tgt, [
-                    'isEnabled',
-                    'key',
-                    'config',
-                    'mode',
-                    'syncInterval'
-                  ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
-                }
-              })
-              const respObj = _.get(respSv, 'data.storage.updateTargets', {})
-              if (!_.get(respObj, 'responseResult.succeeded', false)) {
-                throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occurred'))
-              }
-
-              this.progress += 10
-
-              // -> Wait for success sync
-
-              let statusAttempts = 0
-              while (statusAttempts < 10) {
-                statusAttempts++
-                const respStatus = await this.$apollo.query({
-                  query: storageStatusQuery,
-                  fetchPolicy: 'network-only'
-                })
-                if (_.has(respStatus, 'data.storage.status[0]')) {
-                  const st = _.find(respStatus.data.storage.status, ['key', this.contentMode])
-                  if (!st) {
-                    throw new Error('Storage target could not be configured.')
-                  }
-                  switch (st.status) {
-                    case 'pending':
-                      if (statusAttempts >= 10) {
-                        throw new Error('Storage target is stuck in pending state. Try again.')
-                      } else {
-                        continue
-                      }
-                    case 'operational':
-                      statusAttempts = 10
-                      break
-                    case 'error':
-                      throw new Error(st.message)
-                  }
-                } else {
-                  throw new Error('Failed to fetch storage sync status.')
-                }
-              }
-
-              this.progress += 15
-
-              // -> Perform import all
-
-              const respImport = await this.$apollo.mutate({
-                mutation: targetExecuteActionMutation,
-                variables: {
-                  targetKey: this.contentMode,
-                  handler: 'importAll'
-                }
-              })
-
-              const respImportObj = _.get(respImport, 'data.storage.executeAction', {})
-              if (!_.get(respImportObj, 'responseResult.succeeded', false)) {
-                throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occurred'))
-              }
-
-              this.progress += 15
-            } else {
-              throw new Error('Failed to fetch storage targets.')
-            }
-          } catch (err) {
-            this.$store.commit('pushGraphError', err)
-            this.isLoading = false
-            return
-          }
-        }
-
-        this.isLoading = false
-        this.isSuccess = true
-      }, 1500)
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 162
client/components/admin/admin-utilities-telemetry.vue

@@ -1,162 +0,0 @@
-<template lang='pug'>
-  v-card
-    v-toolbar(flat, color='primary', dark, dense)
-      .subtitle-1 {{ $t('admin:utilities.telemetryTitle') }}
-    v-form
-      v-card-text
-        .subtitle-2 What is telemetry?
-        .body-2.mt-3 Telemetry allows the developers of Wiki.js to improve the software by collecting basic anonymized data about its usage and the host info. #[br] This is entirely optional and #[strong absolutely no] private data (such as content or personal data) is collected.
-        .body-2.mt-3 For maximum privacy, a random client ID is generated during setup. This ID is used to group requests together while keeping complete anonymity. You can reset and generate a new one below at any time.
-        v-divider.my-4
-        .subtitle-2 What is collected?
-        .body-2.mt-3 When telemetry is enabled, only the following data is transmitted:
-        v-list
-          v-list-item
-            v-list-item-avatar: v-icon mdi-information-outline
-            v-list-item-content
-              v-list-item-title.body-2 Version of Wiki.js installed
-              v-list-item-subtitle.caption: em e.g. v2.0.123
-          v-list-item
-            v-list-item-avatar: v-icon mdi-information-outline
-            v-list-item-content
-              v-list-item-title.body-2 Basic OS information
-              v-list-item-subtitle.caption: em Platform (Linux, macOS or Windows), Total CPU cores and DB type (PostgreSQL, MySQL, MariaDB, SQLite or SQL Server)
-          v-list-item
-            v-list-item-avatar: v-icon mdi-information-outline
-            v-list-item-content
-              v-list-item-title.body-2 Crash debug data
-              v-list-item-subtitle.caption: em Stack trace of the error
-          v-list-item
-            v-list-item-avatar: v-icon mdi-information-outline
-            v-list-item-content
-              v-list-item-title.body-2 Setup analytics
-              v-list-item-subtitle.caption: em Installation checkpoint reached
-        .body-2 Note that crash debug data is stored for a maximum of 30 days while analytics are stored for a maximum of 16 months, after which it is permanently deleted.
-        v-divider.my-4
-        .subtitle-2 What is it used for?
-        .body-2.mt-3 Telemetry is used by developers to improve Wiki.js, mostly for the following reasons:
-        v-list(dense)
-          v-list-item
-            v-list-item-avatar: v-icon mdi-chevron-right
-            v-list-item-content: v-list-item-title: .body-2 Identify critical bugs more easily and fix them in a timely manner.
-          v-list-item
-            v-list-item-avatar: v-icon mdi-chevron-right
-            v-list-item-content: v-list-item-title: .body-2 Understand the upgrade rate of current installations.
-          v-list-item
-            v-list-item-avatar: v-icon mdi-chevron-right
-            v-list-item-content: v-list-item-title: .body-2  Optimize performance and testing scenarios based on most popular environments.
-        .body-2 Only authorized developers have access to the data. It is not shared to any 3rd party nor is it used for any other application than improving Wiki.js.
-        v-divider.my-4
-        .subtitle-2 Settings
-        .mt-3
-          v-switch.mt-0(
-            v-model='telemetry',
-            label='Enable Telemetry',
-            color='primary',
-            hint='Allow Wiki.js to transmit telemetry data.',
-            persistent-hint
-          )
-        v-divider.my-4
-        .subtitle-2.mt-3.grey--text.text--darken-1 Client ID
-        .body-2.mt-2 {{clientId}}
-      v-card-chin
-        v-btn.px-3(depressed, color='success', @click='updateTelemetry')
-          v-icon(left) mdi-chevron-right
-          | Save Changes
-        v-spacer
-        v-btn.px-3(outlined, color='grey', @click='resetClientId')
-          v-icon(left) mdi-autorenew
-          span Reset Client ID
-
-</template>
-
-<script>
-import _ from 'lodash'
-
-import utilityTelemetryResetIdMutation from 'gql/admin/utilities/utilities-mutation-telemetry-resetid.gql'
-import utilityTelemetrySetMutation from 'gql/admin/utilities/utilities-mutation-telemetry-set.gql'
-import utilityTelemetryQuery from 'gql/admin/utilities/utilities-query-telemetry.gql'
-
-export default {
-  data() {
-    return {
-      telemetry: false,
-      clientId: 'N/A'
-    }
-  },
-  methods: {
-    async updateTelemetry() {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-telemetry-set')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityTelemetrySetMutation,
-          variables: {
-            enabled: this.telemetry
-          }
-        })
-        const resp = _.get(respRaw, 'data.system.setTelemetry.responseResult', {})
-        if (resp.succeeded) {
-          this.$store.commit('showNotification', {
-            message: 'Telemetry updated successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-telemetry-set')
-      this.loading = false
-    },
-    async resetClientId() {
-      this.loading = true
-      this.$store.commit(`loadingStart`, 'admin-utilities-telemetry-resetid')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: utilityTelemetryResetIdMutation
-        })
-        const resp = _.get(respRaw, 'data.system.resetTelemetryClientId.responseResult', {})
-        if (resp.succeeded) {
-          this.$apollo.queries.telemetry.refetch()
-          this.$store.commit('showNotification', {
-            message: 'Telemetry Client ID reset successfully.',
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'admin-utilities-telemetry-resetid')
-      this.loading = false
-    }
-  },
-  apollo: {
-    telemetry: {
-      query: utilityTelemetryQuery,
-      fetchPolicy: 'network-only',
-      manual: true,
-      result ({ data }) {
-        this.telemetry = _.get(data, 'system.info.telemetry', false)
-        this.clientId = _.get(data, 'system.info.telemetryClientId', 'N/A')
-      },
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-utilities-telemetry-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 91
client/components/admin/admin-utilities.vue

@@ -1,91 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img(src='/_assets/svg/icon-maintenance.svg', alt='Utilities', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text {{$t('admin:utilities.title')}}
-            .subtitle-1.grey--text {{$t('admin:utilities.subtitle')}}
-
-      v-flex(lg3, xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(flat, color='primary', dark, dense)
-            .subtitle-1 {{$t('admin:utilities.tools')}}
-          v-list(two-line, dense).py-0
-            template(v-for='(tool, idx) in tools')
-              v-list-item(:key='tool.key', @click='selectedTool = tool.key', :disabled='!tool.isAvailable')
-                v-list-item-avatar
-                  v-icon(:color='!tool.isAvailable ? `grey lighten-1` : (selectedTool === tool.key ? `blue ` : `grey darken-1`)') {{ tool.icon }}
-                v-list-item-content
-                  v-list-item-title.body-2(:class='!tool.isAvailable ? `grey--text` : (selectedTool === tool.key ? `primary--text` : ``)') {{ $t('admin:utilities.' + tool.i18nKey + 'Title') }}
-                  v-list-item-subtitle: .caption(:class='!tool.isAvailable ? `grey--text text--lighten-1` : (selectedTool === tool.key ? `blue--text ` : ``)') {{ $t('admin:utilities.' + tool.i18nKey + 'Subtitle') }}
-                v-list-item-avatar(v-if='selectedTool === tool.key')
-                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
-              v-divider(v-if='idx < tools.length - 1')
-
-      v-flex.animated.fadeInUp.wait-p2s(xs12, lg9)
-        transition(name='admin-router')
-          component(:is='selectedTool')
-
-</template>
-
-<script>
-
-export default {
-  components: {
-    UtilityAuth: () => import(/* webpackChunkName: "admin" */ './admin-utilities-auth.vue'),
-    UtilityContent: () => import(/* webpackChunkName: "admin" */ './admin-utilities-content.vue'),
-    UtilityCache: () => import(/* webpackChunkName: "admin" */ './admin-utilities-cache.vue'),
-    UtilityImportv1: () => import(/* webpackChunkName: "admin" */ './admin-utilities-importv1.vue'),
-    UtilityTelemetry: () => import(/* webpackChunkName: "admin" */ './admin-utilities-telemetry.vue')
-  },
-  data() {
-    return {
-      selectedTool: 'UtilityAuth',
-      tools: [
-        {
-          key: 'UtilityAuth',
-          icon: 'mdi-lock-open-outline',
-          i18nKey: 'auth',
-          isAvailable: true
-        },
-        {
-          key: 'UtilityContent',
-          icon: 'mdi-content-duplicate',
-          i18nKey: 'content',
-          isAvailable: true
-        },
-        {
-          key: 'UtilityCache',
-          icon: 'mdi-database-refresh',
-          i18nKey: 'cache',
-          isAvailable: true
-        },
-        // {
-        //   key: 'UtilityGraphEndpoint',
-        //   icon: 'mdi-graphql',
-        //   i18nKey: 'graphEndpoint',
-        //   isAvailable: false
-        // },
-        {
-          key: 'UtilityImportv1',
-          icon: 'mdi-database-import',
-          i18nKey: 'importv1',
-          isAvailable: true
-        },
-        {
-          key: 'UtilityTelemetry',
-          icon: 'mdi-math-compass',
-          i18nKey: 'telemetry',
-          isAvailable: true
-        }
-      ]
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 116
client/components/admin/admin-webhooks.vue

@@ -1,116 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row, wrap)
-      v-flex(xs12)
-        .admin-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-winter.svg', alt='Mail', style='width: 80px;')
-          .admin-header-title
-            .headline.primary--text.animated.fadeInLeft {{ $t('admin:webhooks.title') }}
-            .subtitle-1.grey--text.animated.fadeInLeft.wait-p4s {{ $t('admin:webhooks.subtitle') }}
-          v-spacer
-          v-btn.animated.fadeInDown(color='success', depressed, @click='save', large, disabled)
-            v-icon(left) check
-            span {{$t('common:actions.apply')}}
-
-      v-flex(lg3, xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(flat, color='primary', dark, dense)
-            .subtitle-1 Webhooks
-            v-spacer
-            v-btn(outline, small)
-              v-icon.mr-2 add
-              span New
-          v-list(two-line, dense).py-0
-            template(v-for='(str, idx) in hooks')
-              v-list-item(:key='str.key', @click='selectedHook = str.key')
-                v-list-item-avatar
-                  v-icon(color='primary', v-if='str.isEnabled', v-ripple, @click='str.isEnabled = false') check_box
-                  v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') check_box_outline_blank
-                v-list-item-content
-                  v-list-item-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedHook === str.key ? `primary--text` : ``)') {{ str.title }}
-                  v-list-item-sub-title.caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedHook === str.key ? `blue--text ` : ``)') {{ str.description }}
-                v-list-item-avatar(v-if='selectedHook === str.key')
-                  v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
-              v-divider(v-if='idx < hooks.length - 1')
-
-      v-flex(xs12, lg9)
-        v-card.wiki-form.animated.fadeInUp.wait-p2s
-          v-toolbar(color='primary', dense, flat, dark)
-            .subtitle-1 {{hook.title}}
-          v-card-text
-            v-form
-              .authlogo
-                img(:src='hook.logo', :alt='hook.title')
-              .caption.pt-3 {{hook.description}}
-              .caption.pb-3: a(:href='hook.website') {{hook.website}}
-              .body-2(v-if='hook.isEnabled')
-                span This hook is
-
-</template>
-
-<script>
-import _ from 'lodash'
-// import { get } from 'vuex-pathify'
-import mailConfigQuery from 'gql/admin/mail/mail-query-config.gql'
-import mailUpdateConfigMutation from 'gql/admin/mail/mail-mutation-save-config.gql'
-
-export default {
-  data() {
-    return {
-      hooks: [],
-      selectedHook: ''
-    }
-  },
-  computed: {
-    hook() {
-      return _.find(this.hooks, ['id', this.selectedHook]) || {}
-    }
-  },
-  methods: {
-    async save () {
-      try {
-        await this.$apollo.mutate({
-          mutation: mailUpdateConfigMutation,
-          variables: {
-            senderName: this.config.senderName || '',
-            senderEmail: this.config.senderEmail || '',
-            host: this.config.host || '',
-            port: _.toSafeInteger(this.config.port) || 0,
-            secure: this.config.secure || false,
-            user: this.config.user || '',
-            pass: this.config.pass || '',
-            useDKIM: this.config.useDKIM || false,
-            dkimDomainName: this.config.dkimDomainName || '',
-            dkimKeySelector: this.config.dkimKeySelector || '',
-            dkimPrivateKey: this.config.dkimPrivateKey || ''
-          },
-          watchLoading (isLoading) {
-            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-update')
-          }
-        })
-        this.$store.commit('showNotification', {
-          style: 'success',
-          message: 'Configuration saved successfully.',
-          icon: 'check'
-        })
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-    }
-  },
-  apollo: {
-    hooks: {
-      query: mailConfigQuery,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.mail.config),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-mail-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 1 - 1
client/components/common/loader.vue

@@ -8,7 +8,7 @@
           :size='60'
           color='#FFF'
           )
-        img(v-else-if='mode === `icon`', :src='`/_assets/svg/icon-` + icon + `.svg`', :alt='icon')
+        img(v-else-if='mode === `icon`', :src='`/_assets-legacy/svg/icon-` + icon + `.svg`', :alt='icon')
         .subtitle-1.white--text {{ title }}
         .caption {{ subtitle }}
 </template>

+ 3 - 3
client/components/common/nav-header.vue

@@ -222,11 +222,11 @@
               //-   v-list-item-content
               //-     v-list-item-title {{$t('common:header.myWiki')}}
               //-     v-list-item-subtitle.overline Coming soon
-              v-list-item(href='/p')
+              v-list-item(href='/_profile')
                 v-list-item-action: v-icon(color='blue-grey') mdi-face-profile
                 v-list-item-content
                   v-list-item-title(:class='$vuetify.theme.dark ? `blue-grey--text text--lighten-3` : `blue-grey--text`') {{$t('common:header.profile')}}
-              v-list-item(@click='logout')
+              v-list-item(href='/logout')
                 v-list-item-action: v-icon(color='red') mdi-logout
                 v-list-item-title.red--text {{$t('common:header.logout')}}
 
@@ -314,7 +314,7 @@ export default {
           url: (this.pictureUrl === 'internal') ? `/_userav/${this.$store.get('user/id')}` : this.pictureUrl
         }
       } else {
-        const nameParts = this.name.toUpperCase().split(' ')
+        const nameParts = ['X', 'X'] // this.name.toUpperCase().split(' ')
         let initials = _.head(nameParts).charAt(0)
         if (nameParts.length > 1) {
           initials += _.last(nameParts).charAt(0)

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

@@ -2,7 +2,7 @@
   .search-results(v-if='searchIsFocused || (search && search.length > 1)')
     .search-results-container
       .search-results-help(v-if='!search || (search && search.length < 2)')
-        img(src='/_assets/svg/icon-search-alt.svg')
+        img(src='/_assets-legacy/svg/icon-search-alt.svg')
         .mt-4 {{$t('common:header.searchHint')}}
       .search-results-loader(v-else-if='searchIsLoading && (!results || results.length < 1)')
         orbit-spinner(
@@ -12,7 +12,7 @@
         )
         .headline.mt-5 {{$t('common:header.searchLoading')}}
       .search-results-none(v-else-if='!searchIsLoading && (!results || results.length < 1)')
-        img(src='/_assets/svg/icon-no-results.svg', alt='No Results')
+        img(src='/_assets-legacy/svg/icon-no-results.svg', alt='No Results')
         .subheading {{$t('common:header.searchNoResult')}}
       template(v-if='search && search.length >= 2 && results && results.length > 0')
         v-subheader.white--text {{$t('common:header.searchResultsCount', { total: response.totalHits })}}
@@ -20,7 +20,7 @@
           template(v-for='(item, idx) of results')
             v-list-item(@click='goToPage(item)', @click.middle="goToPageInNewTab(item)", :key='item.id', :class='idx === cursor ? `highlighted` : ``')
               v-list-item-avatar(tile)
-                img(src='/_assets/svg/icon-selective-highlighting.svg')
+                img(src='/_assets-legacy/svg/icon-selective-highlighting.svg')
               v-list-item-content
                 v-list-item-title(v-text='item.title')
                 v-list-item-subtitle.caption(v-text='item.description')

+ 32 - 36
client/components/editor.vue

@@ -295,7 +295,6 @@ export default {
                 $content: String!
                 $description: String!
                 $editor: String!
-                $isPrivate: Boolean!
                 $isPublished: Boolean!
                 $locale: String!
                 $path: String!
@@ -303,35 +302,32 @@ export default {
                 $publishStartDate: Date
                 $scriptCss: String
                 $scriptJs: String
+                $siteId: UUID!
                 $tags: [String]!
                 $title: String!
               ) {
-                pages {
-                  create(
-                    content: $content
-                    description: $description
-                    editor: $editor
-                    isPrivate: $isPrivate
-                    isPublished: $isPublished
-                    locale: $locale
-                    path: $path
-                    publishEndDate: $publishEndDate
-                    publishStartDate: $publishStartDate
-                    scriptCss: $scriptCss
-                    scriptJs: $scriptJs
-                    tags: $tags
-                    title: $title
-                  ) {
-                    responseResult {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                    page {
-                      id
-                      updatedAt
-                    }
+                createPage(
+                  content: $content
+                  description: $description
+                  editor: $editor
+                  isPublished: $isPublished
+                  locale: $locale
+                  path: $path
+                  publishEndDate: $publishEndDate
+                  publishStartDate: $publishStartDate
+                  scriptCss: $scriptCss
+                  scriptJs: $scriptJs
+                  siteId: $siteId
+                  tags: $tags
+                  title: $title
+                ) {
+                  operation {
+                    succeeded
+                    message
+                  }
+                  page {
+                    id
+                    updatedAt
                   }
                 }
               }
@@ -341,32 +337,32 @@ export default {
               description: this.$store.get('page/description'),
               editor: this.$store.get('editor/editorKey'),
               locale: this.$store.get('page/locale'),
-              isPrivate: false,
               isPublished: this.$store.get('page/isPublished'),
               path: this.$store.get('page/path'),
               publishEndDate: this.$store.get('page/publishEndDate') || '',
               publishStartDate: this.$store.get('page/publishStartDate') || '',
               scriptCss: this.$store.get('page/scriptCss'),
               scriptJs: this.$store.get('page/scriptJs'),
+              siteId: this.$store.get('site/id'),
               tags: this.$store.get('page/tags'),
               title: this.$store.get('page/title')
             }
           })
-          resp = _.get(resp, 'data.pages.create', {})
-          if (_.get(resp, 'responseResult.succeeded')) {
-            this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
+          resp = resp?.data?.createPage || {}
+          if (resp?.operation?.succeeded) {
+            this.checkoutDateActive = resp?.page?.updatedAt ?? this.checkoutDateActive
             this.isConflict = false
             this.$store.commit('showNotification', {
               message: this.$t('editor:save.createSuccess'),
               style: 'success',
               icon: 'check'
             })
-            this.$store.set('editor/id', _.get(resp, 'page.id'))
+            this.$store.set('editor/id', resp?.page?.id)
             this.$store.set('editor/mode', 'update')
             this.exitConfirmed = true
             window.location.assign(`/${this.$store.get('page/locale')}/${this.$store.get('page/path')}`)
           } else {
-            throw new Error(_.get(resp, 'responseResult.message'))
+            throw new Error(resp?.operation?.message)
           }
         } else {
           // --------------------------------------------
@@ -427,7 +423,7 @@ export default {
                     tags: $tags
                     title: $title
                   ) {
-                    responseResult {
+                    operation {
                       succeeded
                       errorCode
                       slug
@@ -458,7 +454,7 @@ export default {
             }
           })
           resp = _.get(resp, 'data.pages.update', {})
-          if (_.get(resp, 'responseResult.succeeded')) {
+          if (_.get(resp, 'operation.succeeded')) {
             this.checkoutDateActive = _.get(resp, 'page.updatedAt', this.checkoutDateActive)
             this.isConflict = false
             this.$store.commit('showNotification', {
@@ -472,7 +468,7 @@ export default {
               }, 1000)
             }
           } else {
-            throw new Error(_.get(resp, 'responseResult.message'))
+            throw new Error(_.get(resp, 'operation.message'))
           }
         }
 

+ 2 - 2
client/components/editor/editor-api.vue

@@ -59,7 +59,7 @@
                       v-list-item-group(v-model='kind', mandatory, color='primary')
                         v-list-item(value='rest')
                           v-list-item-avatar
-                            img(src='/_assets/svg/icon-transaction-list.svg', alt='REST')
+                            img(src='/_assets-legacy/svg/icon-transaction-list.svg', alt='REST')
                           v-list-item-content
                             v-list-item-title REST API
                             v-list-item-subtitle Classic REST Endpoints
@@ -67,7 +67,7 @@
                             v-icon(:color='kind === `rest` ? `primary` : `grey lighten-3`') mdi-check-circle
                         v-list-item(value='graphql', disabled)
                           v-list-item-avatar
-                            img(src='/_assets/svg/icon-graphql.svg', alt='GraphQL')
+                            img(src='/_assets-legacy/svg/icon-graphql.svg', alt='GraphQL')
                           v-list-item-content
                             v-list-item-title GraphQL
                             v-list-item-subtitle.grey--text.text--lighten-1 Schema-based API

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

@@ -259,7 +259,7 @@ import tabsetHelper from './markdown/tabset'
 const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
 
 // Prism Config
-Prism.plugins.autoloader.languages_path = '/_assets/js/prism/'
+Prism.plugins.autoloader.languages_path = '/_assets-legacy/js/prism/'
 Prism.plugins.NormalizeWhitespace.setDefaults({
   'remove-trailing': true,
   'remove-indent': true,
@@ -378,7 +378,7 @@ md.renderer.rules.katex_block = (tokens, idx) => {
 md.renderer.rules.emoji = (token, idx) => {
   return twemoji.parse(token[idx].content, {
     callback (icon, opts) {
-      return `/_assets/svg/twemoji/${icon}.svg`
+      return `/_assets-legacy/svg/twemoji/${icon}.svg`
     }
   })
 }

+ 4 - 125
client/components/editor/editor-modal-editorselect.vue

@@ -6,46 +6,6 @@
         .subtitle-1.white--text {{$t('editor:select.title')}}
         v-container(grid-list-lg, fluid)
           v-layout(row, wrap, justify-center)
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.primary.animated.fadeInUp(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/editor-icon-api.svg', alt='API', style='width: 36px; opacity: .5;')
-                      .body-2.blue--text.mt-2.text--lighten-2 API Docs
-                      .caption.blue--text.text--lighten-1 REST / GraphQL
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='primary'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.primary.animated.fadeInUp.wait-p1s(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/editor-icon-wikitext.svg', alt='WikiText', style='width: 36px; opacity: .5;')
-                      .body-2.blue--text.mt-2.text--lighten-2 Blog
-                      .caption.blue--text.text--lighten-1 Timeline of Posts
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='primary'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
             v-flex(xs4)
               v-card.radius-7.animated.fadeInUp.wait-p2s(
                 hover
@@ -53,7 +13,7 @@
                 ripple
                 )
                 v-card-text.text-center(@click='selectEditor("code")')
-                  img(src='/_assets/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
+                  img(src='/_assets-legacy/svg/editor-icon-code.svg', alt='Code', style='width: 36px;')
                   .body-2.primary--text.mt-2 Code
                   .caption.grey--text Raw HTML
             v-flex(xs4)
@@ -63,29 +23,9 @@
                 ripple
                 )
                 v-card-text.text-center(@click='selectEditor("markdown")')
-                  img(src='/_assets/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')
+                  img(src='/_assets-legacy/svg/editor-icon-markdown.svg', alt='Markdown', style='width: 36px;')
                   .body-2.primary--text.mt-2 Markdown
                   .caption.grey--text Plain Text Formatting
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.primary.animated.fadeInUp.wait-p2s(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/editor-icon-tabular.svg', alt='Tabular', style='width: 36px; opacity: .5;')
-                      .body-2.blue--text.mt-2.text--lighten-2 Tabular
-                      .caption.blue--text.text--lighten-1 Excel-like
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='primary'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
             v-flex(xs4)
               v-card.radius-7.animated.fadeInUp.wait-p3s(
                 hover
@@ -93,10 +33,9 @@
                 ripple
                 )
                 v-card-text.text-center(@click='selectEditor("ckeditor")')
-                  img(src='/_assets/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
+                  img(src='/_assets-legacy/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
                   .body-2.mt-2.primary--text Visual Editor
                   .caption.grey--text Rich-text WYSIWYG
-        //- .caption.blue--text.text--lighten-2 {{$t('editor:select.cannotChange')}}
 
     v-card.radius-7.mt-2(color='teal darken-3', dark)
       v-card-text.text-center.py-4
@@ -112,69 +51,9 @@
                     ripple
                     )
                     v-card-text.text-center(@click='fromTemplate')
-                      img(src='/_assets/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
+                      img(src='/_assets-legacy/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
                       .body-2.mt-1.teal--text From Template
                       .caption.grey--text Use an existing page...
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.teal.animated.fadeInUp.wait-p1s(
-                    hover
-                    light
-                    ripple
-                    )
-                    //- v-card-text.text-center(@click='selectEditor("redirect")')
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/icon-route.svg', alt='Redirection', style='width: 42px; opacity: .5;')
-                      .body-2.mt-1.teal--text.text--lighten-2 Redirection
-                      .caption.teal--text.text--lighten-1 Redirect the user to...
-            v-flex(xs4)
-              v-hover
-                template(v-slot:default='{ hover }')
-                  v-card.radius-7.teal.animated.fadeInUp.wait-p2s(
-                    hover
-                    light
-                    ripple
-                    )
-                    v-card-text.text-center(@click='')
-                      img(src='/_assets/svg/icon-sewing-patch.svg', alt='Code', style='width: 42px; opacity: .5;')
-                      .body-2.mt-1.teal--text.text--lighten-2 Embed
-                      .caption.teal--text.text--lighten-1 Include external pages
-                    v-fade-transition
-                      v-overlay(
-                        v-if='hover'
-                        absolute
-                        color='teal'
-                        opacity='.8'
-                        )
-                        .body-2.mt-7 Coming Soon
-    v-hover
-      template(v-slot:default='{ hover }')
-        v-card.radius-7.mt-2(color='indigo darken-3', dark)
-          v-toolbar(dense, flat, color='light-green darken-3')
-            v-spacer
-            .caption.mr-1 or convert from
-            v-btn.mx-1.animated.fadeInUp(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-a-circle
-              .body-2.text-none AsciiDoc
-            v-btn.mx-1.animated.fadeInUp.wait-p1s(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-c-circle
-              .body-2.text-none CREOLE
-            v-btn.mx-1.animated.fadeInUp.wait-p2s(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-t-circle
-              .body-2.text-none Textile
-            v-btn.mx-1.animated.fadeInUp.wait-p3s(depressed, color='light-green darken-2', @click='', disabled)
-              v-icon(left) mdi-alpha-w-circle
-              .body-2.text-none WikiText
-            v-spacer
-          v-fade-transition
-            v-overlay(
-              v-if='hover'
-              absolute
-              color='light-green darken-3'
-              opacity='.8'
-              )
-              .body-2 Coming Soon
 
     page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)
 </template>

+ 0 - 801
client/components/login.vue

@@ -1,801 +0,0 @@
-<template lang="pug">
-  v-app
-    .login(:style='`background-image: url(` + bgUrl + `);`')
-      .login-sd
-        .d-flex.mb-5
-          .login-logo
-            v-avatar(tile, size='34')
-              v-img(:src='logoUrl')
-          .login-title
-            .text-h6.grey--text.text--darken-4 {{ siteTitle }}
-        v-alert.mb-0(
-          v-model='errorShown'
-          transition='slide-y-reverse-transition'
-          color='red darken-2'
-          tile
-          dark
-          dense
-          icon='mdi-alert'
-          )
-          .body-2 {{errorMessage}}
-        //-------------------------------------------------
-        //- PROVIDERS LIST
-        //-------------------------------------------------
-        template(v-if='screen === `login` && strategies.length > 1')
-          .login-subtitle
-            .text-subtitle-1 {{$t('auth:selectAuthProvider')}}
-          .login-list
-            v-list.elevation-1.radius-7(nav, light)
-              v-list-item-group(v-model='selectedStrategyKey')
-                v-list-item(
-                  v-for='(stg, idx) of filteredStrategies'
-                  :key='stg.key'
-                  :value='stg.key'
-                  :color='stg.strategy.color'
-                  )
-                  v-avatar.mr-3(tile, size='24', v-html='stg.strategy.icon')
-                  span.text-none {{stg.displayName}}
-        //-------------------------------------------------
-        //- LOGIN FORM
-        //-------------------------------------------------
-        template(v-if='screen === `login` && selectedStrategy.strategy.useForm')
-          .login-subtitle
-            .text-subtitle-1 {{$t('auth:enterCredentials')}}
-          .login-form
-            v-text-field(
-              solo
-              flat
-              prepend-inner-icon='mdi-clipboard-account'
-              background-color='white'
-              color='blue darken-2'
-              hide-details
-              ref='iptEmail'
-              v-model='username'
-              :placeholder='isUsernameEmail ? $t(`auth:fields.email`) : $t(`auth:fields.username`)'
-              :type='isUsernameEmail ? `email` : `text`'
-              :autocomplete='isUsernameEmail ? `email` : `username`'
-              light
-              )
-            v-text-field.mt-2(
-              solo
-              flat
-              prepend-inner-icon='mdi-form-textbox-password'
-              background-color='white'
-              color='blue darken-2'
-              hide-details
-              ref='iptPassword'
-              v-model='password'
-              :append-icon='hidePassword ? "mdi-eye-off" : "mdi-eye"'
-              @click:append='() => (hidePassword = !hidePassword)'
-              :type='hidePassword ? "password" : "text"'
-              :placeholder='$t("auth:fields.password")'
-              autocomplete='current-password'
-              @keyup.enter='login'
-              light
-            )
-            v-btn.mt-2.text-none(
-              width='100%'
-              large
-              color='blue darken-2'
-              dark
-              @click='login'
-              :loading='isLoading'
-              ) {{ $t('auth:actions.login') }}
-            .text-center.mt-5
-              v-btn.text-none(
-                text
-                rounded
-                color='grey darken-3'
-                @click.stop.prevent='forgotPassword'
-                href='#forgot'
-                ): .caption {{ $t('auth:forgotPasswordLink') }}
-              v-btn.text-none(
-                v-if='selectedStrategyKey === `local` && selectedStrategy.selfRegistration'
-                color='indigo darken-2'
-                text
-                rounded
-                href='/register'
-                ): .caption {{ $t('auth:switchToRegister.link') }}
-        //-------------------------------------------------
-        //- FORGOT PASSWORD FORM
-        //-------------------------------------------------
-        template(v-if='screen === `forgot`')
-          .login-subtitle
-            .text-subtitle-1 {{$t('auth:forgotPasswordTitle')}}
-          .login-info {{ $t('auth:forgotPasswordSubtitle') }}
-          .login-form
-            v-text-field(
-              solo
-              flat
-              prepend-inner-icon='mdi-clipboard-account'
-              background-color='white'
-              color='blue darken-2'
-              hide-details
-              ref='iptForgotPwdEmail'
-              v-model='username'
-              :placeholder='$t(`auth:fields.email`)'
-              type='email'
-              autocomplete='email'
-              light
-              )
-            v-btn.mt-2.text-none(
-              width='100%'
-              large
-              color='blue darken-2'
-              dark
-              @click='forgotPasswordSubmit'
-              :loading='isLoading'
-              ) {{ $t('auth:sendResetPassword') }}
-            .text-center.mt-5
-              v-btn.text-none(
-                text
-                rounded
-                color='grey darken-3'
-                @click.stop.prevent='screen = `login`'
-                href='#forgot'
-                ): .caption {{ $t('auth:forgotPasswordCancel') }}
-        //-------------------------------------------------
-        //- CHANGE PASSWORD FORM
-        //-------------------------------------------------
-        template(v-if='screen === `changePwd`')
-          .login-subtitle
-            .text-subtitle-1 {{ $t('auth:changePwd.subtitle') }}
-          .login-form
-            v-text-field.mt-2(
-              type='password'
-              solo
-              flat
-              prepend-inner-icon='mdi-form-textbox-password'
-              background-color='white'
-              color='blue darken-2'
-              hide-details
-              ref='iptNewPassword'
-              v-model='newPassword'
-              :placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'
-              autocomplete='new-password'
-              light
-              )
-              password-strength(slot='progress', v-model='newPassword')
-            v-text-field.mt-2(
-              type='password'
-              solo
-              flat
-              prepend-inner-icon='mdi-form-textbox-password'
-              background-color='white'
-              color='blue darken-2'
-              hide-details
-              v-model='newPasswordVerify'
-              :placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'
-              autocomplete='new-password'
-              @keyup.enter='changePassword'
-              light
-            )
-            v-btn.mt-2.text-none(
-              width='100%'
-              large
-              color='blue darken-2'
-              dark
-              @click='changePassword'
-              :loading='isLoading'
-              ) {{ $t('auth:changePwd.proceed') }}
-
-    //-------------------------------------------------
-    //- TFA FORM
-    //-------------------------------------------------
-    v-dialog(v-model='isTFAShown', max-width='500', persistent)
-      v-card
-        .login-tfa.text-center.pa-5.grey--text.text--darken-3
-          img(src='_assets/svg/icon-pin-pad.svg')
-          .subtitle-2 {{$t('auth:tfaFormTitle')}}
-          v-text-field.login-tfa-field.mt-2(
-            solo
-            flat
-            background-color='white'
-            color='blue darken-2'
-            hide-details
-            ref='iptTFA'
-            v-model='securityCode'
-            :placeholder='$t("auth:tfa.placeholder")'
-            autocomplete='one-time-code'
-            @keyup.enter='verifySecurityCode(false)'
-            light
-          )
-          v-btn.mt-2.text-none(
-            width='100%'
-            large
-            color='blue darken-2'
-            dark
-            @click='verifySecurityCode(false)'
-            :loading='isLoading'
-            ) {{ $t('auth:tfa.verifyToken') }}
-
-    //-------------------------------------------------
-    //- SETUP TFA FORM
-    //-------------------------------------------------
-    v-dialog(v-model='isTFASetupShown', max-width='600', persistent)
-      v-card
-        .login-tfa.text-center.pa-5.grey--text.text--darken-3
-          .subtitle-1.primary--text {{$t('auth:tfaSetupTitle')}}
-          v-divider.my-5
-          .subtitle-2 {{$t('auth:tfaSetupInstrFirst')}}
-          .caption (#[a(href='https://authy.com/', target='_blank', noopener) Authy], #[a(href='https://support.google.com/accounts/answer/1066447', target='_blank', noopener) Google Authenticator], #[a(href='https://www.microsoft.com/en-us/account/authenticator', target='_blank', noopener) Microsoft Authenticator], etc.)
-          .login-tfa-qr.mt-5(v-if='isTFASetupShown', v-html='tfaQRImage')
-          .subtitle-2.mt-5 {{$t('auth:tfaSetupInstrSecond')}}
-          v-text-field.login-tfa-field.mt-2(
-            solo
-            flat
-            background-color='white'
-            color='blue darken-2'
-            hide-details
-            ref='iptTFASetup'
-            v-model='securityCode'
-            :placeholder='$t("auth:tfa.placeholder")'
-            autocomplete='one-time-code'
-            @keyup.enter='verifySecurityCode(true)'
-            light
-          )
-          v-btn.mt-2.text-none(
-            width='100%'
-            large
-            color='blue darken-2'
-            dark
-            @click='verifySecurityCode(true)'
-            :loading='isLoading'
-            ) {{ $t('auth:tfa.verifyToken') }}
-
-    loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
-    notify(style='padding-top: 64px;')
-</template>
-
-<script>
-/* global siteConfig */
-
-// <span>Photo by <a href="https://unsplash.com/@isaacquesada?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Isaac Quesada</a> on <a href="/t/textures-patterns?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>
-
-import _ from 'lodash'
-import Cookies from 'js-cookie'
-import gql from 'graphql-tag'
-import { sync } from 'vuex-pathify'
-
-export default {
-  i18nOptions: { namespaces: 'auth' },
-  props: {
-    bgUrl: {
-      type: String,
-      default: ''
-    },
-    hideLocal: {
-      type: Boolean,
-      default: false
-    },
-    changePwdContinuationToken: {
-      type: String,
-      default: null
-    }
-  },
-  data () {
-    return {
-      error: false,
-      strategies: [],
-      selectedStrategyKey: 'unselected',
-      selectedStrategy: { key: 'unselected', strategy: { useForm: false, usernameType: 'email' } },
-      screen: 'login',
-      username: '',
-      password: '',
-      hidePassword: true,
-      securityCode: '',
-      continuationToken: '',
-      isLoading: false,
-      loaderColor: 'grey darken-4',
-      loaderTitle: 'Working...',
-      isShown: false,
-      newPassword: '',
-      newPasswordVerify: '',
-      isTFAShown: false,
-      isTFASetupShown: false,
-      tfaQRImage: '',
-      errorShown: false,
-      errorMessage: ''
-    }
-  },
-  computed: {
-    activeModal: sync('editor/activeModal'),
-    siteTitle () {
-      return siteConfig.title
-    },
-    isSocialShown () {
-      return this.strategies.length > 1
-    },
-    logoUrl () { return siteConfig.logoUrl },
-    filteredStrategies () {
-      const qParams = new URLSearchParams(window.location.search)
-      if (this.hideLocal && !qParams.has('all')) {
-        return _.reject(this.strategies, ['key', 'local'])
-      } else {
-        return this.strategies
-      }
-    },
-    isUsernameEmail () {
-      return this.selectedStrategy.strategy.usernameType === `email`
-    }
-  },
-  watch: {
-    filteredStrategies (newValue, oldValue) {
-      if (_.head(newValue).strategy.useForm) {
-        this.selectedStrategyKey = _.head(newValue).key
-      }
-    },
-    selectedStrategyKey (newValue, oldValue) {
-      this.selectedStrategy = _.find(this.strategies, ['key', newValue])
-      if (this.screen === 'changePwd') {
-        return
-      }
-      this.screen = 'login'
-      if (!this.selectedStrategy.strategy.useForm) {
-        this.isLoading = true
-        window.location.assign('/login/' + newValue)
-      } else {
-        this.$nextTick(() => {
-          this.$refs.iptEmail.focus()
-        })
-      }
-    }
-  },
-  mounted () {
-    this.isShown = true
-    if (this.changePwdContinuationToken) {
-      this.screen = 'changePwd'
-      this.continuationToken = this.changePwdContinuationToken
-    }
-  },
-  methods: {
-    /**
-     * LOGIN
-     */
-    async login () {
-      this.errorShown = false
-      if (this.username.length < 2) {
-        this.errorMessage = this.$t('auth:invalidEmailUsername')
-        this.errorShown = true
-        this.$refs.iptEmail.focus()
-      } else if (this.password.length < 2) {
-        this.errorMessage = this.$t('auth:invalidPassword')
-        this.errorShown = true
-        this.$refs.iptPassword.focus()
-      } else {
-        this.loaderColor = 'grey darken-4'
-        this.loaderTitle = this.$t('auth:signingIn')
-        this.isLoading = true
-        try {
-          const resp = await this.$apollo.mutate({
-            mutation: gql`
-              mutation($username: String!, $password: String!, $strategy: String!) {
-                authentication {
-                  login(username: $username, password: $password, strategy: $strategy) {
-                    responseResult {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                    jwt
-                    mustChangePwd
-                    mustProvideTFA
-                    mustSetupTFA
-                    continuationToken
-                    redirect
-                    tfaQRImage
-                  }
-                }
-              }
-            `,
-            variables: {
-              username: this.username,
-              password: this.password,
-              strategy: this.selectedStrategy.key
-            }
-          })
-          if (_.has(resp, 'data.authentication.login')) {
-            const respObj = _.get(resp, 'data.authentication.login', {})
-            if (respObj.responseResult.succeeded === true) {
-              this.handleLoginResponse(respObj)
-            } else {
-              throw new Error(respObj.responseResult.message)
-            }
-          } else {
-            throw new Error(this.$t('auth:genericError'))
-          }
-        } catch (err) {
-          console.error(err)
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: err.message,
-            icon: 'alert'
-          })
-          this.isLoading = false
-        }
-      }
-    },
-    /**
-     * VERIFY TFA CODE
-     */
-    async verifySecurityCode (setup = false) {
-      if (this.securityCode.length !== 6) {
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: 'Enter a valid security code.',
-          icon: 'alert'
-        })
-        if (setup) {
-          this.$refs.iptTFASetup.focus()
-        } else {
-          this.$refs.iptTFA.focus()
-        }
-      } else {
-        this.loaderColor = 'grey darken-4'
-        this.loaderTitle = this.$t('auth:signingIn')
-        this.isLoading = true
-        try {
-          const resp = await this.$apollo.mutate({
-            mutation: gql`
-              mutation(
-                $continuationToken: String!
-                $securityCode: String!
-                $setup: Boolean
-                ) {
-                authentication {
-                  loginTFA(
-                    continuationToken: $continuationToken
-                    securityCode: $securityCode
-                    setup: $setup
-                    ) {
-                    responseResult {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                    jwt
-                    mustChangePwd
-                    continuationToken
-                    redirect
-                  }
-                }
-              }
-            `,
-            variables: {
-              continuationToken: this.continuationToken,
-              securityCode: this.securityCode,
-              setup
-            }
-          })
-          if (_.has(resp, 'data.authentication.loginTFA')) {
-            let respObj = _.get(resp, 'data.authentication.loginTFA', {})
-            if (respObj.responseResult.succeeded === true) {
-              this.handleLoginResponse(respObj)
-            } else {
-              if (!setup) {
-                this.isTFAShown = false
-              }
-              throw new Error(respObj.responseResult.message)
-            }
-          } else {
-            throw new Error(this.$t('auth:genericError'))
-          }
-        } catch (err) {
-          console.error(err)
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: err.message,
-            icon: 'alert'
-          })
-          this.isLoading = false
-        }
-      }
-    },
-    /**
-     * CHANGE PASSWORD
-     */
-    async changePassword () {
-      this.loaderColor = 'grey darken-4'
-      this.loaderTitle = this.$t('auth:changePwd.loading')
-      this.isLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $continuationToken: String!
-              $newPassword: String!
-            ) {
-              authentication {
-                loginChangePassword (
-                  continuationToken: $continuationToken
-                  newPassword: $newPassword
-                ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                  jwt
-                  continuationToken
-                  redirect
-                }
-              }
-            }
-          `,
-          variables: {
-            continuationToken: this.continuationToken,
-            newPassword: this.newPassword
-          }
-        })
-        if (_.has(resp, 'data.authentication.loginChangePassword')) {
-          let respObj = _.get(resp, 'data.authentication.loginChangePassword', {})
-          if (respObj.responseResult.succeeded === true) {
-            this.handleLoginResponse(respObj)
-          } else {
-            throw new Error(respObj.responseResult.message)
-          }
-        } else {
-          throw new Error(this.$t('auth:genericError'))
-        }
-      } catch (err) {
-        console.error(err)
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: err.message,
-          icon: 'alert'
-        })
-        this.isLoading = false
-      }
-    },
-    /**
-     * SWITCH TO FORGOT PASSWORD SCREEN
-     */
-    forgotPassword () {
-      this.screen = 'forgot'
-      this.$nextTick(() => {
-        this.$refs.iptForgotPwdEmail.focus()
-      })
-    },
-    /**
-     * FORGOT PASSWORD SUBMIT
-     */
-    async forgotPasswordSubmit () {
-      this.loaderColor = 'grey darken-4'
-      this.loaderTitle = this.$t('auth:forgotPasswordLoading')
-      this.isLoading = true
-      try {
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation (
-              $email: String!
-            ) {
-              authentication {
-                forgotPassword (
-                  email: $email
-                ) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                }
-              }
-            }
-          `,
-          variables: {
-            email: this.username
-          }
-        })
-        if (_.has(resp, 'data.authentication.forgotPassword.responseResult')) {
-          let respObj = _.get(resp, 'data.authentication.forgotPassword.responseResult', {})
-          if (respObj.succeeded === true) {
-            this.$store.commit('showNotification', {
-              style: 'success',
-              message: this.$t('auth:forgotPasswordSuccess'),
-              icon: 'email'
-            })
-            this.screen = 'login'
-          } else {
-            throw new Error(respObj.message)
-          }
-        } else {
-          throw new Error(this.$t('auth:genericError'))
-        }
-      } catch (err) {
-        console.error(err)
-        this.$store.commit('showNotification', {
-          style: 'red',
-          message: err.message,
-          icon: 'alert'
-        })
-      }
-      this.isLoading = false
-    },
-    handleLoginResponse (respObj) {
-      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.securityCode = ''
-        this.isTFAShown = true
-        setTimeout(() => {
-          this.$refs.iptTFA.focus()
-        }, 500)
-        this.isLoading = false
-      } else if (respObj.mustSetupTFA === true) {
-        this.securityCode = ''
-        this.isTFASetupShown = true
-        this.tfaQRImage = respObj.tfaQRImage
-        setTimeout(() => {
-          this.$refs.iptTFASetup.focus()
-        }, 500)
-        this.isLoading = false
-      } else {
-        this.loaderColor = 'green darken-1'
-        this.loaderTitle = this.$t('auth:loginSuccess')
-        Cookies.set('jwt', respObj.jwt, { expires: 365 })
-        _.delay(() => {
-          const loginRedirect = Cookies.get('loginRedirect')
-          if (loginRedirect === '/' && respObj.redirect) {
-            Cookies.remove('loginRedirect')
-            window.location.replace(respObj.redirect)
-          } else if (loginRedirect) {
-            Cookies.remove('loginRedirect')
-            window.location.replace(loginRedirect)
-          } else if (respObj.redirect) {
-            window.location.replace(respObj.redirect)
-          } else {
-            window.location.replace('/')
-          }
-        }, 1000)
-      }
-    }
-  },
-  apollo: {
-    strategies: {
-      query: gql`
-        query loginFetchSiteStrategies(
-          $siteId: UUID
-        ) {
-          authStrategies(
-            siteId: $siteId
-            enabledOnly: true
-            ) {
-            key
-            strategy {
-              key
-              logo
-              color
-              icon
-              useForm
-              usernameType
-            }
-            displayName
-            order
-            selfRegistration
-          }
-        }
-      `,
-      variables () {
-        return {
-          siteId: siteConfig.id
-        }
-      },
-      update: (data) => _.sortBy(data.authStrategies, ['order']),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-  .login {
-    // background-image: url('/_assets/img/splash/1.jpg');
-    background-color: mc('grey', '900');
-    background-size: cover;
-    background-position: center center;
-    width: 100%;
-    height: 100%;
-
-    &-sd {
-      background-color: rgba(255,255,255,.8);
-      backdrop-filter: blur(10px);
-      -webkit-backdrop-filter: blur(10px);
-      border-left: 1px solid rgba(255,255,255,.85);
-      border-right: 1px solid rgba(255,255,255,.85);
-      width: 450px;
-      height: 100%;
-      margin-left: 5vw;
-
-      @at-root .no-backdropfilter & {
-        background-color: rgba(255,255,255,.95);
-      }
-
-      @include until($tablet) {
-        margin-left: 0;
-        width: 100%;
-      }
-    }
-
-    &-logo {
-      padding: 12px 0 0 12px;
-      width: 58px;
-      height: 58px;
-      background-color: #222;
-      margin-left: 12px;
-      border-bottom-left-radius: 7px;
-      border-bottom-right-radius: 7px;
-    }
-
-    &-title {
-      height: 58px;
-      padding-left: 12px;
-      display: flex;
-      align-items: center;
-      text-shadow: .5px .5px #FFF;
-    }
-
-    &-subtitle {
-      padding: 24px 12px 12px 12px;
-      color: #111;
-      font-weight: 500;
-      text-shadow: 1px 1px rgba(255,255,255,.5);
-      background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,.15));
-      text-align: center;
-      border-bottom: 1px solid rgba(0,0,0,.3);
-    }
-
-    &-info {
-      border-top: 1px solid rgba(255,255,255,.85);
-      background-color: rgba(255,255,255,.15);
-      border-bottom: 1px solid rgba(0,0,0,.15);
-      padding: 12px;
-      font-size: 13px;
-      text-align: center;
-      color: mc('grey', '900');
-    }
-
-    &-list {
-      border-top: 1px solid rgba(255,255,255,.85);
-      padding: 12px;
-    }
-
-    &-form {
-      padding: 12px;
-      border-top: 1px solid rgba(255,255,255,.85);
-    }
-
-    &-main {
-      flex: 1 0 100vw;
-      height: 100vh;
-    }
-
-    &-tfa {
-      background-color: #EEE;
-      border: 7px solid #FFF;
-
-      &-field input {
-        text-align: center;
-      }
-
-      &-qr {
-        background-color: #FFF;
-        padding: 5px;
-        border-radius: 5px;
-        width: 200px;
-        height: 200px;
-        margin: 0 auto;
-      }
-    }
-  }
-</style>

+ 1 - 1
client/components/new-page.vue

@@ -2,7 +2,7 @@
   v-app
     .newpage
       .newpage-content
-        img.animated.fadeIn(src='/_assets/svg/icon-delete-file.svg', alt='Not Found')
+        img.animated.fadeIn(src='/_assets-legacy/svg/icon-delete-file.svg', alt='Not Found')
         .headline {{ $t('newpage.title') }}
         .subtitle-1.mt-3 {{ $t('newpage.subtitle') }}
         v-btn.mt-5(:href='`/e/` + locale + `/` + path', x-large)

+ 0 - 98
client/components/profile.vue

@@ -1,98 +0,0 @@
-<template lang='pug'>
-  v-app(:dark='$vuetify.theme.dark').profile
-    nav-header
-    v-navigation-drawer.pb-0(v-model='profileDrawerShown', app, fixed, clipped, left, permanent)
-      v-list(dense, nav)
-        v-list-item(to='/profile', color='primary')
-          v-list-item-action: v-icon mdi-face-profile
-          v-list-item-content
-            v-list-item-title {{$t('profile:title')}}
-        //- v-list-item(to='/preferences', disabled)
-        //-   v-list-item-action: v-icon(color='grey lighten-1') mdi-cog-outline
-        //-   v-list-item-content
-        //-     v-list-item-title Preferences
-        //-     v-list-item-subtitle.caption.grey--text.text--lighten-1 Coming soon
-        v-list-item(to='/pages', color='primary')
-          v-list-item-action: v-icon mdi-file-document-outline
-          v-list-item-content
-            v-list-item-title {{$t('profile:pages.title')}}
-        //- v-list-item(to='/comments', disabled)
-        //-   v-list-item-action: v-icon(color='grey lighten-1') mdi-message-reply-text
-        //-   v-list-item-content
-        //-     v-list-item-title {{$t('profile:comments.title')}}
-        //-     v-list-item-subtitle.caption.grey--text.text--lighten-1 Coming soon
-
-    v-content(:class='$vuetify.theme.dark ? "grey darken-4" : "grey lighten-5"')
-      transition(name='profile-router')
-        router-view
-
-    nav-footer
-    notify
-    search-results
-</template>
-
-<script>
-import VueRouter from 'vue-router'
-
-/* global WIKI */
-
-const router = new VueRouter({
-  mode: 'history',
-  base: '/p',
-  routes: [
-    { path: '/', redirect: '/profile' },
-    { path: '/profile', component: () => import(/* webpackChunkName: "profile" */ './profile/profile.vue') },
-    { path: '/pages', component: () => import(/* webpackChunkName: "profile" */ './profile/pages.vue') },
-    { path: '/comments', component: () => import(/* webpackChunkName: "profile" */ './profile/comments.vue') }
-  ]
-})
-
-router.beforeEach((to, from, next) => {
-  WIKI.$store.commit('loadingStart', 'profile')
-  next()
-})
-
-router.afterEach((to, from) => {
-  WIKI.$store.commit('loadingStop', 'profile')
-})
-
-export default {
-  i18nOptions: { namespaces: 'profile' },
-  data() {
-    return {
-      profileDrawerShown: true
-    }
-  },
-  router,
-  created() {
-    this.$store.commit('page/SET_MODE', 'profile')
-  }
-}
-</script>
-
-<style lang='scss'>
-
-.profile-router {
-  &-enter-active, &-leave-active {
-    transition: opacity .25s ease;
-    opacity: 1;
-  }
-  &-enter-active {
-    transition-delay: .25s;
-  }
-  &-enter, &-leave-to {
-    opacity: 0;
-  }
-}
-
-.profile-header {
-  display: flex;
-  justify-content: flex-start;
-  align-items: center;
-
-  &-title {
-    margin-left: 1rem;
-  }
-}
-
-</style>

+ 0 - 20
client/components/profile/comments.vue

@@ -1,20 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, fill-height, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .headline.primary--text Comments
-        .subheading.grey--text List of comments I posted
-</template>
-
-<script>
-
-export default {
-  data() {
-    return { }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 121
client/components/profile/pages.vue

@@ -1,121 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .profile-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-file.svg', alt='Users', style='width: 80px;')
-          .profile-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('profile:pages.title')}}
-            .subheading.grey--text.animated.fadeInLeft {{$t('profile:pages.subtitle')}}
-          v-spacer
-          v-btn.animated.fadeInDown.wait-p1s(color='grey', outlined, @click='refresh', large)
-            v-icon.grey--text mdi-refresh
-      v-flex(xs12)
-        v-card.animated.fadeInUp
-          v-data-table(
-            :items='pages'
-            :headers='headers'
-            :page.sync='pagination'
-            :items-per-page='15'
-            :loading='loading'
-            must-sort,
-            sort-by='updatedAt',
-            sort-desc,
-            hide-default-footer
-          )
-            template(slot='item', slot-scope='props')
-              tr.is-clickable(:active='props.selected', @click='goToPage(props.item.id)')
-                td
-                  .body-2: strong {{ props.item.title }}
-                  .caption {{ props.item.description }}
-                td.admin-pages-path
-                  v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
-                  span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
-                td {{ props.item.createdAt | moment('calendar') }}
-                td {{ props.item.updatedAt | moment('calendar') }}
-            template(slot='no-data')
-              v-alert.ma-3(icon='mdi-alert', :value='true', outlined, color='grey')
-                em.caption {{$t('profile:pages.emptyList')}}
-          .text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
-            v-pagination(v-model='pagination', :length='pageTotal')
-</template>
-
-<script>
-import gql from 'graphql-tag'
-
-export default {
-  data() {
-    return {
-      selectedPage: {},
-      pagination: 1,
-      pages: [],
-      loading: false
-    }
-  },
-  computed: {
-    headers () {
-      return [
-        { text: this.$t('profile:pages.headerTitle'), value: 'title' },
-        { text: this.$t('profile:pages.headerPath'), value: 'path' },
-        { text: this.$t('profile:pages.headerCreatedAt'), value: 'createdAt', width: 250 },
-        { text: this.$t('profile:pages.headerUpdatedAt'), value: 'updatedAt', width: 250 }
-      ]
-    },
-    pageTotal () {
-      return Math.ceil(this.pages.length / 15)
-    }
-  },
-  methods: {
-    async refresh() {
-      await this.$apollo.queries.pages.refetch()
-      this.$store.commit('showNotification', {
-        message: this.$t('profile:pages.refreshSuccess'),
-        style: 'success',
-        icon: 'cached'
-      })
-    },
-    goToPage(id) {
-      window.location.assign(`/i/` + id)
-    }
-  },
-  apollo: {
-    pages: {
-      query: gql`
-        query($creatorId: Int, $authorId: Int) {
-          pages {
-            list(creatorId: $creatorId, authorId: $authorId) {
-              id
-              locale
-              path
-              title
-              description
-              contentType
-              isPublished
-              isPrivate
-              privateNS
-              createdAt
-              updatedAt
-            }
-          }
-        }
-      `,
-      variables () {
-        return {
-          creatorId: this.$store.get('user/id'),
-          authorId: this.$store.get('user/id')
-        }
-      },
-      fetchPolicy: 'network-only',
-      update: (data) => data.pages.list,
-      watchLoading (isLoading) {
-        this.loading = isLoading
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'profile-pages-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 923
client/components/profile/profile.vue

@@ -1,923 +0,0 @@
-<template lang='pug'>
-  v-container(fluid, grid-list-lg)
-    v-layout(row wrap)
-      v-flex(xs12)
-        .profile-header
-          img.animated.fadeInUp(src='/_assets/svg/icon-profile.svg', alt='Users', style='width: 80px;')
-          .profile-header-title
-            .headline.primary--text.animated.fadeInLeft {{$t('profile:title')}}
-            .subheading.grey--text.animated.fadeInLeft {{$t('profile:subtitle')}}
-          v-spacer
-          v-btn.animated.fadeInDown(color='success', depressed, @click='saveProfile', :loading='saveLoading', large)
-            v-icon(left) mdi-check
-            span {{$t('common:actions.save')}}
-          //- v-btn.animated.fadeInDown(outlined, color='primary', disabled).mr-0
-          //-   v-icon(left) mdi-earth
-          //-   span {{$t('profile:viewPublicProfile')}}
-      v-flex(lg6 xs12)
-        v-card.animated.fadeInUp
-          v-toolbar(color='blue-grey', dark, dense, flat)
-            v-toolbar-title.subtitle-1 {{$t('profile:myInfo')}}
-          v-list(two-line, dense)
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-account
-              v-list-item-content
-                v-list-item-title {{$t('profile:displayName')}}
-                v-list-item-subtitle {{ user.name }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.name'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptDisplayName`)')
-                      v-icon(left) mdi-pencil
-                      span {{ $t('common:actions:edit') }}
-                  v-card
-                    v-text-field(
-                      ref='iptDisplayName'
-                      v-model='user.name'
-                      :label='$t(`profile:displayName`)'
-                      solo
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.name = false'
-                      @keydown.enter='editPop.name = false'
-                      @keydown.esc='editPop.name = false'
-                    )
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-map-marker
-              v-list-item-content
-                v-list-item-title {{$t('profile:location')}}
-                v-list-item-subtitle {{ user.location }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.location'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptLocation`)')
-                      v-icon(left) mdi-pencil
-                      span {{ $t('common:actions:edit') }}
-                  v-card
-                    v-text-field(
-                      ref='iptLocation'
-                      v-model='user.location'
-                      :label='$t(`profile:location`)'
-                      solo
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.location = false'
-                      @keydown.enter='editPop.location = false'
-                      @keydown.esc='editPop.location = false'
-                    )
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-briefcase
-              v-list-item-content
-                v-list-item-title {{$t('profile:jobTitle')}}
-                v-list-item-subtitle {{ user.jobTitle }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.jobTitle'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptJobTitle`)')
-                      v-icon(left) mdi-pencil
-                      span {{ $t('common:actions:edit') }}
-                  v-card
-                    v-text-field(
-                      ref='iptJobTitle'
-                      v-model='user.jobTitle'
-                      :label='$t(`profile:jobTitle`)'
-                      solo
-                      hide-details
-                      append-icon='mdi-check'
-                      @click:append='editPop.jobTitle = false'
-                      @keydown.enter='editPop.jobTitle = false'
-                      @keydown.esc='editPop.jobTitle = false'
-                    )
-
-        v-card.mt-3.animated.fadeInUp.wait-p2s
-          v-toolbar(color='blue-grey', dark, dense, flat)
-            v-toolbar-title
-              .subtitle-1 {{$t('profile:auth.title')}}
-          v-card-text.pt-0
-            v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.provider')}}
-            v-toolbar(
-              flat
-              :color='$vuetify.theme.dark ? "grey darken-2" : "purple lighten-5"'
-              dense
-              :class='$vuetify.theme.dark ? "grey--text text--lighten-1" : "purple--text text--darken-4"'
-              )
-              v-icon(:color='$vuetify.theme.dark ? "grey lighten-1" : "purple darken-4"') mdi-shield-lock
-              .subheading.ml-3 {{ user.providerName }}
-            //- v-divider.mt-3
-            //- v-subheader.pl-0: span.subtitle-2 Two-Factor Authentication (2FA)
-            //- .caption.mb-2 2FA adds an extra layer of security by requiring a unique code generated on your smartphone when signing in.
-            //- v-btn(color='purple darken-4', disabled).ml-0 Enable 2FA
-            //- v-btn(color='purple darken-4', dark, depressed, disabled).ml-0 Disable 2FA
-            template(v-if='user.providerKey === `local`')
-              v-divider.mt-3
-              v-subheader.pl-0: span.subtitle-2 {{$t('profile:auth.changePassword')}}
-              v-text-field(
-                ref='iptCurrentPass'
-                v-model='currentPass'
-                outlined
-                :label='$t(`profile:auth.currentPassword`)'
-                type='password'
-                prepend-inner-icon='mdi-form-textbox-password'
-                )
-              v-text-field(
-                ref='iptNewPass'
-                v-model='newPass'
-                outlined
-                :label='$t(`profile:auth.newPassword`)'
-                type='password'
-                prepend-inner-icon='mdi-form-textbox-password'
-                autocomplete='off'
-                counter='255'
-                loading
-                )
-                password-strength(slot='progress', v-model='newPass')
-              v-text-field(
-                ref='iptVerifyPass'
-                v-model='verifyPass'
-                outlined
-                :label='$t(`profile:auth.verifyPassword`)'
-                type='password'
-                prepend-inner-icon='mdi-form-textbox-password'
-                autocomplete='off'
-                hide-details
-                )
-          v-card-chin(v-if='user.providerKey === `local`')
-            v-spacer
-            v-btn.px-4(color='purple darken-4', dark, depressed, @click='changePassword', :loading='changePassLoading')
-              v-icon(left) mdi-progress-check
-              span {{$t('profile:auth.changePassword')}}
-      v-flex(lg6 xs12)
-        //- v-card
-        //-   v-toolbar(color='blue-grey', dark, dense, flat)
-        //-     v-toolbar-title
-        //-       .subtitle-1 Picture
-        //-   v-card-title
-        //-     v-avatar.blue(v-if='picture.kind === `initials`', :size='40')
-        //-       span.white--text.subheading {{picture.initials}}
-        //-     v-avatar(v-else-if='picture.kind === `image`', :size='40')
-        //-       v-img(:src='picture.url')
-        //-     v-btn(outlined).mx-4 Upload Picture
-        //-     v-btn(outlined, disabled) Remove Picture
-        v-card.animated.fadeInUp.wait-p2s
-          v-toolbar(color='blue-grey', dark, dense, flat)
-            v-toolbar-title.subtitle-1 {{$t('profile:preferences')}}
-          v-list(two-line, dense)
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-map-clock-outline
-              v-list-item-content
-                v-list-item-title {{$t('profile:timezone')}}
-                v-list-item-subtitle {{ user.timezone }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.timezone'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  max-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptTimezone`)')
-                      v-icon(left) mdi-pencil
-                      span {{ $t('common:actions:edit') }}
-                  v-card(flat)
-                    v-select(
-                      ref='iptTimezone'
-                      :items='timezones'
-                      v-model='user.timezone'
-                      :label='$t(`profile:timezone`)'
-                      solo
-                      flat
-                      dense
-                      hide-details
-                      @keydown.enter='editPop.timezone = false'
-                      @keydown.esc='editPop.timezone = false'
-                      style='height: 38px;'
-                    )
-                    v-card-chin
-                      v-spacer
-                      v-btn(
-                        small
-                        text
-                        color='primary'
-                        @click='editPop.timezone = false'
-                        )
-                        v-icon(left) mdi-check
-                        span {{$t('common:actions.ok')}}
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-calendar-month-outline
-              v-list-item-content
-                v-list-item-title {{$t('profile:dateFormat')}}
-                v-list-item-subtitle {{ user.dateFormat && user.dateFormat.length > 0 ? user.dateFormat : $t('profile:localeDefault') }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.dateFormat'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  max-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptDateFormat`)')
-                      v-icon(left) mdi-pencil
-                      span {{ $t('common:actions:edit') }}
-                  v-card(flat)
-                    v-select(
-                      ref='iptDateFormat'
-                      :items='dateFormats'
-                      v-model='user.dateFormat'
-                      :label='$t(`profile:dateFormat`)'
-                      solo
-                      flat
-                      dense
-                      hide-details
-                      @keydown.enter='editPop.dateFormat = false'
-                      @keydown.esc='editPop.dateFormat = false'
-                      style='height: 38px;'
-                    )
-                    v-card-chin
-                      v-spacer
-                      v-btn(
-                        small
-                        text
-                        color='primary'
-                        @click='editPop.dateFormat = false'
-                        )
-                        v-icon(left) mdi-check
-                        span {{$t('common:actions.ok')}}
-            v-divider
-            v-list-item
-              v-list-item-avatar(size='32')
-                v-icon mdi-palette
-              v-list-item-content
-                v-list-item-title {{$t('profile:appearance')}}
-                v-list-item-subtitle {{ currentAppearance }}
-              v-list-item-action
-                v-menu(
-                  v-model='editPop.appearance'
-                  :close-on-content-click='false'
-                  min-width='350'
-                  max-width='350'
-                  left
-                  )
-                  template(v-slot:activator='{ on }')
-                    v-btn(text, color='grey', small, v-on='on', @click='focusField(`iptAppearance`)')
-                      v-icon(left) mdi-pencil
-                      span {{ $t('common:actions:edit') }}
-                  v-card(flat)
-                    v-select(
-                      ref='iptAppearance'
-                      :items='appearances'
-                      v-model='user.appearance'
-                      :label='$t(`profile:appearance`)'
-                      solo
-                      flat
-                      dense
-                      hide-details
-                      @keydown.enter='editPop.appearance = false'
-                      @keydown.esc='editPop.appearance = false'
-                      style='height: 38px;'
-                    )
-                    v-card-chin
-                      v-spacer
-                      v-btn(
-                        small
-                        text
-                        color='primary'
-                        @click='editPop.appearance = false'
-                        )
-                        v-icon(left) mdi-check
-                        span {{$t('common:actions.ok')}}
-
-        v-card.mt-3.animated.fadeInUp.wait-p3s
-          v-toolbar(color='primary', dark, dense, flat)
-            v-toolbar-title
-              .subtitle-1 {{$t('profile:groups.title')}}
-          v-list(dense)
-            template(v-for='(grp, idx) of user.groups')
-              v-list-item(:key='`grp-id-` + grp')
-                v-list-item-avatar(size='32')
-                  v-icon mdi-account-group
-                v-list-item-content
-                  v-list-item-title.body-2 {{grp}}
-              v-divider(v-if='idx < user.groups.length - 1')
-
-        v-card.mt-3.animated.fadeInUp.wait-p4s
-          v-toolbar(color='teal', dark, dense, flat)
-            v-toolbar-title
-              .subtitle-1 {{$t('profile:activity.title')}}
-          v-card-text.grey--text.text--darken-2
-            .caption.grey--text {{$t('profile:activity.joinedOn')}}
-            .body-2: strong {{ user.createdAt | moment('LLLL') }}
-            .caption.grey--text.mt-3 {{$t('profile:activity.lastUpdatedOn')}}
-            .body-2: strong {{ user.updatedAt | moment('LLLL') }}
-            .caption.grey--text.mt-3 {{$t('profile:activity.lastLoginOn')}}
-            .body-2: strong {{ user.lastLoginAt | moment('LLLL') }}
-            v-divider.mt-3
-            .caption.grey--text.mt-3 {{$t('profile:activity.pagesCreated')}}
-            .body-2: strong {{ user.pagesTotal }}
-            .caption.grey--text.mt-3 {{$t('profile:activity.commentsPosted')}}
-            .body-2: strong 0
-</template>
-
-<script>
-import { get } from 'vuex-pathify'
-import gql from 'graphql-tag'
-import _ from 'lodash'
-import Cookies from 'js-cookie'
-import validate from 'validate.js'
-
-import PasswordStrength from '../common/password-strength.vue'
-
-/* global WIKI, siteConfig */
-
-export default {
-  i18nOptions: {
-    namespaces: ['profile', 'auth']
-  },
-  components: {
-    PasswordStrength
-  },
-  data() {
-    return {
-      saveLoading: false,
-      changePassLoading: false,
-      user: {
-        name: 'unknown',
-        location: '',
-        jobTitle: '',
-        timezone: '',
-        dateFormat: '',
-        appearance: '',
-        createdAt: '1970-01-01',
-        updatedAt: '1970-01-01',
-        lastLoginAt: '1970-01-01',
-        groups: []
-      },
-      currentPass: '',
-      newPass: '',
-      verifyPass: '',
-      editPop: {
-        name: false,
-        location: false,
-        jobTitle: false,
-        timezone: false,
-        dateFormat: false,
-        appearance: false
-      },
-      timezones: [
-        { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
-        { text: '(GMT-11:00) Pago Pago', value: 'Pacific/Pago_Pago' },
-        { text: '(GMT-10:00) Hawaii Time', value: 'Pacific/Honolulu' },
-        { text: '(GMT-10:00) Rarotonga', value: 'Pacific/Rarotonga' },
-        { text: '(GMT-10:00) Tahiti', value: 'Pacific/Tahiti' },
-        { text: '(GMT-09:30) Marquesas', value: 'Pacific/Marquesas' },
-        { text: '(GMT-09:00) Alaska Time', value: 'America/Anchorage' },
-        { text: '(GMT-09:00) Gambier', value: 'Pacific/Gambier' },
-        { text: '(GMT-08:00) Pacific Time', value: 'America/Los_Angeles' },
-        { text: '(GMT-08:00) Pacific Time - Tijuana', value: 'America/Tijuana' },
-        { text: '(GMT-08:00) Pacific Time - Vancouver', value: 'America/Vancouver' },
-        { text: '(GMT-08:00) Pacific Time - Whitehorse', value: 'America/Whitehorse' },
-        { text: '(GMT-08:00) Pitcairn', value: 'Pacific/Pitcairn' },
-        { text: '(GMT-07:00) Mountain Time', value: 'America/Denver' },
-        { text: '(GMT-07:00) Mountain Time - Arizona', value: 'America/Phoenix' },
-        { text: '(GMT-07:00) Mountain Time - Chihuahua, Mazatlan', value: 'America/Mazatlan' },
-        { text: '(GMT-07:00) Mountain Time - Dawson Creek', value: 'America/Dawson_Creek' },
-        { text: '(GMT-07:00) Mountain Time - Edmonton', value: 'America/Edmonton' },
-        { text: '(GMT-07:00) Mountain Time - Hermosillo', value: 'America/Hermosillo' },
-        { text: '(GMT-07:00) Mountain Time - Yellowknife', value: 'America/Yellowknife' },
-        { text: '(GMT-06:00) Belize', value: 'America/Belize' },
-        { text: '(GMT-06:00) Central Time', value: 'America/Chicago' },
-        { text: '(GMT-06:00) Central Time - Mexico City', value: 'America/Mexico_City' },
-        { text: '(GMT-06:00) Central Time - Regina', value: 'America/Regina' },
-        { text: '(GMT-06:00) Central Time - Tegucigalpa', value: 'America/Tegucigalpa' },
-        { text: '(GMT-06:00) Central Time - Winnipeg', value: 'America/Winnipeg' },
-        { text: '(GMT-06:00) Costa Rica', value: 'America/Costa_Rica' },
-        { text: '(GMT-06:00) El Salvador', value: 'America/El_Salvador' },
-        { text: '(GMT-06:00) Galapagos', value: 'Pacific/Galapagos' },
-        { text: '(GMT-06:00) Guatemala', value: 'America/Guatemala' },
-        { text: '(GMT-06:00) Managua', value: 'America/Managua' },
-        { text: '(GMT-05:00) America Cancun', value: 'America/Cancun' },
-        { text: '(GMT-05:00) Bogota', value: 'America/Bogota' },
-        { text: '(GMT-05:00) Easter Island', value: 'Pacific/Easter' },
-        { text: '(GMT-05:00) Eastern Time', value: 'America/New_York' },
-        { text: '(GMT-05:00) Eastern Time - Iqaluit', value: 'America/Iqaluit' },
-        { text: '(GMT-05:00) Eastern Time - Toronto', value: 'America/Toronto' },
-        { text: '(GMT-05:00) Guayaquil', value: 'America/Guayaquil' },
-        { text: '(GMT-05:00) Havana', value: 'America/Havana' },
-        { text: '(GMT-05:00) Jamaica', value: 'America/Jamaica' },
-        { text: '(GMT-05:00) Lima', value: 'America/Lima' },
-        { text: '(GMT-05:00) Nassau', value: 'America/Nassau' },
-        { text: '(GMT-05:00) Panama', value: 'America/Panama' },
-        { text: '(GMT-05:00) Port-au-Prince', value: 'America/Port-au-Prince' },
-        { text: '(GMT-05:00) Rio Branco', value: 'America/Rio_Branco' },
-        { text: '(GMT-04:00) Atlantic Time - Halifax', value: 'America/Halifax' },
-        { text: '(GMT-04:00) Barbados', value: 'America/Barbados' },
-        { text: '(GMT-04:00) Bermuda', value: 'Atlantic/Bermuda' },
-        { text: '(GMT-04:00) Boa Vista', value: 'America/Boa_Vista' },
-        { text: '(GMT-04:00) Caracas', value: 'America/Caracas' },
-        { text: '(GMT-04:00) Curacao', value: 'America/Curacao' },
-        { text: '(GMT-04:00) Grand Turk', value: 'America/Grand_Turk' },
-        { text: '(GMT-04:00) Guyana', value: 'America/Guyana' },
-        { text: '(GMT-04:00) La Paz', value: 'America/La_Paz' },
-        { text: '(GMT-04:00) Manaus', value: 'America/Manaus' },
-        { text: '(GMT-04:00) Martinique', value: 'America/Martinique' },
-        { text: '(GMT-04:00) Port of Spain', value: 'America/Port_of_Spain' },
-        { text: '(GMT-04:00) Porto Velho', value: 'America/Porto_Velho' },
-        { text: '(GMT-04:00) Puerto Rico', value: 'America/Puerto_Rico' },
-        { text: '(GMT-04:00) Santo Domingo', value: 'America/Santo_Domingo' },
-        { text: '(GMT-04:00) Thule', value: 'America/Thule' },
-        { text: '(GMT-03:30) Newfoundland Time - St. Johns', value: 'America/St_Johns' },
-        { text: '(GMT-03:00) Araguaina', value: 'America/Araguaina' },
-        { text: '(GMT-03:00) Asuncion', value: 'America/Asuncion' },
-        { text: '(GMT-03:00) Belem', value: 'America/Belem' },
-        { text: '(GMT-03:00) Buenos Aires', value: 'America/Argentina/Buenos_Aires' },
-        { text: '(GMT-03:00) Campo Grande', value: 'America/Campo_Grande' },
-        { text: '(GMT-03:00) Cayenne', value: 'America/Cayenne' },
-        { text: '(GMT-03:00) Cuiaba', value: 'America/Cuiaba' },
-        { text: '(GMT-03:00) Fortaleza', value: 'America/Fortaleza' },
-        { text: '(GMT-03:00) Godthab', value: 'America/Godthab' },
-        { text: '(GMT-03:00) Maceio', value: 'America/Maceio' },
-        { text: '(GMT-03:00) Miquelon', value: 'America/Miquelon' },
-        { text: '(GMT-03:00) Montevideo', value: 'America/Montevideo' },
-        { text: '(GMT-03:00) Palmer', value: 'Antarctica/Palmer' },
-        { text: '(GMT-03:00) Paramaribo', value: 'America/Paramaribo' },
-        { text: '(GMT-03:00) Punta Arenas', value: 'America/Punta_Arenas' },
-        { text: '(GMT-03:00) Recife', value: 'America/Recife' },
-        { text: '(GMT-03:00) Rothera', value: 'Antarctica/Rothera' },
-        { text: '(GMT-03:00) Salvador', value: 'America/Bahia' },
-        { text: '(GMT-03:00) Santiago', value: 'America/Santiago' },
-        { text: '(GMT-03:00) Stanley', value: 'Atlantic/Stanley' },
-        { text: '(GMT-02:00) Noronha', value: 'America/Noronha' },
-        { text: '(GMT-02:00) Sao Paulo', value: 'America/Sao_Paulo' },
-        { text: '(GMT-02:00) South Georgia', value: 'Atlantic/South_Georgia' },
-        { text: '(GMT-01:00) Azores', value: 'Atlantic/Azores' },
-        { text: '(GMT-01:00) Cape Verde', value: 'Atlantic/Cape_Verde' },
-        { text: '(GMT-01:00) Scoresbysund', value: 'America/Scoresbysund' },
-        { text: '(GMT+00:00) Abidjan', value: 'Africa/Abidjan' },
-        { text: '(GMT+00:00) Accra', value: 'Africa/Accra' },
-        { text: '(GMT+00:00) Bissau', value: 'Africa/Bissau' },
-        { text: '(GMT+00:00) Canary Islands', value: 'Atlantic/Canary' },
-        { text: '(GMT+00:00) Casablanca', value: 'Africa/Casablanca' },
-        { text: '(GMT+00:00) Danmarkshavn', value: 'America/Danmarkshavn' },
-        { text: '(GMT+00:00) Dublin', value: 'Europe/Dublin' },
-        { text: '(GMT+00:00) El Aaiun', value: 'Africa/El_Aaiun' },
-        { text: '(GMT+00:00) Faeroe', value: 'Atlantic/Faroe' },
-        { text: '(GMT+00:00) GMT (no daylight saving)', value: 'Etc/GMT' },
-        { text: '(GMT+00:00) Lisbon', value: 'Europe/Lisbon' },
-        { text: '(GMT+00:00) London', value: 'Europe/London' },
-        { text: '(GMT+00:00) Monrovia', value: 'Africa/Monrovia' },
-        { text: '(GMT+00:00) Reykjavik', value: 'Atlantic/Reykjavik' },
-        { text: '(GMT+01:00) Algiers', value: 'Africa/Algiers' },
-        { text: '(GMT+01:00) Amsterdam', value: 'Europe/Amsterdam' },
-        { text: '(GMT+01:00) Andorra', value: 'Europe/Andorra' },
-        { text: '(GMT+01:00) Berlin', value: 'Europe/Berlin' },
-        { text: '(GMT+01:00) Brussels', value: 'Europe/Brussels' },
-        { text: '(GMT+01:00) Budapest', value: 'Europe/Budapest' },
-        { text: '(GMT+01:00) Central European Time - Belgrade', value: 'Europe/Belgrade' },
-        { text: '(GMT+01:00) Central European Time - Prague', value: 'Europe/Prague' },
-        { text: '(GMT+01:00) Ceuta', value: 'Africa/Ceuta' },
-        { text: '(GMT+01:00) Copenhagen', value: 'Europe/Copenhagen' },
-        { text: '(GMT+01:00) Gibraltar', value: 'Europe/Gibraltar' },
-        { text: '(GMT+01:00) Lagos', value: 'Africa/Lagos' },
-        { text: '(GMT+01:00) Luxembourg', value: 'Europe/Luxembourg' },
-        { text: '(GMT+01:00) Madrid', value: 'Europe/Madrid' },
-        { text: '(GMT+01:00) Malta', value: 'Europe/Malta' },
-        { text: '(GMT+01:00) Monaco', value: 'Europe/Monaco' },
-        { text: '(GMT+01:00) Ndjamena', value: 'Africa/Ndjamena' },
-        { text: '(GMT+01:00) Oslo', value: 'Europe/Oslo' },
-        { text: '(GMT+01:00) Paris', value: 'Europe/Paris' },
-        { text: '(GMT+01:00) Rome', value: 'Europe/Rome' },
-        { text: '(GMT+01:00) Stockholm', value: 'Europe/Stockholm' },
-        { text: '(GMT+01:00) Tirane', value: 'Europe/Tirane' },
-        { text: '(GMT+01:00) Tunis', value: 'Africa/Tunis' },
-        { text: '(GMT+01:00) Vienna', value: 'Europe/Vienna' },
-        { text: '(GMT+01:00) Warsaw', value: 'Europe/Warsaw' },
-        { text: '(GMT+01:00) Zurich', value: 'Europe/Zurich' },
-        { text: '(GMT+02:00) Amman', value: 'Asia/Amman' },
-        { text: '(GMT+02:00) Athens', value: 'Europe/Athens' },
-        { text: '(GMT+02:00) Beirut', value: 'Asia/Beirut' },
-        { text: '(GMT+02:00) Bucharest', value: 'Europe/Bucharest' },
-        { text: '(GMT+02:00) Cairo', value: 'Africa/Cairo' },
-        { text: '(GMT+02:00) Chisinau', value: 'Europe/Chisinau' },
-        { text: '(GMT+02:00) Damascus', value: 'Asia/Damascus' },
-        { text: '(GMT+02:00) Gaza', value: 'Asia/Gaza' },
-        { text: '(GMT+02:00) Helsinki', value: 'Europe/Helsinki' },
-        { text: '(GMT+02:00) Jerusalem', value: 'Asia/Jerusalem' },
-        { text: '(GMT+02:00) Johannesburg', value: 'Africa/Johannesburg' },
-        { text: '(GMT+02:00) Khartoum', value: 'Africa/Khartoum' },
-        { text: '(GMT+02:00) Kyiv', value: 'Europe/Kyiv' },
-        { text: '(GMT+02:00) Maputo', value: 'Africa/Maputo' },
-        { text: '(GMT+02:00) Moscow-01 - Kaliningrad', value: 'Europe/Kaliningrad' },
-        { text: '(GMT+02:00) Nicosia', value: 'Asia/Nicosia' },
-        { text: '(GMT+02:00) Riga', value: 'Europe/Riga' },
-        { text: '(GMT+02:00) Sofia', value: 'Europe/Sofia' },
-        { text: '(GMT+02:00) Tallinn', value: 'Europe/Tallinn' },
-        { text: '(GMT+02:00) Tripoli', value: 'Africa/Tripoli' },
-        { text: '(GMT+02:00) Vilnius', value: 'Europe/Vilnius' },
-        { text: '(GMT+02:00) Windhoek', value: 'Africa/Windhoek' },
-        { text: '(GMT+03:00) Baghdad', value: 'Asia/Baghdad' },
-        { text: '(GMT+03:00) Istanbul', value: 'Europe/Istanbul' },
-        { text: '(GMT+03:00) Minsk', value: 'Europe/Minsk' },
-        { text: '(GMT+03:00) Moscow+00 - Moscow', value: 'Europe/Moscow' },
-        { text: '(GMT+03:00) Nairobi', value: 'Africa/Nairobi' },
-        { text: '(GMT+03:00) Qatar', value: 'Asia/Qatar' },
-        { text: '(GMT+03:00) Riyadh', value: 'Asia/Riyadh' },
-        { text: '(GMT+03:00) Syowa', value: 'Antarctica/Syowa' },
-        { text: '(GMT+03:30) Tehran', value: 'Asia/Tehran' },
-        { text: '(GMT+04:00) Baku', value: 'Asia/Baku' },
-        { text: '(GMT+04:00) Dubai', value: 'Asia/Dubai' },
-        { text: '(GMT+04:00) Mahe', value: 'Indian/Mahe' },
-        { text: '(GMT+04:00) Mauritius', value: 'Indian/Mauritius' },
-        { text: '(GMT+04:00) Moscow+01 - Samara', value: 'Europe/Samara' },
-        { text: '(GMT+04:00) Reunion', value: 'Indian/Reunion' },
-        { text: '(GMT+04:00) Tbilisi', value: 'Asia/Tbilisi' },
-        { text: '(GMT+04:00) Yerevan', value: 'Asia/Yerevan' },
-        { text: '(GMT+04:30) Kabul', value: 'Asia/Kabul' },
-        { text: '(GMT+05:00) Aqtau', value: 'Asia/Aqtau' },
-        { text: '(GMT+05:00) Aqtobe', value: 'Asia/Aqtobe' },
-        { text: '(GMT+05:00) Ashgabat', value: 'Asia/Ashgabat' },
-        { text: '(GMT+05:00) Dushanbe', value: 'Asia/Dushanbe' },
-        { text: '(GMT+05:00) Karachi', value: 'Asia/Karachi' },
-        { text: '(GMT+05:00) Kerguelen', value: 'Indian/Kerguelen' },
-        { text: '(GMT+05:00) Maldives', value: 'Indian/Maldives' },
-        { text: '(GMT+05:00) Mawson', value: 'Antarctica/Mawson' },
-        { text: '(GMT+05:00) Moscow+02 - Yekaterinburg', value: 'Asia/Yekaterinburg' },
-        { text: '(GMT+05:00) Tashkent', value: 'Asia/Tashkent' },
-        { text: '(GMT+05:30) Colombo', value: 'Asia/Colombo' },
-        { text: '(GMT+05:30) India Standard Time', value: 'Asia/Kolkata' },
-        { text: '(GMT+05:45) Kathmandu', value: 'Asia/Kathmandu' },
-        { text: '(GMT+06:00) Almaty', value: 'Asia/Almaty' },
-        { text: '(GMT+06:00) Bishkek', value: 'Asia/Bishkek' },
-        { text: '(GMT+06:00) Chagos', value: 'Indian/Chagos' },
-        { text: '(GMT+06:00) Dhaka', value: 'Asia/Dhaka' },
-        { text: '(GMT+06:00) Moscow+03 - Omsk', value: 'Asia/Omsk' },
-        { text: '(GMT+06:00) Thimphu', value: 'Asia/Thimphu' },
-        { text: '(GMT+06:00) Vostok', value: 'Antarctica/Vostok' },
-        { text: '(GMT+06:30) Cocos', value: 'Indian/Cocos' },
-        { text: '(GMT+06:30) Rangoon', value: 'Asia/Yangon' },
-        { text: '(GMT+07:00) Bangkok', value: 'Asia/Bangkok' },
-        { text: '(GMT+07:00) Christmas', value: 'Indian/Christmas' },
-        { text: '(GMT+07:00) Davis', value: 'Antarctica/Davis' },
-        { text: '(GMT+07:00) Hanoi', value: 'Asia/Saigon' },
-        { text: '(GMT+07:00) Hovd', value: 'Asia/Hovd' },
-        { text: '(GMT+07:00) Jakarta', value: 'Asia/Jakarta' },
-        { text: '(GMT+07:00) Moscow+04 - Krasnoyarsk', value: 'Asia/Krasnoyarsk' },
-        { text: '(GMT+08:00) Brunei', value: 'Asia/Brunei' },
-        { text: '(GMT+08:00) China Time - Beijing', value: 'Asia/Shanghai' },
-        { text: '(GMT+08:00) Choibalsan', value: 'Asia/Choibalsan' },
-        { text: '(GMT+08:00) Hong Kong', value: 'Asia/Hong_Kong' },
-        { text: '(GMT+08:00) Kuala Lumpur', value: 'Asia/Kuala_Lumpur' },
-        { text: '(GMT+08:00) Macau', value: 'Asia/Macau' },
-        { text: '(GMT+08:00) Makassar', value: 'Asia/Makassar' },
-        { text: '(GMT+08:00) Manila', value: 'Asia/Manila' },
-        { text: '(GMT+08:00) Moscow+05 - Irkutsk', value: 'Asia/Irkutsk' },
-        { text: '(GMT+08:00) Singapore', value: 'Asia/Singapore' },
-        { text: '(GMT+08:00) Taipei', value: 'Asia/Taipei' },
-        { text: '(GMT+08:00) Ulaanbaatar', value: 'Asia/Ulaanbaatar' },
-        { text: '(GMT+08:00) Western Time - Perth', value: 'Australia/Perth' },
-        { text: '(GMT+08:30) Pyongyang', value: 'Asia/Pyongyang' },
-        { text: '(GMT+09:00) Dili', value: 'Asia/Dili' },
-        { text: '(GMT+09:00) Jayapura', value: 'Asia/Jayapura' },
-        { text: '(GMT+09:00) Moscow+06 - Yakutsk', value: 'Asia/Yakutsk' },
-        { text: '(GMT+09:00) Palau', value: 'Pacific/Palau' },
-        { text: '(GMT+09:00) Seoul', value: 'Asia/Seoul' },
-        { text: '(GMT+09:00) Tokyo', value: 'Asia/Tokyo' },
-        { text: '(GMT+09:30) Central Time - Darwin', value: 'Australia/Darwin' },
-        { text: '(GMT+10:00) Dumont D\'Urville', value: 'Antarctica/DumontDUrville' },
-        { text: '(GMT+10:00) Eastern Time - Brisbane', value: 'Australia/Brisbane' },
-        { text: '(GMT+10:00) Guam', value: 'Pacific/Guam' },
-        { text: '(GMT+10:00) Moscow+07 - Vladivostok', value: 'Asia/Vladivostok' },
-        { text: '(GMT+10:00) Port Moresby', value: 'Pacific/Port_Moresby' },
-        { text: '(GMT+10:00) Truk', value: 'Pacific/Chuuk' },
-        { text: '(GMT+10:30) Central Time - Adelaide', value: 'Australia/Adelaide' },
-        { text: '(GMT+11:00) Casey', value: 'Antarctica/Casey' },
-        { text: '(GMT+11:00) Eastern Time - Hobart', value: 'Australia/Hobart' },
-        { text: '(GMT+11:00) Eastern Time - Melbourne, Sydney', value: 'Australia/Sydney' },
-        { text: '(GMT+11:00) Efate', value: 'Pacific/Efate' },
-        { text: '(GMT+11:00) Guadalcanal', value: 'Pacific/Guadalcanal' },
-        { text: '(GMT+11:00) Kosrae', value: 'Pacific/Kosrae' },
-        { text: '(GMT+11:00) Moscow+08 - Magadan', value: 'Asia/Magadan' },
-        { text: '(GMT+11:00) Norfolk', value: 'Pacific/Norfolk' },
-        { text: '(GMT+11:00) Noumea', value: 'Pacific/Noumea' },
-        { text: '(GMT+11:00) Ponape', value: 'Pacific/Pohnpei' },
-        { text: '(GMT+12:00) Funafuti', value: 'Pacific/Funafuti' },
-        { text: '(GMT+12:00) Kwajalein', value: 'Pacific/Kwajalein' },
-        { text: '(GMT+12:00) Majuro', value: 'Pacific/Majuro' },
-        { text: '(GMT+12:00) Moscow+09 - Petropavlovsk-Kamchatskiy', value: 'Asia/Kamchatka' },
-        { text: '(GMT+12:00) Nauru', value: 'Pacific/Nauru' },
-        { text: '(GMT+12:00) Tarawa', value: 'Pacific/Tarawa' },
-        { text: '(GMT+12:00) Wake', value: 'Pacific/Wake' },
-        { text: '(GMT+12:00) Wallis', value: 'Pacific/Wallis' },
-        { text: '(GMT+13:00) Auckland', value: 'Pacific/Auckland' },
-        { text: '(GMT+13:00) Enderbury', value: 'Pacific/Enderbury' },
-        { text: '(GMT+13:00) Fakaofo', value: 'Pacific/Fakaofo' },
-        { text: '(GMT+13:00) Fiji', value: 'Pacific/Fiji' },
-        { text: '(GMT+13:00) Tongatapu', value: 'Pacific/Tongatapu' },
-        { text: '(GMT+14:00) Apia', value: 'Pacific/Apia' },
-        { text: '(GMT+14:00) Kiritimati', value: 'Pacific/Kiritimati' }
-      ]
-    }
-  },
-  computed: {
-    dateFormats () {
-      return [
-        { text: this.$t('profile:localeDefault'), value: '' },
-        { text: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },
-        { text: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },
-        { text: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },
-        { text: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
-        { text: 'YYYY/MM/DD', value: 'YYYY/MM/DD' }
-      ]
-    },
-    appearances () {
-      return [
-        { text: this.$t('profile:appearanceDefault'), value: '' },
-        { text: this.$t('profile:appearanceLight'), value: 'light' },
-        { text: this.$t('profile:appearanceDark'), value: 'dark' }
-      ]
-    },
-    currentAppearance () {
-      return _.get(_.find(this.appearances, ['value', this.user.appearance]), 'text', false) || this.$t('profile:appearanceDefault')
-    },
-    pictureUrl: get('user/pictureUrl'),
-    picture () {
-      if (this.pictureUrl && this.pictureUrl.length > 1) {
-        return {
-          kind: 'image',
-          url: this.pictureUrl
-        }
-      } else {
-        const nameParts = this.user.name.toUpperCase().split(' ')
-        let initials = _.head(nameParts).charAt(0)
-        if (nameParts.length > 1) {
-          initials += _.last(nameParts).charAt(0)
-        }
-        return {
-          kind: 'initials',
-          initials
-        }
-      }
-    }
-  },
-  watch: {
-    'user.appearance': (newValue, oldValue) => {
-      if (newValue === '') {
-        WIKI.$vuetify.theme.dark = siteConfig.darkMode
-      } else {
-        WIKI.$vuetify.theme.dark = (newValue === 'dark')
-      }
-    },
-    'user.dateFormat': (newValue, oldValue) => {
-      if (newValue === '') {
-        WIKI.$moment.updateLocale(WIKI.$moment.locale(), null)
-      } else {
-        WIKI.$moment.updateLocale(WIKI.$moment.locale(), {
-          longDateFormat: {
-            'L': newValue
-          }
-        })
-      }
-    },
-    'user.timezone': (newValue, oldValue) => {
-      if (newValue === '') {
-        WIKI.$moment.tz.setDefault()
-      } else {
-        WIKI.$moment.tz.setDefault(newValue)
-      }
-    }
-  },
-  methods: {
-    /**
-     * Focus an input after delay
-     */
-    focusField (ipt) {
-      this.$nextTick(() => {
-        _.delay(() => {
-          this.$refs[ipt].focus()
-        }, 200)
-      })
-    },
-    /**
-     * Save User Profile
-     */
-    async saveProfile () {
-      this.saveLoading = true
-      this.$store.commit(`loadingStart`, 'profile-save')
-
-      try {
-        const respRaw = await this.$apollo.mutate({
-          mutation: gql`
-            mutation ($name: String!, $location: String!, $jobTitle: String!, $timezone: String!, $dateFormat: String!, $appearance: String!) {
-              users {
-                updateProfile(name: $name, location: $location, jobTitle: $jobTitle, timezone: $timezone, dateFormat: $dateFormat, appearance: $appearance) {
-                  responseResult {
-                    succeeded
-                    errorCode
-                    slug
-                    message
-                  }
-                  jwt
-                }
-              }
-            }
-          `,
-          variables: {
-            name: this.user.name,
-            location: this.user.location,
-            jobTitle: this.user.jobTitle,
-            timezone: this.user.timezone,
-            dateFormat: this.user.dateFormat,
-            appearance: this.user.appearance
-          }
-        })
-        const resp = _.get(respRaw, 'data.users.updateProfile.responseResult', {})
-        if (resp.succeeded) {
-          Cookies.set('jwt', _.get(respRaw, 'data.users.updateProfile.jwt', ''), { expires: 365 })
-          this.$store.set('user/name', this.user.name)
-          this.$store.commit('showNotification', {
-            message: this.$t('profile:save.success'),
-            style: 'success',
-            icon: 'check'
-          })
-        } else {
-          throw new Error(resp.message)
-        }
-      } catch (err) {
-        this.$store.commit('pushGraphError', err)
-      }
-
-      this.$store.commit(`loadingStop`, 'profile-save')
-      this.saveLoading = false
-    },
-    /**
-     * Change Password
-     */
-    async changePassword () {
-      const validation = validate({
-        current: this.currentPass,
-        password: this.newPass,
-        verifyPassword: this.verifyPass
-      }, {
-        current: {
-          presence: {
-            message: this.$t('auth:missingPassword'),
-            allowEmpty: false
-          },
-          length: {
-            minimum: 6,
-            tooShort: this.$t('auth:passwordTooShort')
-          }
-        },
-        password: {
-          presence: {
-            message: this.$t('auth:missingPassword'),
-            allowEmpty: false
-          },
-          length: {
-            minimum: 6,
-            tooShort: this.$t('auth:passwordTooShort')
-          }
-        },
-        verifyPassword: {
-          equality: {
-            attribute: 'password',
-            message: this.$t('auth:passwordNotMatch')
-          }
-        }
-      }, { fullMessages: false })
-
-      if (validation) {
-        if (validation.current) {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: validation.current[0],
-            icon: 'warning'
-          })
-          this.$refs.iptCurrentPass.focus()
-        } else if (validation.password) {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: validation.password[0],
-            icon: 'warning'
-          })
-          this.$refs.iptNewPass.focus()
-        } else if (validation.verifyPassword) {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: validation.verifyPassword[0],
-            icon: 'warning'
-          })
-          this.$refs.iptVerifyPass.focus()
-        }
-      } else {
-        this.changePassLoading = true
-        this.$store.commit(`loadingStart`, 'profile-changepassword')
-
-        try {
-          const respRaw = await this.$apollo.mutate({
-            mutation: gql`
-              mutation ($current: String!, $new: String!) {
-                users {
-                  changePassword(current: $current, new: $new) {
-                    responseResult {
-                      succeeded
-                      errorCode
-                      slug
-                      message
-                    }
-                    jwt
-                  }
-                }
-              }
-            `,
-            variables: {
-              current: this.currentPass,
-              new: this.newPass
-            }
-          })
-          const resp = _.get(respRaw, 'data.users.changePassword.responseResult', {})
-          if (resp.succeeded) {
-            this.currentPass = ''
-            this.newPass = ''
-            this.verifyPass = ''
-            Cookies.set('jwt', _.get(respRaw, 'data.users.changePassword.jwt', ''), { expires: 365 })
-            this.$store.commit('showNotification', {
-              message: this.$t('profile:auth.changePassSuccess'),
-              style: 'success',
-              icon: 'check'
-            })
-          } else {
-            throw new Error(resp.message)
-          }
-        } catch (err) {
-          this.$store.commit('pushGraphError', err)
-        }
-
-        this.$store.commit(`loadingStop`, 'profile-changepassword')
-        this.changePassLoading = false
-      }
-    }
-  },
-  apollo: {
-    user: {
-      query: gql`
-        {
-          users {
-            profile {
-              id
-              name
-              email
-              providerKey
-              providerName
-              isSystem
-              isVerified
-              location
-              jobTitle
-              timezone
-              dateFormat
-              appearance
-              createdAt
-              updatedAt
-              lastLoginAt
-              groups
-              pagesTotal
-            }
-          }
-        }
-      `,
-      fetchPolicy: 'network-only',
-      update: (data) => _.cloneDeep(data.users.profile),
-      watchLoading (isLoading) {
-        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'profile-refresh')
-      }
-    }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 306
client/components/register.vue

@@ -1,306 +0,0 @@
-<template lang="pug">
-  v-app
-    .register
-      v-container(grid-list-lg)
-        v-layout(row, wrap)
-          v-flex(
-            xs12
-            offset-sm1, sm10
-            offset-md2, md8
-            offset-lg3, lg6
-            offset-xl4, xl4
-            )
-            transition(name='fadeUp')
-              v-card.elevation-5.md2(v-show='isShown')
-                v-toolbar(color='indigo', flat, dense, dark)
-                  v-spacer
-                  .subheading {{ $t('auth:registerTitle') }}
-                  v-spacer
-                v-card-text.text-center
-                  h1.display-1.indigo--text.py-2 {{ siteTitle }}
-                  .body-2 {{ $t('auth:registerSubTitle') }}
-                  v-text-field.md2.mt-3(
-                    solo
-                    flat
-                    prepend-icon='mdi-email'
-                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
-                    hide-details
-                    ref='iptEmail'
-                    v-model='email'
-                    :placeholder='$t("auth:fields.email")'
-                    color='indigo'
-                    )
-                  v-text-field.md2.mt-2(
-                    solo
-                    flat
-                    prepend-icon='mdi-form-textbox-password'
-                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
-                    ref='iptPassword'
-                    v-model='password'
-                    :append-icon='hidePassword ? "mdi-eye-off" : "mdi-eye"'
-                    @click:append='() => (hidePassword = !hidePassword)'
-                    :type='hidePassword ? "password" : "text"'
-                    :placeholder='$t("auth:fields.password")'
-                    color='indigo'
-                    loading
-                    counter='255'
-                    )
-                    password-strength(slot='progress', v-model='password')
-                  v-text-field.md2.mt-2(
-                    solo
-                    flat
-                    prepend-icon='mdi-form-textbox-password'
-                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
-                    hide-details
-                    ref='iptVerifyPassword'
-                    v-model='verifyPassword'
-                    @click:append='() => (hidePassword = !hidePassword)'
-                    type='password'
-                    :placeholder='$t("auth:fields.verifyPassword")'
-                    color='indigo'
-                  )
-                  v-text-field.md2.mt-2(
-                    solo
-                    flat
-                    prepend-icon='mdi-account'
-                    :background-color='$vuetify.theme.dark ? `grey darken-3` : `grey lighten-4`'
-                    ref='iptName'
-                    v-model='name'
-                    :placeholder='$t("auth:fields.name")'
-                    @keyup.enter='register'
-                    color='indigo'
-                    counter='255'
-                    )
-                v-card-actions.pb-4
-                  v-spacer
-                  v-btn.md2(
-                    width='100%'
-                    max-width='250px'
-                    large
-                    dark
-                    color='indigo'
-                    @click='register'
-                    rounded
-                    :loading='isLoading'
-                    ) {{ $t('auth:actions.register') }}
-                  v-spacer
-                v-divider
-                v-card-actions.py-3.grey(:class='$vuetify.theme.dark ? `darken-4-l1` : `lighten-4`')
-                  v-spacer
-                  i18next.caption(path='auth:switchToLogin.text', tag='div')
-                    a.caption(href='/login', place='link') {{ $t('auth:switchToLogin.link') }}
-                  v-spacer
-
-    loader(v-model='isLoading', :mode='loaderMode', :icon='loaderIcon', :color='loaderColor', :title='loaderTitle', :subtitle='loaderSubtitle')
-    nav-footer(color='grey darken-5', dark-color='grey darken-5')
-    notify(style='padding-top: 64px;')
-</template>
-
-<script>
-/* global siteConfig */
-
-import _ from 'lodash'
-import validate from 'validate.js'
-import PasswordStrength from './common/password-strength.vue'
-
-import registerMutation from 'gql/register/register-mutation-create.gql'
-
-export default {
-  i18nOptions: { namespaces: 'auth' },
-  components: {
-    PasswordStrength
-  },
-  data () {
-    return {
-      email: '',
-      password: '',
-      verifyPassword: '',
-      name: '',
-      hidePassword: true,
-      isLoading: false,
-      isShown: false,
-      loaderColor: 'grey darken-4',
-      loaderTitle: 'Working...',
-      loaderSubtitle: 'Please wait',
-      loaderMode: 'icon',
-      loaderIcon: 'checkmark'
-    }
-  },
-  computed: {
-    siteTitle () {
-      return siteConfig.title
-    }
-  },
-  mounted () {
-    this.isShown = true
-    this.$nextTick(() => {
-      this.$refs.iptEmail.focus()
-    })
-  },
-  methods: {
-    /**
-     * REGISTER
-     */
-    async register () {
-      const validation = validate({
-        email: this.email,
-        password: this.password,
-        verifyPassword: this.verifyPassword,
-        name: this.name
-      }, {
-        email: {
-          presence: {
-            message: this.$t('auth:missingEmail'),
-            allowEmpty: false
-          },
-          email: {
-            message: this.$t('auth:invalidEmail')
-          }
-        },
-        password: {
-          presence: {
-            message: this.$t('auth:missingPassword'),
-            allowEmpty: false
-          },
-          length: {
-            minimum: 6,
-            tooShort: this.$t('auth:passwordTooShort')
-          }
-        },
-        verifyPassword: {
-          equality: {
-            attribute: 'password',
-            message: this.$t('auth:passwordNotMatch')
-          }
-        },
-        name: {
-          presence: {
-            message: this.$t('auth:missingName'),
-            allowEmpty: false
-          },
-          length: {
-            minimum: 2,
-            maximum: 255,
-            tooShort: this.$t('auth:nameTooShort'),
-            tooLong: this.$t('auth:nameTooLong')
-          }
-        }
-      }, { fullMessages: false })
-
-      if (validation) {
-        if (validation.email) {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: validation.email[0],
-            icon: 'warning'
-          })
-          this.$refs.iptEmail.focus()
-        } else if (validation.password) {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: validation.password[0],
-            icon: 'warning'
-          })
-          this.$refs.iptPassword.focus()
-        } else if (validation.verifyPassword) {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: validation.verifyPassword[0],
-            icon: 'warning'
-          })
-          this.$refs.iptVerifyPassword.focus()
-        } else {
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: validation.name[0],
-            icon: 'warning'
-          })
-          this.$refs.iptName.focus()
-        }
-      } else {
-        this.loaderColor = 'grey darken-4'
-        this.loaderTitle = this.$t('auth:registering')
-        this.loaderSubtitle = this.$t(`auth:pleaseWait`)
-        this.loaderMode = 'loading'
-        this.isLoading = true
-        try {
-          let resp = await this.$apollo.mutate({
-            mutation: registerMutation,
-            variables: {
-              email: this.email,
-              password: this.password,
-              name: this.name
-            }
-          })
-          if (_.has(resp, 'data.authentication.register')) {
-            let respObj = _.get(resp, 'data.authentication.register', {})
-            if (respObj.responseResult.succeeded === true) {
-              this.loaderColor = 'grey darken-4'
-              this.loaderTitle = this.$t('auth:registerSuccess')
-              this.loaderSubtitle = this.$t(`auth:registerCheckEmail`)
-              this.loaderMode = 'icon'
-              this.isShown = false
-            } else {
-              throw new Error(respObj.responseResult.message)
-            }
-          } else {
-            throw new Error(this.$t('auth:genericError'))
-          }
-        } catch (err) {
-          console.error(err)
-          this.$store.commit('showNotification', {
-            style: 'red',
-            message: err.message,
-            icon: 'warning'
-          })
-          this.isLoading = false
-        }
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-  .register {
-    background-color: mc('indigo', '900');
-    background-image: url('../static/svg/motif-blocks.svg');
-    background-repeat: repeat;
-    background-size: 200px;
-    width: 100%;
-    height: 100%;
-    animation: loginBgReveal 20s linear infinite;
-
-    @include keyframes(loginBgReveal) {
-      0% {
-        background-position-x: 0;
-      }
-      100% {
-        background-position-x: 800px;
-      }
-    }
-
-    &::before {
-      content: '';
-      position: absolute;
-      background-image: url('../static/svg/motif-overlay.svg');
-      background-attachment: fixed;
-      background-size: cover;
-      opacity: .5;
-      top: 0;
-      left: 0;
-      width: 100vw;
-      height: 100vh;
-    }
-
-    > .container {
-      height: 100%;
-      align-items: center;
-      display: flex;
-    }
-
-    .v-text-field.centered input {
-      text-align: center;
-    }
-  }
-</style>

+ 0 - 273
client/components/setup.vue

@@ -1,273 +0,0 @@
-<template lang="pug">
-  v-app.setup
-    v-content
-      v-container
-        v-layout
-          v-flex(xs12, lg6, offset-lg3)
-            v-card.elevation-20.radius-7.animated.fadeInUp
-              v-alert(v-if='isDevMode', tile, dark, color='red darken-3', icon='mdi-alert', prominent)
-                .body-2 You are running an unstable, unreleased development version. This code base is #[strong NOT] for production use!
-                .body-2.mt-3 Cloning the dev branch directly from GitHub is #[strong NOT] the proper way to install Wiki.js!
-                .body-2 Read the #[a(href='https://docs.requarks.io/install', style='color: #FFF;') documentation] on correctly installing the latest stable version.
-              .text-center
-                img.setup-logo.animated.fadeInUp.wait-p2s(src='/_assets/svg/logo-wikijs-full.svg', alt='Wiki.js Logo')
-              v-alert(v-model='error', type='error', icon='mdi-alert', tile, dismissible) {{ errorMessage }}
-              v-alert(v-if='!error', tile, color='blue lighten-5', :value='true')
-                v-icon.mr-3(color='blue') mdi-package-variant
-                span.blue--text You are about to install Wiki.js #[strong {{wikiVersion}}].
-              v-card-text
-                .overline.pl-3 Administrator Account
-                v-container.pa-3.mt-3(grid-list-xl)
-                  v-layout(row, wrap)
-                    v-flex(xs12)
-                      v-text-field(
-                        outlined
-                        v-model='conf.adminEmail',
-                        label='Administrator Email',
-                        hint='The email address of the administrator account.',
-                        persistent-hint
-                        required
-                        ref='adminEmailInput'
-                      )
-                    v-flex(xs6)
-                      v-text-field(
-                        outlined
-                        ref='adminPassword',
-                        counter='255'
-                        v-model='conf.adminPassword',
-                        label='Password',
-                        :append-icon="pwdMode ? 'mdi-eye-off' : 'mdi-eye'"
-                        @click:append="() => (pwdMode = !pwdMode)"
-                        :type="pwdMode ? 'password' : 'text'"
-                        hint='At least 8 characters long.',
-                        persistent-hint
-                      )
-                    v-flex(xs6)
-                      v-text-field(
-                        outlined
-                        ref='adminPasswordConfirm',
-                        counter='255'
-                        v-model='conf.adminPasswordConfirm',
-                        label='Confirm Password',
-                        :append-icon="pwdConfirmMode ? 'mdi-eye-off' : 'mdi-eye'"
-                        @click:append="() => (pwdConfirmMode = !pwdConfirmMode)"
-                        :type="pwdConfirmMode ? 'password' : 'text'"
-                        hint='Verify your password again.',
-                        persistent-hint
-                      )
-                v-divider.mb-4
-                .overline.pl-3.mb-5 Site URL
-                v-text-field.mb-4.mx-3(
-                  outlined
-                  ref='adminSiteUrl',
-                  v-model='conf.siteUrl',
-                  label='Site URL',
-                  hint='Full URL to your wiki, without the trailing slash (e.g. https://wiki.example.com). This should be the public facing URL, not the internal one if using a reverse-proxy.',
-                  persistent-hint
-                  @keyup.enter='install'
-                )
-                v-divider.mb-4
-                .overline.pl-3.mb-3 Telemetry
-                v-switch.ml-3(
-                  inset
-                  color='primary',
-                  v-model='conf.telemetry',
-                  label='Allow Telemetry',
-                  persistent-hint,
-                  hint='Help Wiki.js developers improve this app with anonymized telemetry.'
-                )
-                a.pl-3(style='font-size: 12px; letter-spacing: initial;', href='https://docs.requarks.io/telemetry', target='_blank') Learn more
-              v-divider.mt-2
-              v-card-actions
-                v-btn(color='primary', @click='install', :disabled='loading', x-large, depressed, block)
-                  v-icon(left) mdi-check
-                  span Install
-
-    v-dialog(v-model='loading', width='450', persistent)
-      v-card(color='primary', dark).radius-7
-        v-card-text.text-center.py-5
-          .py-3(style='width: 64px; display:inline-block;')
-            breeding-rhombus-spinner(
-              :animation-duration='2000'
-              :size='64'
-              color='#FFF'
-              )
-          template(v-if='!success')
-            .subtitle-1.white--text Finalizing your installation...
-            .caption Just a moment
-          template(v-else)
-            .subtitle-1.white--text Installation complete!
-            .caption Redirecting...
-</template>
-
-<script>
-import _ from 'lodash'
-import validate from 'validate.js'
-import { BreedingRhombusSpinner } from 'epic-spinners'
-import confetti from 'canvas-confetti'
-
-/* global siteConfig */
-
-export default {
-  components: {
-    BreedingRhombusSpinner
-  },
-  props: {
-    wikiVersion: {
-      type: String,
-      required: true
-    }
-  },
-  data() {
-    return {
-      loading: false,
-      success: false,
-      error: false,
-      errorMessage: '',
-      conf: {
-        adminEmail: '',
-        adminPassword: '',
-        adminPasswordConfirm: '',
-        siteUrl: 'https://wiki.yourdomain.com',
-        telemetry: true
-      },
-      pwdMode: true,
-      pwdConfirmMode: true,
-      isDevMode: false
-    }
-  },
-  mounted() {
-    _.delay(() => {
-      this.$refs.adminEmailInput.focus()
-    }, 500)
-    this.isDevMode = siteConfig.devMode === true
-  },
-  methods: {
-    async install () {
-      this.error = false
-
-      const validationResults = validate(this.conf, {
-        adminEmail: {
-          presence: {
-            allowEmpty: false
-          },
-          email: true
-        },
-        adminPassword: {
-          presence: {
-            allowEmpty: false
-          },
-          length: {
-            minimum: 6,
-            maximum: 255
-          }
-        },
-        adminPasswordConfirm: {
-          equality: 'adminPassword'
-        },
-        siteUrl: {
-          presence: {
-            allowEmpty: false
-          },
-          url: {
-            schemes: ['http', 'https'],
-            allowLocal: true,
-            allowDataUrl: false
-          },
-          format: {
-            pattern: '^(?!.*/$).*$',
-            flags: 'i',
-            message: 'must not have a trailing slash'
-          }
-        }
-      }, {
-        format: 'flat'
-      })
-      if (validationResults) {
-        this.error = true
-        this.errorMessage = validationResults[0]
-        this.$forceUpdate()
-        return
-      }
-
-      this.loading = true
-      this.success = false
-      this.$forceUpdate()
-
-      _.delay(async () => {
-        try {
-          const resp = await fetch('/finalize', {
-            method: 'POST',
-            cache: 'no-cache',
-            headers: {
-              'Content-Type': 'application/json'
-            },
-            body: JSON.stringify(this.conf)
-          }).then(res => res.json())
-
-          if (resp.ok === true) {
-            _.delay(() => {
-              confetti({
-                particleCount: 100,
-                spread: 70,
-                zIndex: 100000
-              })
-              this.success = true
-              _.delay(() => {
-                window.location.assign('/login')
-              }, 3000)
-            }, 10000)
-          } else {
-            this.error = true
-            this.errorMessage = resp.error
-            this.loading = false
-          }
-        } catch (err) {
-          window.alert(err.message)
-        }
-      }, 1000)
-    }
-  }
-}
-
-</script>
-
-<style lang='scss'>
-.setup {
-  .v-application--wrap {
-    padding-top: 10vh;
-    background-color: #111;
-    background-image: linear-gradient(45deg, mc('blue', '100'), mc('blue', '700'), mc('indigo', '900'));
-    background-blend-mode: exclusion;
-
-    &::before {
-      content: '';
-      position: absolute;
-      left: 0;
-      top: 0;
-      width: 100%;
-      height: 100vh;
-      z-index: 0;
-      background-color: transparent;
-      background-image: url(../static/svg/motif-grid.svg) !important;
-      background-size: 100px;
-      background-repeat: repeat;
-      animation: bg-anim 100s linear infinite;
-    }
-  }
-
-  @keyframes bg-anim {
-    0% {
-      background-position: 0 0;
-    }
-    100% {
-      background-position: 100% 100%;
-    }
-  }
-
-  &-logo {
-    width: 400px;
-    margin: 2rem 0 2rem 0;
-  }
-}
-</style>

+ 3 - 3
client/components/tags.vue

@@ -89,7 +89,7 @@
           v-btn(text, height='40'): v-icon(size='20') mdi-chevron-double-down
       v-divider
       .text-center.pt-10(v-if='selection.length < 1')
-        img(src='/_assets/svg/icon-price-tag.svg')
+        img(src='/_assets-legacy/svg/icon-price-tag.svg')
         .subtitle-2.grey--text {{$t('tags:selectOneMoreTagsHint')}}
       .px-5.py-2(v-else)
         v-data-iterator(
@@ -112,11 +112,11 @@
               .subtitle-2.grey--text.mt-5 {{$t('tags:retrievingResultsLoading')}}
           template(v-slot:no-data)
             .text-center.pt-10
-              img(src='/_assets/svg/icon-info.svg')
+              img(src='/_assets-legacy/svg/icon-info.svg')
               .subtitle-2.grey--text {{$t('tags:noResults')}}
           template(v-slot:no-results)
             .text-center.pt-10
-              img(src='/_assets/svg/icon-info.svg')
+              img(src='/_assets-legacy/svg/icon-info.svg')
               .subtitle-2.grey--text {{$t('tags:noResultsWithFilter')}}
           template(v-slot:default='props')
             v-row(align='stretch')

+ 0 - 35
client/components/welcome.vue

@@ -1,35 +0,0 @@
-<template lang='pug'>
-  v-app
-    .onboarding
-      .onboarding-content
-        img.animated.fadeIn(src='/_assets-legacy/svg/logo-wikijs.svg', alt='Wiki.js')
-        .headline.animated.fadeInUp {{ $t('welcome.title') }}
-        .subtitle-1.mt-3.animated.fadeInUp.wait-p1s {{ $t('welcome.subtitle') }}
-        div
-          v-btn.mt-5.mx-3.animated.fadeInUp.wait-p2s(color='primary', :href='`/e/` + locale + `/home`', x-large)
-            v-icon(left) mdi-plus
-            span {{ $t('welcome.createhome') }}
-          v-btn.mt-5.mx-3.animated.fadeInUp.wait-p3s(color='primary', href='/a', x-large)
-            v-icon(left) mdi-view-dashboard
-            span {{ $t('welcome.goadmin') }}
-
-</template>
-
-<script>
-
-export default {
-  props: {
-    locale: {
-      type: String,
-      default: 'en'
-    }
-  },
-  data() {
-    return { }
-  }
-}
-</script>
-
-<style lang='scss'>
-
-</style>

+ 0 - 12
client/graph/admin/analytics/analytics-mutation-save-providers.gql

@@ -1,12 +0,0 @@
-mutation($providers: [AnalyticsProviderInput]!) {
-  analytics {
-    updateProviders(providers: $providers) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 17
client/graph/admin/analytics/analytics-query-providers.gql

@@ -1,17 +0,0 @@
-query {
-  analytics {
-    providers {
-      isEnabled
-      key
-      title
-      description
-      isAvailable
-      logo
-      website
-      config {
-        key
-        value
-      }
-    }
-  }
-}

+ 0 - 8
client/graph/admin/auth/auth-query-groups.gql

@@ -1,8 +0,0 @@
-query {
-  groups {
-    list {
-      id
-      name
-    }
-  }
-}

+ 0 - 7
client/graph/admin/auth/auth-query-host.gql

@@ -1,7 +0,0 @@
-{
-  site {
-    config {
-      host
-    }
-  }
-}

+ 0 - 21
client/graph/admin/auth/auth-query-strategies.gql

@@ -1,21 +0,0 @@
-query {
-  authentication {
-    strategies {
-      isEnabled
-      key
-      title
-      description
-      isAvailable
-      useForm
-      logo
-      website
-      config {
-        key
-        value
-      }
-      selfRegistration
-      domainWhitelist
-      autoEnrollGroups
-    }
-  }
-}

+ 0 - 17
client/graph/admin/contribute/contribute-query-contributors.gql

@@ -1,17 +0,0 @@
-query {
-  contribute {
-    contributors {
-      company
-      currency
-      description
-      id
-      image
-      name
-      profile
-      tier
-      totalDonated
-      twitter
-      website
-    }
-  }
-}

+ 0 - 12
client/graph/admin/dashboard/dashboard-query-stats.gql

@@ -1,12 +0,0 @@
-query {
-  system {
-    info {
-      currentVersion
-      latestVersion
-      groupsTotal
-      pagesTotal
-      usersTotal
-      tagsTotal
-    }
-  }
-}

+ 0 - 16
client/graph/admin/dev/dev-mutation-save-flags.gql

@@ -1,16 +0,0 @@
-mutation (
-  $flags: [SystemFlagInput]!
-) {
-  system {
-    updateFlags(
-      flags: $flags
-    ) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 8
client/graph/admin/dev/dev-query-flags.gql

@@ -1,8 +0,0 @@
-{
-  system {
-    flags {
-      key
-      value
-    }
-  }
-}

+ 0 - 12
client/graph/admin/groups/groups-mutation-assign.gql

@@ -1,12 +0,0 @@
-mutation ($groupId: Int!, $userId: Int!) {
-  groups {
-    assignUser(groupId: $groupId, userId: $userId) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 18
client/graph/admin/groups/groups-mutation-create.gql

@@ -1,18 +0,0 @@
-mutation ($name: String!) {
-  groups {
-    create(name: $name) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-      group {
-        id
-        name
-        createdAt
-        updatedAt
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/groups/groups-mutation-unassign.gql

@@ -1,12 +0,0 @@
-mutation ($groupId: Int!, $userId: Int!) {
-  groups {
-    unassignUser(groupId: $groupId, userId: $userId) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/groups/groups-query-list.gql

@@ -1,12 +0,0 @@
-query {
-  groups {
-    list {
-      id
-      name
-      isSystem
-      userCount
-      createdAt
-      updatedAt
-    }
-  }
-}

+ 0 - 12
client/graph/admin/locale/locale-mutation-download.gql

@@ -1,12 +0,0 @@
-mutation($locale: String!) {
-  localization {
-    downloadLocale(locale: $locale) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/locale/locale-mutation-save.gql

@@ -1,12 +0,0 @@
-mutation($locale: String!, $autoUpdate: Boolean!, $namespacing: Boolean!, $namespaces: [String]!) {
-  localization {
-    updateLocale(locale: $locale, autoUpdate: $autoUpdate, namespacing: $namespacing, namespaces: $namespaces) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 21
client/graph/admin/locale/locale-query-list.gql

@@ -1,21 +0,0 @@
-{
-  localization {
-    locales {
-      availability
-      code
-      createdAt
-      isInstalled
-      installDate
-      isRTL
-      name
-      nativeName
-      updatedAt
-    }
-    config {
-      locale
-      autoUpdate
-      namespacing
-      namespaces
-    }
-  }
-}

+ 0 - 12
client/graph/admin/logging/logging-mutation-save-loggers.gql

@@ -1,12 +0,0 @@
-mutation($loggers: [LoggerInput]) {
-  logging {
-    updateLoggers(loggers: $loggers) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 17
client/graph/admin/logging/logging-query-loggers.gql

@@ -1,17 +0,0 @@
-query {
-  logging {
-    loggers(orderBy: "title ASC") {
-      isEnabled
-      key
-      title
-      description
-      logo
-      website
-      level
-      config {
-        key
-        value
-      }
-    }
-  }
-}

+ 0 - 7
client/graph/admin/logging/logging-subscription-livetrail.gql

@@ -1,7 +0,0 @@
-subscription {
-  loggingLiveTrail {
-    level
-    output
-    timestamp
-  }
-}

+ 0 - 38
client/graph/admin/mail/mail-mutation-save-config.gql

@@ -1,38 +0,0 @@
-mutation (
-  $senderName: String!,
-  $senderEmail: String!,
-  $host: String!,
-  $port: Int!,
-  $secure: Boolean!,
-  $verifySSL: Boolean!,
-  $user: String!,
-  $pass: String!,
-  $useDKIM: Boolean!,
-  $dkimDomainName: String!,
-  $dkimKeySelector: String!,
-  $dkimPrivateKey: String!
-) {
-  mail {
-    updateConfig(
-      senderName: $senderName,
-      senderEmail: $senderEmail,
-      host: $host,
-      port: $port,
-      secure: $secure,
-      verifySSL: $verifySSL,
-      user: $user,
-      pass: $pass,
-      useDKIM: $useDKIM,
-      dkimDomainName: $dkimDomainName,
-      dkimKeySelector: $dkimKeySelector,
-      dkimPrivateKey: $dkimPrivateKey
-    ) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/mail/mail-mutation-sendtest.gql

@@ -1,12 +0,0 @@
-mutation ($recipientEmail: String!) {
-  mail {
-    sendTest(recipientEmail: $recipientEmail) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 18
client/graph/admin/mail/mail-query-config.gql

@@ -1,18 +0,0 @@
-{
-  mail {
-    config {
-      senderName
-      senderEmail
-      host
-      port
-      secure
-      verifySSL
-      user
-      pass
-      useDKIM
-      dkimDomainName
-      dkimKeySelector
-      dkimPrivateKey
-    }
-  }
-}

+ 0 - 17
client/graph/admin/pages/pages-query-list.gql

@@ -1,17 +0,0 @@
-query {
-  pages {
-    list {
-      id
-      locale
-      path
-      title
-      description
-      contentType
-      isPublished
-      isPrivate
-      privateNS
-      createdAt
-      updatedAt
-    }
-  }
-}

+ 0 - 27
client/graph/admin/pages/pages-query-single.gql

@@ -1,27 +0,0 @@
-query($id: Int!) {
-  pages {
-    single(id:$id) {
-      id
-      path
-      hash
-      title
-      description
-      isPrivate
-      isPublished
-      privateNS
-      publishStartDate
-      publishEndDate
-      contentType
-      createdAt
-      updatedAt
-      editor
-      locale
-      authorId
-      authorName
-      authorEmail
-      creatorId
-      creatorName
-      creatorEmail
-    }
-  }
-}

+ 0 - 12
client/graph/admin/rendering/rendering-mutation-save-renderers.gql

@@ -1,12 +0,0 @@
-mutation($renderers: [RendererInput]) {
-  rendering {
-    updateRenderers(renderers: $renderers) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 18
client/graph/admin/rendering/rendering-query-renderers.gql

@@ -1,18 +0,0 @@
-{
-  rendering {
-    renderers {
-      isEnabled
-      key
-      title
-      description
-      icon
-      dependsOn
-      input
-      output
-      config {
-        key
-        value
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/search/search-mutation-rebuild-index.gql

@@ -1,12 +0,0 @@
-mutation {
-  search {
-    rebuildIndex {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/search/search-mutation-save-engines.gql

@@ -1,12 +0,0 @@
-mutation($engines: [SearchEngineInput]) {
-  search {
-    updateSearchEngines(engines: $engines) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 17
client/graph/admin/search/search-query-engines.gql

@@ -1,17 +0,0 @@
-query {
-  search {
-    searchEngines(orderBy: "title") {
-      isEnabled
-      key
-      title
-      description
-      logo
-      website
-      isAvailable
-      config {
-        key
-        value
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/storage/storage-mutation-executeaction.gql

@@ -1,12 +0,0 @@
-mutation($targetKey: String!, $handler: String!) {
-  storage {
-    executeAction(targetKey: $targetKey, handler: $handler) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/storage/storage-mutation-save-targets.gql

@@ -1,12 +0,0 @@
-mutation($targets: [StorageTargetInput]!) {
-  storage {
-    updateTargets(targets: $targets) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 11
client/graph/admin/storage/storage-query-status.gql

@@ -1,11 +0,0 @@
-query {
-  storage {
-    status {
-      key
-      title
-      status
-      message
-      lastAttempt
-    }
-  }
-}

+ 0 - 27
client/graph/admin/storage/storage-query-targets.gql

@@ -1,27 +0,0 @@
-query {
-  storage {
-    targets {
-      isAvailable
-      isEnabled
-      key
-      title
-      description
-      logo
-      website
-      supportedModes
-      mode
-      hasSchedule
-      syncInterval
-      syncIntervalDefault
-      config {
-        key
-        value
-      }
-      actions {
-        handler
-        label
-        hint
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/system/system-mutation-upgrade.gql

@@ -1,12 +0,0 @@
-mutation {
-  system {
-    performUpgrade {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 21
client/graph/admin/system/system-query-info.gql

@@ -1,21 +0,0 @@
-query {
-  system {
-    info {
-      configFile
-      cpuCores
-      currentVersion
-      dbHost
-      dbType
-      dbVersion
-      hostname
-      latestVersion
-      latestVersionReleaseDate
-      nodeVersion
-      operatingSystem
-      platform
-      ramTotal
-      upgradeCapable
-      workingDirectory
-    }
-  }
-}

+ 0 - 12
client/graph/admin/theme/theme-mutation-save.gql

@@ -1,12 +0,0 @@
-mutation($theme: String!, $iconset: String!, $darkMode: Boolean!, $injectCSS: String, $injectHead: String, $injectBody: String) {
-  theming {
-    setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {
-      responseResult {
-        succeeded
-        errorCode
-        slug
-        message
-      }
-    }
-  }
-}

+ 0 - 12
client/graph/admin/theme/theme-query-config.gql

@@ -1,12 +0,0 @@
-query {
-  theming {
-    config {
-      theme
-      iconset
-      darkMode
-      injectCSS
-      injectHead
-      injectBody
-    }
-  }
-}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.