feat: admin terminal + legacy code cleanup

pull/5698/head
Nicolas Giard 2 years ago
parent 05797652a0
commit 3ebbbc8b5e
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

4
.gitignore vendored

@ -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

@ -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'))

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

File diff suppressed because it is too large Load Diff

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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)

@ -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')

@ -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'))
}
}

@ -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

@ -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`
}
})
}

@ -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>

@ -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>

@ -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)

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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')

@ -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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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
}
}
}
}

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

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

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

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

@ -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
}
}
}
}

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

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

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

@ -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
}
}
}

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

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

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

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

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

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

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

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

@ -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
}
}
}
}

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

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

@ -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
}
}
}
}

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save