feat: admin auth + config ref + modules sidebar ui + GQL upload (wip)
This commit is contained in:
parent
596833180e
commit
6fe49309c1
@ -5,11 +5,10 @@ import VueRouter from 'vue-router'
|
||||
import VueClipboards from 'vue-clipboards'
|
||||
import VeeValidate from 'vee-validate'
|
||||
import { ApolloClient } from 'apollo-client'
|
||||
import { createPersistedQueryLink } from 'apollo-link-persisted-queries'
|
||||
import { BatchHttpLink } from 'apollo-link-batch-http'
|
||||
import { ApolloLink, split } from 'apollo-link'
|
||||
// import { createHttpLink } from 'apollo-link-http'
|
||||
import { WebSocketLink } from 'apollo-link-ws'
|
||||
import { createUploadLink } from 'apollo-upload-client'
|
||||
import { ErrorLink } from 'apollo-link-error'
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory'
|
||||
import { getMainDefinition } from 'apollo-utilities'
|
||||
@ -56,6 +55,33 @@ store.commit('user/REFRESH_AUTH')
|
||||
const graphQLEndpoint = window.location.protocol + '//' + window.location.host + '/graphql'
|
||||
const graphQLWSEndpoint = ((window.location.protocol === 'https:') ? 'wss:' : 'ws:') + '//' + window.location.host + '/graphql-subscriptions'
|
||||
|
||||
const graphQLFetch = async (uri, options) => {
|
||||
// Strip __typename fields from variables
|
||||
let body = JSON.parse(options.body)
|
||||
body = body.map(bd => {
|
||||
return ({
|
||||
...bd,
|
||||
variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })
|
||||
})
|
||||
})
|
||||
options.body = JSON.stringify(body)
|
||||
|
||||
// Inject authentication token
|
||||
const jwtToken = Cookies.get('jwt')
|
||||
if (jwtToken) {
|
||||
options.headers.Authorization = `Bearer ${jwtToken}`
|
||||
}
|
||||
|
||||
const resp = await fetch(uri, options)
|
||||
|
||||
// Handle renewed JWT
|
||||
const newJWT = resp.headers.get('new-jwt')
|
||||
if (newJWT) {
|
||||
Cookies.set('jwt', newJWT, { expires: 365 })
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
const graphQLLink = ApolloLink.from([
|
||||
new ErrorLink(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors) {
|
||||
@ -79,7 +105,6 @@ const graphQLLink = ApolloLink.from([
|
||||
})
|
||||
}
|
||||
}),
|
||||
// createPersistedQueryLink(),
|
||||
new BatchHttpLink({
|
||||
includeExtensions: true,
|
||||
uri: graphQLEndpoint,
|
||||
@ -93,10 +118,6 @@ const graphQLLink = ApolloLink.from([
|
||||
variables: JSON.parse(JSON.stringify(bd.variables), (key, value) => { return key === '__typename' ? undefined : value })
|
||||
})
|
||||
})
|
||||
// body = {
|
||||
// ...body,
|
||||
// variables: JSON.parse(JSON.stringify(body.variables), (key, value) => { return key === '__typename' ? undefined : value })
|
||||
// }
|
||||
options.body = JSON.stringify(body)
|
||||
|
||||
// Inject authentication token
|
||||
@ -117,6 +138,28 @@ const graphQLLink = ApolloLink.from([
|
||||
})
|
||||
])
|
||||
|
||||
const graphQLUploadLink = createUploadLink({
|
||||
includeExtensions: true,
|
||||
uri: graphQLEndpoint,
|
||||
credentials: 'include',
|
||||
fetch: async (uri, options) => {
|
||||
// Inject authentication token
|
||||
const jwtToken = Cookies.get('jwt')
|
||||
if (jwtToken) {
|
||||
options.headers.Authorization = `Bearer ${jwtToken}`
|
||||
}
|
||||
|
||||
const resp = await fetch(uri, options)
|
||||
|
||||
// Handle renewed JWT
|
||||
const newJWT = resp.headers.get('new-jwt')
|
||||
if (newJWT) {
|
||||
Cookies.set('jwt', newJWT, { expires: 365 })
|
||||
}
|
||||
return resp
|
||||
}
|
||||
})
|
||||
|
||||
const graphQLWSLink = new WebSocketLink({
|
||||
uri: graphQLWSEndpoint,
|
||||
options: {
|
||||
@ -129,7 +172,7 @@ window.graphQL = new ApolloClient({
|
||||
link: split(({ query }) => {
|
||||
const { kind, operation } = getMainDefinition(query)
|
||||
return kind === 'OperationDefinition' && operation === 'subscription'
|
||||
}, graphQLWSLink, graphQLLink),
|
||||
}, graphQLWSLink, split(operation => operation.getContext().hasUpload, graphQLUploadLink, graphQLLink)),
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: (process.env.node_env === 'development')
|
||||
})
|
||||
|
@ -49,7 +49,7 @@
|
||||
v-list-tile(to='/auth')
|
||||
v-list-tile-avatar: v-icon lock_outline
|
||||
v-list-tile-title {{ $t('admin:auth.title') }}
|
||||
v-list-tile(to='/editor')
|
||||
v-list-tile(to='/editor', disabled)
|
||||
v-list-tile-avatar: v-icon transform
|
||||
v-list-tile-title {{ $t('admin:editor.title') }}
|
||||
v-list-tile(to='/logging')
|
||||
|
@ -6,7 +6,7 @@
|
||||
img.animated.fadeInUp(src='/svg/icon-unlock.svg', alt='Authentication', style='width: 80px;')
|
||||
.admin-header-title
|
||||
.headline.primary--text.animated.fadeInLeft Authentication
|
||||
.subheading.grey--text.animated.fadeInLeft.wait-p4s Configure the authentication settings of your wiki #[v-chip(label, color='primary', small).white--text coming soon]
|
||||
.subheading.grey--text.animated.fadeInLeft.wait-p4s Configure the authentication settings of your wiki
|
||||
v-spacer
|
||||
v-btn.animated.fadeInDown.wait-p2s(outline, color='grey', @click='refresh', large)
|
||||
v-icon refresh
|
||||
@ -14,158 +14,186 @@
|
||||
v-icon(left) check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-card.mt-3.animated.fadeInUp
|
||||
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='strategy in activeStrategies', :key='strategy.key') {{ strategy.title }}
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subheading Strategies
|
||||
v-list(two-line, dense).py-0
|
||||
template(v-for='(str, idx) in strategies')
|
||||
v-list-tile(:key='str.key', @click='selectedStrategy = str.key', :disabled='!str.isAvailable')
|
||||
v-list-tile-avatar
|
||||
v-icon(color='grey', v-if='!str.isAvailable') indeterminate_check_box
|
||||
v-icon(color='primary', v-else-if='str.isEnabled', v-ripple, @click='str.key !== `local` && (str.isEnabled = false)') check_box
|
||||
v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') check_box_outline_blank
|
||||
v-list-tile-content
|
||||
v-list-tile-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedStrategy === str.key ? `primary--text` : ``)') {{ str.title }}
|
||||
v-list-tile-sub-title.caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedStrategy === str.key ? `blue--text ` : ``)') {{ str.description }}
|
||||
v-list-tile-avatar(v-if='selectedStrategy === str.key')
|
||||
v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
|
||||
v-divider(v-if='idx < strategies.length - 1')
|
||||
|
||||
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
|
||||
v-container.pa-3(fluid, grid-list-md)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12, md6)
|
||||
.body-2.grey--text.text--darken-1 Select which authentication strategies to enable:
|
||||
.caption.grey--text.pb-2 Some strategies require additional configuration in their dedicated tab (when selected).
|
||||
v-form
|
||||
//- TODO - Prevent crash on unfinished strategies
|
||||
v-checkbox.my-0(
|
||||
v-for='strategy in strategies'
|
||||
v-model='strategy.isEnabled'
|
||||
:key='strategy.key'
|
||||
:label='strategy.title'
|
||||
color='primary'
|
||||
:disabled='strategy.key === `local` || true'
|
||||
hide-details
|
||||
)
|
||||
v-flex(xs12, md6)
|
||||
.pa-3.grey.radius-7(:class='$vuetify.dark ? "darken-4" : "lighten-5"')
|
||||
.body-2.grey--text.text--darken-1 Advanced Settings
|
||||
v-text-field.mt-3.md2(
|
||||
v-model='jwtAudience'
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
prepend-icon='account_balance'
|
||||
label='JWT Audience'
|
||||
hint='Audience URN used in JWT issued upon login. Usually your domain name. (e.g. urn:your.domain.com)'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.mt-3.md2(
|
||||
v-model='jwtExpiration'
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
prepend-icon='schedule'
|
||||
label='Token Expiration'
|
||||
hint='The expiration period of a token until it must be renewed. (default: 30m)'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.mt-3.md2(
|
||||
v-model='jwtRenewablePeriod'
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
prepend-icon='update'
|
||||
label='Token Renewal Period'
|
||||
hint='The maximum period a token can be renewed when expired. (default: 14d)'
|
||||
persistent-hint
|
||||
)
|
||||
v-card.wiki-form.mt-3.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subheading Global Advanced settings
|
||||
v-card-text
|
||||
v-text-field.md2(
|
||||
v-model='jwtAudience'
|
||||
outline
|
||||
prepend-icon='account_balance'
|
||||
label='JWT Audience'
|
||||
hint='Audience URN used in JWT issued upon login. Usually your domain name. (e.g. urn:your.domain.com)'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.mt-3.md2(
|
||||
v-model='jwtExpiration'
|
||||
outline
|
||||
prepend-icon='schedule'
|
||||
label='Token Expiration'
|
||||
hint='The expiration period of a token until it must be renewed. (default: 30m)'
|
||||
persistent-hint
|
||||
)
|
||||
v-text-field.mt-3.md2(
|
||||
v-model='jwtRenewablePeriod'
|
||||
outline
|
||||
prepend-icon='update'
|
||||
label='Token Renewal Period'
|
||||
hint='The maximum period a token can be renewed when expired. (default: 14d)'
|
||||
persistent-hint
|
||||
)
|
||||
|
||||
v-tab-item(v-for='(strategy, n) in activeStrategies', :key='strategy.key', :transition='false', :reverse-transition='false')
|
||||
v-card.wiki-form.pa-3(flat, tile)
|
||||
v-form
|
||||
.authlogo
|
||||
img(:src='strategy.logo', :alt='strategy.title')
|
||||
v-subheader.pl-0 {{strategy.title}}
|
||||
.caption {{strategy.description}}
|
||||
.caption: a(:href='strategy.website') {{strategy.website}}
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Strategy Configuration
|
||||
.body-1.ml-3(v-if='!strategy.config || strategy.config.length < 1') This strategy has no configuration options you can modify.
|
||||
template(v-else, v-for='cfg in strategy.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.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='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 Registration
|
||||
.pr-3
|
||||
v-switch.ml-3(
|
||||
v-model='strategy.selfRegistration'
|
||||
label='Allow self-registration'
|
||||
color='primary'
|
||||
hint='Allow any user successfully authorized by the strategy to access the wiki.'
|
||||
persistent-hint
|
||||
)
|
||||
v-combobox.ml-3.mt-3(
|
||||
label='Limit to specific email domains'
|
||||
v-model='strategy.domainWhitelist'
|
||||
prepend-icon='mail_outline'
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
persistent-hint
|
||||
small-chips
|
||||
deletable-chips
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
)
|
||||
v-autocomplete.ml-3(
|
||||
outline
|
||||
background-color='grey lighten-2'
|
||||
:items='groups'
|
||||
item-text='name'
|
||||
item-value='id'
|
||||
label='Assign to group'
|
||||
v-model='strategy.autoEnrollGroups'
|
||||
prepend-icon='people'
|
||||
hint='Automatically assign new users to these groups.'
|
||||
small-chips
|
||||
persistent-hint
|
||||
deletable-chips
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
)
|
||||
template(v-if='strategy.key === `local`')
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Security
|
||||
.pr-3
|
||||
v-switch.ml-3(
|
||||
:disabled='true'
|
||||
v-model='strategy.recaptcha'
|
||||
label='Use reCAPTCHA by Google'
|
||||
color='primary'
|
||||
hint='Protects against spam robots and malicious registrations.'
|
||||
persistent-hint
|
||||
)
|
||||
v-flex(xs12, lg9)
|
||||
|
||||
v-card.wiki-form.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subheading {{strategy.title}}
|
||||
v-card-text
|
||||
v-form
|
||||
.authlogo
|
||||
img(:src='strategy.logo', :alt='strategy.title')
|
||||
.caption.pt-3 {{strategy.description}}
|
||||
.caption.pb-3: a(:href='strategy.website') {{strategy.website}}
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Strategy Configuration
|
||||
.body-1.ml-3(v-if='!strategy.config || strategy.config.length < 1'): em This strategy has no configuration options you can modify.
|
||||
template(v-else, v-for='cfg in strategy.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.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='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 Registration
|
||||
.pr-3
|
||||
v-switch.ml-3(
|
||||
v-model='strategy.selfRegistration'
|
||||
label='Allow self-registration'
|
||||
color='primary'
|
||||
hint='Allow any user successfully authorized by the strategy to access the wiki.'
|
||||
persistent-hint
|
||||
)
|
||||
v-switch.ml-3(
|
||||
v-if='strategy.useForm'
|
||||
:disabled='!strategy.selfRegistration || true'
|
||||
v-model='strategy.recaptcha'
|
||||
label='Use reCAPTCHA by Google'
|
||||
color='primary'
|
||||
hint='Protects against spam robots and malicious registrations.'
|
||||
persistent-hint
|
||||
)
|
||||
v-combobox.ml-3.mt-3(
|
||||
label='Limit to specific email domains'
|
||||
v-model='strategy.domainWhitelist'
|
||||
prepend-icon='mail_outline'
|
||||
outline
|
||||
:disabled='!strategy.selfRegistration'
|
||||
hint='A list of domains authorized to register. The user email address domain must match one of these to gain access.'
|
||||
persistent-hint
|
||||
small-chips
|
||||
deletable-chips
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
)
|
||||
v-autocomplete.mt-3.ml-3(
|
||||
outline
|
||||
:disabled='!strategy.selfRegistration'
|
||||
:items='groups'
|
||||
item-text='name'
|
||||
item-value='id'
|
||||
label='Assign to group'
|
||||
v-model='strategy.autoEnrollGroups'
|
||||
prepend-icon='people'
|
||||
hint='Automatically assign new users to these groups.'
|
||||
small-chips
|
||||
persistent-hint
|
||||
deletable-chips
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
)
|
||||
template(v-if='strategy.useForm')
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Security
|
||||
v-switch.ml-3(
|
||||
v-model='strategy.recaptcha'
|
||||
:disabled='true'
|
||||
label='Force all users to use Two-Factor Authentication (2FA)'
|
||||
color='primary'
|
||||
hint='Users will be required to setup 2FA the first time they login and cannot be disabled by the user.'
|
||||
persistent-hint
|
||||
)
|
||||
|
||||
v-card.mt-3.wiki-form.animated.fadeInUp.wait-p4s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subheading Configuration Reference
|
||||
v-card-text
|
||||
.body-1 Some strategies may require some configuration values to be set on your provider.
|
||||
v-alert.mt-3.radius-7(v-if='host.length < 8', color='red', outline, :value='true', icon='warning') You must set a valid #[strong Site URL] first! Click on #[strong General] in the left sidebar.
|
||||
.pa-3.mt-3.radius-7.grey(v-else, :class='$vuetify.dark ? `darken-3-d5` : `lighten-3`')
|
||||
.body-2 Allowed Web Origins
|
||||
.body-1 {{host}}
|
||||
v-divider.my-3
|
||||
.body-2 Callback URL
|
||||
.body-1 {{host}}/login/callback/{{strategy.key}}
|
||||
v-divider.my-3
|
||||
.body-2 Login URL
|
||||
.body-1 {{host}}/login
|
||||
v-divider.my-3
|
||||
.body-2 Logout URL
|
||||
.body-1 {{host}}
|
||||
v-divider.my-3
|
||||
.body-2 Token Endpoint Authentication Method
|
||||
.body-1 HTTP-POST
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -174,6 +202,7 @@ import _ from 'lodash'
|
||||
import groupsQuery from 'gql/admin/auth/auth-query-groups.gql'
|
||||
import strategiesQuery from 'gql/admin/auth/auth-query-strategies.gql'
|
||||
import strategiesSaveMutation from 'gql/admin/auth/auth-mutation-save-strategies.gql'
|
||||
import hostQuery from 'gql/admin/auth/auth-query-host.gql'
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
@ -183,6 +212,9 @@ export default {
|
||||
return {
|
||||
groups: [],
|
||||
strategies: [],
|
||||
selectedStrategy: '',
|
||||
host: '',
|
||||
strategy: {},
|
||||
jwtAudience: 'urn:wiki.js',
|
||||
jwtExpiration: '30m',
|
||||
jwtRenewablePeriod: '14d'
|
||||
@ -193,6 +225,14 @@ export default {
|
||||
return _.filter(this.strategies, 'isEnabled')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedStrategy(newValue, oldValue) {
|
||||
this.strategy = _.find(this.strategies, ['key', newValue]) || {}
|
||||
},
|
||||
strategies(newValue, oldValue) {
|
||||
this.selectedStrategy = 'local'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async refresh() {
|
||||
await this.$apollo.queries.strategies.refetch()
|
||||
@ -238,7 +278,13 @@ export default {
|
||||
strategies: {
|
||||
query: strategiesQuery,
|
||||
fetchPolicy: 'network-only',
|
||||
update: (data) => _.cloneDeep(data.authentication.strategies).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})),
|
||||
update: (data) => _.cloneDeep(data.authentication.strategies).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-auth-refresh')
|
||||
}
|
||||
@ -250,6 +296,14 @@ export default {
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,17 +21,19 @@
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subheading Search Engine
|
||||
v-card-text
|
||||
v-radio-group.my-0(v-model='selectedEngine')
|
||||
v-radio.my-1(
|
||||
v-for='(engine, n) in engines'
|
||||
:key='engine.key'
|
||||
:label='engine.title'
|
||||
:value='engine.key'
|
||||
:disabled='!engine.isAvailable'
|
||||
color='primary'
|
||||
hide-details
|
||||
)
|
||||
v-list.py-0(two-line, dense)
|
||||
template(v-for='(eng, idx) in engines')
|
||||
v-list-tile(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
|
||||
v-list-tile-avatar
|
||||
v-icon(color='grey', v-if='!eng.isAvailable') cancel
|
||||
v-icon(color='primary', v-else-if='eng.key === selectedEngine') radio_button_checked
|
||||
v-icon(color='grey', v-else) radio_button_unchecked
|
||||
v-list-tile-content
|
||||
v-list-tile-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }}
|
||||
v-list-tile-sub-title.caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}
|
||||
v-list-tile-avatar(v-if='selectedEngine === eng.key')
|
||||
v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
|
||||
v-divider(v-if='idx < engines.length - 1')
|
||||
|
||||
v-flex(lg9, xs12)
|
||||
v-card.wiki-form.animated.fadeInUp.wait-p2s
|
||||
|
@ -14,167 +14,175 @@
|
||||
v-icon(left) check
|
||||
span {{$t('common:actions.apply')}}
|
||||
|
||||
v-card.mt-3.animated.fadeInUp
|
||||
v-tabs(color='grey darken-2', fixed-tabs, slider-color='white', show-arrows, dark, v-model='currentTab')
|
||||
v-tab(key='settings'): v-icon settings
|
||||
v-tab(v-for='tgt in activeTargets', :key='tgt.key') {{ tgt.title }}
|
||||
v-flex(lg3, xs12)
|
||||
v-card.animated.fadeInUp
|
||||
v-toolbar(flat, color='primary', dark, dense)
|
||||
.subheading Targets
|
||||
v-list(two-line, dense).py-0
|
||||
template(v-for='(tgt, idx) in targets')
|
||||
v-list-tile(:key='tgt.key', @click='selectedTarget = tgt.key', :disabled='!tgt.isAvailable')
|
||||
v-list-tile-avatar
|
||||
v-icon(color='grey', v-if='!tgt.isAvailable') indeterminate_check_box
|
||||
v-icon(color='primary', v-else-if='tgt.isEnabled', v-ripple, @click='tgt.key !== `local` && (tgt.isEnabled = false)') check_box
|
||||
v-icon(color='grey', v-else, v-ripple, @click='tgt.isEnabled = true') check_box_outline_blank
|
||||
v-list-tile-content
|
||||
v-list-tile-title.body-2(:class='!tgt.isAvailable ? `grey--text` : (selectedTarget === tgt.key ? `primary--text` : ``)') {{ tgt.title }}
|
||||
v-list-tile-sub-title.caption(:class='!tgt.isAvailable ? `grey--text text--lighten-1` : (selectedTarget === tgt.key ? `blue--text ` : ``)') {{ tgt.description }}
|
||||
v-list-tile-avatar(v-if='selectedTarget === tgt.key')
|
||||
v-icon.animated.fadeInLeft(color='primary') arrow_forward_ios
|
||||
v-divider(v-if='idx < targets.length - 1')
|
||||
|
||||
v-tab-item(key='settings', :transition='false', :reverse-transition='false')
|
||||
v-container.pa-3(fluid, grid-list-md)
|
||||
v-layout(row, wrap)
|
||||
v-flex(xs12, md6)
|
||||
.body-2.grey--text.text--darken-1 Select which storage targets to enable:
|
||||
.caption.grey--text.pb-2 Some storage targets require additional configuration in their dedicated tab (when selected).
|
||||
v-form
|
||||
v-checkbox.my-0(
|
||||
:disabled='!tgt.isAvailable'
|
||||
v-for='tgt in targets'
|
||||
v-model='tgt.isEnabled'
|
||||
:key='tgt.key'
|
||||
:label='tgt.title'
|
||||
color='primary'
|
||||
hide-details
|
||||
)
|
||||
v-flex(xs12, md6)
|
||||
.pa-3.grey.radius-7(:class='$vuetify.dark ? "darken-4" : "lighten-5"')
|
||||
v-layout.pa-2(row, justify-space-between)
|
||||
.body-2.grey--text.text--darken-1 Status
|
||||
.d-flex
|
||||
looping-rhombuses-spinner.mt-1(
|
||||
:animation-duration='5000'
|
||||
:rhombus-size='10'
|
||||
color='#BBB'
|
||||
)
|
||||
.caption.ml-3.grey--text This panel refreshes automatically.
|
||||
v-divider
|
||||
v-toolbar.mt-2.radius-7(
|
||||
v-for='(tgt, n) in status'
|
||||
:key='tgt.key'
|
||||
dense
|
||||
:color='getStatusColor(tgt.status)'
|
||||
dark
|
||||
flat
|
||||
:extended='tgt.status !== `pending`',
|
||||
:extension-height='tgt.status === `error` ? 100 : 70'
|
||||
)
|
||||
.pa-3.red.darken-2.radius-7(v-if='tgt.status === `error`', slot='extension') {{tgt.message}}
|
||||
v-toolbar.radius-7(
|
||||
color='green darken-2'
|
||||
v-else-if='tgt.status !== `pending`'
|
||||
slot='extension'
|
||||
flat
|
||||
dense
|
||||
)
|
||||
span Last synchronization {{tgt.lastAttempt | moment('from') }}
|
||||
.body-2 {{tgt.title}}
|
||||
v-spacer
|
||||
.body-1 {{tgt.status}}
|
||||
v-alert.mt-3.radius-7(v-if='status.length < 1', outline, :value='true', color='indigo') You don't have any active storage target.
|
||||
v-card.mt-3.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(flat, :color='$vuetify.dark ? `grey darken-3-l5` : `grey darken-3`', dark, dense)
|
||||
.subheading 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-tile(:key='tgt.key')
|
||||
template(v-if='tgt.status === `pending`')
|
||||
v-list-tile-avatar(color='purple')
|
||||
v-icon(color='white') schedule
|
||||
v-list-tile-content
|
||||
v-list-tile-title.body-2 {{tgt.title}}
|
||||
v-list-tile-sub-title.purple--text.caption {{tgt.status}}
|
||||
v-list-tile-action
|
||||
v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
|
||||
template(v-else-if='tgt.status === `operational`')
|
||||
v-list-tile-avatar(color='green')
|
||||
v-icon(color='white') check_circle
|
||||
v-list-tile-content
|
||||
v-list-tile-title.body-2 {{tgt.title}}
|
||||
v-list-tile-sub-title.green--text.caption Last synchronization {{tgt.lastAttempt | moment('from') }}
|
||||
template(v-else)
|
||||
v-list-tile-avatar(color='red')
|
||||
v-icon(color='white') highlight_off
|
||||
v-list-tile-content
|
||||
v-list-tile-title.body-2 {{tgt.title}}
|
||||
v-list-tile-sub-title.red--text.caption Last attempt was {{tgt.lastAttempt | moment('from') }}
|
||||
v-list-tile-action
|
||||
v-menu
|
||||
v-btn(slot='activator', icon)
|
||||
v-icon(color='red') info
|
||||
v-card(width='450')
|
||||
v-toolbar(flat, color='red', dark, dense) Error Message
|
||||
v-card-text {{tgt.message}}
|
||||
|
||||
v-tab-item(v-for='(tgt, n) in activeTargets', :key='tgt.key', :transition='false', :reverse-transition='false')
|
||||
v-card.wiki-form.pa-3(flat, tile)
|
||||
v-form
|
||||
.targetlogo
|
||||
img(:src='tgt.logo', :alt='tgt.title')
|
||||
v-subheader.pl-0 {{tgt.title}}
|
||||
.caption {{tgt.description}}
|
||||
.caption: a(:href='tgt.website') {{tgt.website}}
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Target Configuration
|
||||
.body-1.ml-3(v-if='!tgt.config || tgt.config.length < 1') This storage target has no configuration options you can modify.
|
||||
template(v-else, v-for='cfg in tgt.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.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='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 Sync Direction
|
||||
.body-1.ml-3 Choose how content synchronization is handled for this storage target.
|
||||
.pr-3.pt-3
|
||||
v-radio-group.ml-3.py-0(v-model='tgt.mode')
|
||||
v-radio(
|
||||
label='Bi-directional'
|
||||
color='primary'
|
||||
value='sync'
|
||||
:disabled='tgt.supportedModes.indexOf(`sync`) < 0'
|
||||
)
|
||||
v-radio(
|
||||
label='Push to target'
|
||||
color='primary'
|
||||
value='push'
|
||||
:disabled='tgt.supportedModes.indexOf(`push`) < 0'
|
||||
)
|
||||
v-radio(
|
||||
label='Pull from target'
|
||||
color='primary'
|
||||
value='pull'
|
||||
:disabled='tgt.supportedModes.indexOf(`pull`) < 0'
|
||||
)
|
||||
.body-1.ml-3
|
||||
strong Bi-directional #[em.red--text.text--lighten-2(v-if='tgt.supportedModes.indexOf(`sync`) < 0') Unsupported]
|
||||
.pb-3 In bi-directional mode, content is first pulled from the storage target. Any newer content overwrites local content. New content since last sync is then pushed to the storage target, overwriting any content on target if present.
|
||||
strong Push to target #[em.red--text.text--lighten-2(v-if='tgt.supportedModes.indexOf(`push`) < 0') Unsupported]
|
||||
.pb-3 Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios.
|
||||
strong Pull from target #[em.red--text.text--lighten-2(v-if='tgt.supportedModes.indexOf(`pull`) < 0') Unsupported]
|
||||
.pb-3 Content is always pulled from the storage target, overwriting any local content which already exists. This choice is usually reserved for single-use content import. Caution with this option as any local content will always be overwritten!
|
||||
v-divider(v-if='n < status.length - 1')
|
||||
v-list-tile(v-if='status.length < 1')
|
||||
em You don't have any active storage target.
|
||||
|
||||
template(v-if='tgt.hasSchedule')
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Sync Schedule
|
||||
.body-1.ml-3 For performance reasons, this storage target synchronize changes on an interval-based schedule, instead of on every change. Define at which interval should the synchronization occur.
|
||||
.pa-3
|
||||
duration-picker(v-model='tgt.syncInterval')
|
||||
.caption.mt-3 Currently set to every #[strong {{getDefaultSchedule(tgt.syncInterval)}}].
|
||||
.caption The default is every #[strong {{getDefaultSchedule(tgt.syncIntervalDefault)}}].
|
||||
v-flex(xs12, lg9)
|
||||
v-card.wiki-form.animated.fadeInUp.wait-p2s
|
||||
v-toolbar(color='primary', dense, flat, dark)
|
||||
.subheading {{target.title}}
|
||||
v-card-text
|
||||
v-form
|
||||
.targetlogo
|
||||
img(:src='target.logo', :alt='target.title')
|
||||
v-subheader.pl-0 {{target.title}}
|
||||
.caption {{target.description}}
|
||||
.caption: a(:href='target.website') {{target.website}}
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Target Configuration
|
||||
.body-1.ml-3(v-if='!target.config || target.config.length < 1') This storage target has no configuration options you can modify.
|
||||
template(v-else, v-for='cfg in target.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.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='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 Sync Direction
|
||||
.body-1.ml-3 Choose how content synchronization is handled for this storage target.
|
||||
.pr-3.pt-3
|
||||
v-radio-group.ml-3.py-0(v-model='target.mode')
|
||||
v-radio(
|
||||
label='Bi-directional'
|
||||
color='primary'
|
||||
value='sync'
|
||||
:disabled='target.supportedModes.indexOf(`sync`) < 0'
|
||||
)
|
||||
v-radio(
|
||||
label='Push to target'
|
||||
color='primary'
|
||||
value='push'
|
||||
:disabled='target.supportedModes.indexOf(`push`) < 0'
|
||||
)
|
||||
v-radio(
|
||||
label='Pull from target'
|
||||
color='primary'
|
||||
value='pull'
|
||||
:disabled='target.supportedModes.indexOf(`pull`) < 0'
|
||||
)
|
||||
.body-1.ml-3
|
||||
strong Bi-directional #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`sync`) < 0') Unsupported]
|
||||
.pb-3 In bi-directional mode, content is first pulled from the storage target. Any newer content overwrites local content. New content since last sync is then pushed to the storage target, overwriting any content on target if present.
|
||||
strong Push to target #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`push`) < 0') Unsupported]
|
||||
.pb-3 Content is always pushed to the storage target, overwriting any existing content. This is safest choice for backup scenarios.
|
||||
strong Pull from target #[em.red--text.text--lighten-2(v-if='target.supportedModes.indexOf(`pull`) < 0') Unsupported]
|
||||
.pb-3 Content is always pulled from the storage target, overwriting any local content which already exists. This choice is usually reserved for single-use content import. Caution with this option as any local content will always be overwritten!
|
||||
|
||||
template(v-if='tgt.actions && tgt.actions.length > 0')
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Actions
|
||||
v-container.pt-0(grid-list-xl, fluid)
|
||||
v-layout(row, wrap, fill-height)
|
||||
v-flex(xs12, lg6, xl4, v-for='act of tgt.actions')
|
||||
v-card.radius-7.grey(flat, :class='$vuetify.dark ? `darken-3-d5` : `lighten-3`', height='100%')
|
||||
v-card-text
|
||||
.subheading(v-html='act.label')
|
||||
.body-1.mt-2(v-html='act.hint')
|
||||
v-btn.mx-0.mt-3(
|
||||
@click='executeAction(tgt.key, act.handler)'
|
||||
outline
|
||||
:color='$vuetify.dark ? `blue` : `primary`'
|
||||
:disabled='runningAction'
|
||||
:loading='runningActionHandler === act.handler'
|
||||
) Run
|
||||
template(v-if='target.hasSchedule')
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Sync Schedule
|
||||
.body-1.ml-3 For performance reasons, this storage target synchronize changes on an interval-based schedule, instead of on every change. Define at which interval should the synchronization occur.
|
||||
.pa-3
|
||||
duration-picker(v-model='target.syncInterval')
|
||||
.caption.mt-3 Currently set to every #[strong {{getDefaultSchedule(target.syncInterval)}}].
|
||||
.caption The default is every #[strong {{getDefaultSchedule(target.syncIntervalDefault)}}].
|
||||
|
||||
template(v-if='target.actions && target.actions.length > 0')
|
||||
v-divider.mt-3
|
||||
v-subheader.pl-0 Actions
|
||||
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.dark ? `darken-3-d5` : `lighten-3`', height='100%')
|
||||
v-card-text
|
||||
.subheading(v-html='act.label')
|
||||
.body-1.mt-2(v-html='act.hint')
|
||||
v-btn.mx-0.mt-3(
|
||||
@click='executeAction(target.key, act.handler)'
|
||||
outline
|
||||
:color='$vuetify.dark ? `blue` : `primary`'
|
||||
:disabled='runningAction'
|
||||
:loading='runningActionHandler === act.handler'
|
||||
) Run
|
||||
|
||||
</template>
|
||||
|
||||
@ -205,7 +213,8 @@ export default {
|
||||
return {
|
||||
runningAction: false,
|
||||
runningActionHandler: '',
|
||||
currentTab: 0,
|
||||
selectedTarget: '',
|
||||
target: {},
|
||||
targets: [],
|
||||
status: []
|
||||
}
|
||||
@ -215,6 +224,14 @@ export default {
|
||||
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()
|
||||
@ -238,7 +255,6 @@ export default {
|
||||
])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))}))
|
||||
}
|
||||
})
|
||||
this.currentTab = 0
|
||||
this.$store.commit('showNotification', {
|
||||
message: 'Storage configuration saved successfully.',
|
||||
style: 'success',
|
||||
|
@ -9,7 +9,7 @@
|
||||
v-toolbar.radius-7(color='teal lighten-5', dense, flat, height='44')
|
||||
.body-2.teal--text Images
|
||||
v-btn.ml-3.my-0.radius-7(outline, large, color='teal', disabled)
|
||||
v-icon(left) keyboard_backspace
|
||||
v-icon(left) keyboard_arrow_up
|
||||
span Parent Folder
|
||||
v-btn.my-0.radius-7(outline, large, color='teal')
|
||||
v-icon(left) add
|
||||
@ -48,14 +48,15 @@
|
||||
ref='pond'
|
||||
label-idle='Browse or Drop files here...'
|
||||
allow-multiple='true'
|
||||
accepted-file-types='image/jpeg, image/png'
|
||||
accepted-file-types='image/jpeg, image/png, image/gif, image/svg'
|
||||
:files='files'
|
||||
max-files='10'
|
||||
)
|
||||
v-divider
|
||||
v-card-actions.pa-3
|
||||
.caption.grey--text.text-darken-2 Max 20 files, 5 MB each
|
||||
.caption.grey--text.text-darken-2 Max 10 files, 5 MB each
|
||||
v-spacer
|
||||
v-btn(color='teal', dark) Upload
|
||||
v-btn(color='teal', dark, @click='upload') Upload
|
||||
|
||||
v-card.mt-3.radius-7.animated.fadeInRight.wait-p4s(light)
|
||||
v-card-text.pb-0
|
||||
@ -96,11 +97,11 @@ import { sync } from 'vuex-pathify'
|
||||
import vueFilePond from 'vue-filepond'
|
||||
import 'filepond/dist/filepond.min.css'
|
||||
|
||||
import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css'
|
||||
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
|
||||
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
|
||||
|
||||
const FilePond = vueFilePond(FilePondPluginFileValidateType, FilePondPluginImagePreview)
|
||||
import uploadFileMutation from 'gql/editor/upload.gql'
|
||||
|
||||
const FilePond = vueFilePond(FilePondPluginFileValidateType)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -135,6 +136,21 @@ export default {
|
||||
methods: {
|
||||
insert () {
|
||||
this.activeModal = ''
|
||||
},
|
||||
async upload () {
|
||||
const files = this.$refs.pond.getFiles()
|
||||
for (let fl of files) {
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: uploadFileMutation,
|
||||
variables: {
|
||||
data: fl.file
|
||||
},
|
||||
context: {
|
||||
hasUpload: true
|
||||
}
|
||||
})
|
||||
console.info(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
client/graph/admin/auth/auth-query-host.gql
Normal file
7
client/graph/admin/auth/auth-query-host.gql
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
site {
|
||||
config {
|
||||
host
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ query {
|
||||
key
|
||||
title
|
||||
description
|
||||
isAvailable
|
||||
useForm
|
||||
logo
|
||||
website
|
||||
|
10
client/graph/editor/upload.gql
Normal file
10
client/graph/editor/upload.gql
Normal file
@ -0,0 +1,10 @@
|
||||
mutation ($file: Upload!) {
|
||||
assets {
|
||||
upload(data:$file) {
|
||||
responseResult {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -76,6 +76,7 @@
|
||||
"graphql-rate-limit-directive": "0.1.0",
|
||||
"graphql-subscriptions": "1.0.0",
|
||||
"graphql-tools": "4.0.4",
|
||||
"graphql-upload": "8.0.5",
|
||||
"highlight.js": "9.14.2",
|
||||
"i18next": "14.0.1",
|
||||
"i18next-express-middleware": "1.7.1",
|
||||
@ -192,6 +193,7 @@
|
||||
"apollo-link-http": "1.5.11",
|
||||
"apollo-link-persisted-queries": "0.2.2",
|
||||
"apollo-link-ws": "1.0.14",
|
||||
"apollo-upload-client": "10.0.0",
|
||||
"apollo-utilities": "1.1.2",
|
||||
"autoprefixer": "9.4.7",
|
||||
"babel-eslint": "10.0.1",
|
||||
@ -221,7 +223,6 @@
|
||||
"file-loader": "3.0.1",
|
||||
"filepond": "4.2.0",
|
||||
"filepond-plugin-file-validate-type": "1.2.2",
|
||||
"filepond-plugin-image-preview": "4.0.3",
|
||||
"filesize.js": "1.0.2",
|
||||
"grapesjs": "0.14.52",
|
||||
"graphiql": "0.12.0",
|
||||
|
16
server/db/migrations-sqlite/2.0.0-beta.99.js
Normal file
16
server/db/migrations-sqlite/2.0.0-beta.99.js
Normal file
@ -0,0 +1,16 @@
|
||||
exports.up = knex => {
|
||||
const dbCompat = {
|
||||
charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
|
||||
}
|
||||
return knex.schema
|
||||
.createTable('assetData', table => {
|
||||
if (dbCompat.charset) { table.charset('utf8mb4') }
|
||||
table.integer('id').primary()
|
||||
table.binary('data').notNullable()
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = knex => {
|
||||
return knex.schema
|
||||
.dropTableIfExists('assetData')
|
||||
}
|
16
server/db/migrations/2.0.0-beta.99.js
Normal file
16
server/db/migrations/2.0.0-beta.99.js
Normal file
@ -0,0 +1,16 @@
|
||||
exports.up = knex => {
|
||||
const dbCompat = {
|
||||
charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
|
||||
}
|
||||
return knex.schema
|
||||
.createTable('assetData', table => {
|
||||
if (dbCompat.charset) { table.charset('utf8mb4') }
|
||||
table.integer('id').primary()
|
||||
table.binary('data').notNullable()
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = knex => {
|
||||
return knex.schema
|
||||
.dropTableIfExists('assetData')
|
||||
}
|
@ -7,6 +7,7 @@ const PubSub = require('graphql-subscriptions').PubSub
|
||||
const { LEVEL, MESSAGE } = require('triple-beam')
|
||||
const Transport = require('winston-transport')
|
||||
const { createRateLimitTypeDef } = require('graphql-rate-limit-directive')
|
||||
const { GraphQLUpload } = require('graphql-upload')
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
@ -26,7 +27,9 @@ schemas.forEach(schema => {
|
||||
|
||||
// Resolvers
|
||||
|
||||
let resolvers = {}
|
||||
let resolvers = {
|
||||
Upload: GraphQLUpload
|
||||
}
|
||||
const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))
|
||||
resolversObj.forEach(resolver => {
|
||||
_.merge(resolvers, resolver)
|
||||
|
45
server/graph/schemas/asset.graphql
Normal file
45
server/graph/schemas/asset.graphql
Normal file
@ -0,0 +1,45 @@
|
||||
# ===============================================
|
||||
# ASSETS
|
||||
# ===============================================
|
||||
|
||||
extend type Query {
|
||||
assets: AssetQuery
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
assets: AssetMutation
|
||||
}
|
||||
|
||||
# -----------------------------------------------
|
||||
# QUERIES
|
||||
# -----------------------------------------------
|
||||
|
||||
type AssetQuery {
|
||||
list(
|
||||
root: String
|
||||
kind: [AssetKind]
|
||||
): [AssetItem]
|
||||
}
|
||||
|
||||
# -----------------------------------------------
|
||||
# MUTATIONS
|
||||
# -----------------------------------------------
|
||||
|
||||
type AssetMutation {
|
||||
upload(
|
||||
data: Upload!
|
||||
): DefaultResponse
|
||||
}
|
||||
|
||||
# -----------------------------------------------
|
||||
# TYPES
|
||||
# -----------------------------------------------
|
||||
|
||||
type AssetItem {
|
||||
id: Int!
|
||||
}
|
||||
|
||||
enum AssetKind {
|
||||
IMAGE
|
||||
BINARY
|
||||
}
|
@ -58,6 +58,7 @@ type AuthenticationStrategy {
|
||||
props: [String]
|
||||
title: String!
|
||||
description: String
|
||||
isAvailable: Boolean
|
||||
useForm: Boolean!
|
||||
logo: String
|
||||
color: String
|
||||
|
@ -11,7 +11,6 @@ const https = require('https')
|
||||
const path = require('path')
|
||||
const _ = require('lodash')
|
||||
const { ApolloServer } = require('apollo-server-express')
|
||||
// const oauth2orize = require('oauth2orize')
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
@ -61,12 +60,6 @@ module.exports = async () => {
|
||||
maxAge: '7d'
|
||||
}))
|
||||
|
||||
// ----------------------------------------
|
||||
// OAuth2 Server
|
||||
// ----------------------------------------
|
||||
|
||||
// const OAuth2Server = oauth2orize.createServer()
|
||||
|
||||
// ----------------------------------------
|
||||
// Passport Authentication
|
||||
// ----------------------------------------
|
||||
@ -137,6 +130,7 @@ module.exports = async () => {
|
||||
path: '/graphql-subscriptions'
|
||||
}
|
||||
})
|
||||
app.use('/graphql', mw.upload)
|
||||
apolloServer.applyMiddleware({ app })
|
||||
|
||||
// ----------------------------------------
|
||||
|
8
server/middlewares/upload.js
Normal file
8
server/middlewares/upload.js
Normal file
@ -0,0 +1,8 @@
|
||||
const { graphqlUploadExpress } = require('graphql-upload')
|
||||
|
||||
/* global WIKI */
|
||||
|
||||
/**
|
||||
* GraphQL File Upload Middleware
|
||||
*/
|
||||
module.exports = graphqlUploadExpress({ maxFileSize: 5000000, maxFiles: 20 })
|
@ -5,8 +5,21 @@ author: requarks.io
|
||||
logo: https://static.requarks.io/logo/auth0.svg
|
||||
color: deep-orange
|
||||
website: https://auth0.com/
|
||||
isAvailable: true
|
||||
useForm: false
|
||||
props:
|
||||
domain: String
|
||||
clientId: String
|
||||
clientSecret: String
|
||||
domain:
|
||||
type: String
|
||||
title: Domain
|
||||
hint: Your Auth0 domain (e.g. something.auth0.com)
|
||||
order: 1
|
||||
clientId:
|
||||
type: String
|
||||
title: Client ID
|
||||
hint: Application Client ID
|
||||
order: 2
|
||||
clientSecret:
|
||||
type: String
|
||||
title: Client Secret
|
||||
hint: Application Client Secret
|
||||
order: 3
|
||||
|
@ -5,5 +5,6 @@ author: requarks.io
|
||||
logo: https://static.requarks.io/logo/wikijs.svg
|
||||
color: yellow darken-3
|
||||
website: https://wiki.js.org
|
||||
isAvailable: true
|
||||
useForm: true
|
||||
props: {}
|
||||
|
Loading…
Reference in New Issue
Block a user