feat(admin): migrate extensions + security to vue 3 composable

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

@ -12,5 +12,6 @@
},
"i18n-ally.localesPaths": [
"ux/src/i18n/locales"
]
],
"i18n-ally.keystyle": "nested"
}

@ -4,8 +4,8 @@ q-page.admin-extensions
.col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-module.svg')
.col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft {{ $t('admin.extensions.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.extensions.subtitle') }}
.text-h5.text-primary.animated.fadeInLeft {{ t('admin.extensions.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.extensions.subtitle') }}
.col-auto
q-btn.acrylic-btn.q-mr-sm(
icon='las la-question-circle'
@ -19,7 +19,7 @@ q-page.admin-extensions
icon='las la-redo-alt'
flat
color='secondary'
:loading='loading > 0'
:loading='state.loading > 0'
@click='load'
)
q-separator(inset)
@ -28,7 +28,7 @@ q-page.admin-extensions
q-card.shadow-1
q-list(separator)
q-item(
v-for='(ext, idx) of extensions'
v-for='(ext, idx) of state.extensions'
:key='`ext-` + ext.key'
)
blueprint-icon(icon='module')
@ -49,9 +49,9 @@ q-page.admin-extensions
q-tooltip(
anchor='center left'
self='center right'
) {{$t('admin.extensions.installed')}}
) {{t('admin.extensions.installed')}}
q-btn(
:label='$t(`admin.extensions.install`)'
:label='t(`admin.extensions.install`)'
color='blue-7'
v-if='ext.isCompatible && !ext.isInstalled && ext.isInstallable'
@click='install(ext)'
@ -59,21 +59,21 @@ q-page.admin-extensions
)
q-btn(
v-else-if='ext.isCompatible && ext.isInstalled && ext.isInstallable'
:label='$t(`admin.extensions.reinstall`)'
:label='t(`admin.extensions.reinstall`)'
color='blue-7'
@click='install(ext)'
no-caps
)
q-btn(
v-else-if='ext.isCompatible && ext.isInstalled && !ext.isInstallable'
:label='$t(`admin.extensions.installed`)'
:label='t(`admin.extensions.installed`)'
color='positive'
no-caps
:ripple='false'
)
q-btn(
v-else-if='ext.isCompatible'
:label='$t(`admin.extensions.instructions`)'
:label='t(`admin.extensions.instructions`)'
icon='las la-info-circle'
color='indigo'
outline
@ -85,109 +85,132 @@ q-page.admin-extensions
q-tooltip(
anchor='center left'
self='center right'
) {{$t('admin.extensions.instructionsHint')}}
) {{t('admin.extensions.instructionsHint')}}
q-btn(
v-else
color='negative'
outline
:label='$t(`admin.extensions.incompatible`)'
:label='t(`admin.extensions.incompatible`)'
no-caps
:ripple='false'
)
</template>
<script>
<script setup>
import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep'
import { createMetaMixin } from 'quasar'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, watch } from 'vue'
export default {
mixins: [
createMetaMixin(function () {
return {
title: this.$t('admin.extensions.title')
import { useAdminStore } from 'src/stores/admin'
import { useSiteStore } from 'src/stores/site'
import { useDataStore } from 'src/stores/data'
// QUASAR
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const siteStore = useSiteStore()
const dataStore = useDataStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('admin.extensions.title')
})
// DATA
const state = reactive({
loading: false,
extensions: []
})
// METHODS
async function load () {
state.loading++
$q.loading.show()
const resp = await APOLLO_CLIENT.query({
query: gql`
query fetchExtensions {
systemExtensions {
key
title
description
isInstalled
isInstallable
isCompatible
}
}
})
],
data () {
return {
loading: false,
extensions: []
}
},
mounted () {
this.load()
},
methods: {
async load () {
this.loading++
this.$q.loading.show()
const resp = await this.$apollo.query({
query: gql`
query fetchExtensions {
systemExtensions {
key
title
description
isInstalled
isInstallable
isCompatible
}
}
`,
fetchPolicy: 'network-only'
})
this.extensions = cloneDeep(resp?.data?.systemExtensions)
this.$q.loading.hide()
this.loading--
},
async install (ext) {
this.$q.loading.show({
message: this.$t('admin.extensions.installing') + '<br>' + this.$t('admin.extensions.installingHint'),
html: true
})
try {
const respRaw = await this.$apollo.mutate({
mutation: gql`
mutation installExtension (
$key: String!
) {
installExtension (
key: $key
) {
status {
succeeded
message
}
}
`,
fetchPolicy: 'network-only'
})
state.extensions = cloneDeep(resp?.data?.systemExtensions)
$q.loading.hide()
state.loading--
}
async function install (ext) {
$q.loading.show({
message: t('admin.extensions.installing') + '<br>' + t('admin.extensions.installingHint'),
html: true
})
try {
const respRaw = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation installExtension (
$key: String!
) {
installExtension (
key: $key
) {
status {
succeeded
message
}
`,
variables: {
key: ext.key
}
})
if (respRaw.data?.installExtension?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.extensions.installSuccess')
})
ext.isInstalled = true
this.$forceUpdate()
} else {
throw new Error(respRaw.data?.installExtension?.status?.message || 'An unexpected error occured')
}
} catch (err) {
this.$q.notify({
type: 'negative',
message: this.$t('admin.extensions.installFailed'),
caption: err.message
})
`,
variables: {
key: ext.key
}
this.$q.loading.hide()
})
if (respRaw.data?.installExtension?.status?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.extensions.installSuccess')
})
ext.isInstalled = true
// this.$forceUpdate()
} else {
throw new Error(respRaw.data?.installExtension?.status?.message || 'An unexpected error occured')
}
} catch (err) {
$q.notify({
type: 'negative',
message: t('admin.extensions.installFailed'),
caption: err.message
})
}
$q.loading.hide()
}
// MOUNTED
onMounted(() => {
load()
})
</script>
<style lang='scss'>

@ -4,8 +4,8 @@ q-page.admin-mail
.col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-protect.svg')
.col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft {{ $t('admin.security.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.security.subtitle') }}
.text-h5.text-primary.animated.fadeInLeft {{ t('admin.security.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.security.subtitle') }}
.col-auto
q-btn.q-mr-sm.acrylic-btn(
icon='las la-question-circle'
@ -19,16 +19,16 @@ q-page.admin-mail
icon='las la-redo-alt'
flat
color='secondary'
:loading='loading > 0'
:loading='state.loading > 0'
@click='load'
)
q-btn(
unelevated
icon='mdi-check'
:label='$t(`common.actions.apply`)'
icon='fa-solid fa-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
:loading='loading > 0'
:loading='state.loading > 0'
)
q-separator(inset)
.row.q-pa-md.q-col-gutter-md
@ -38,134 +38,134 @@ q-page.admin-mail
//- -----------------------
q-card.shadow-1.q-pb-sm
q-card-section
.text-subtitle1 {{$t('admin.security.title')}}
.text-subtitle1 {{t('admin.security.title')}}
q-item.q-pt-none
q-item-section
q-card.bg-negative.text-white.rounded-borders(flat)
q-card-section.items-center(horizontal)
q-card-section.col-auto.q-pr-none
q-icon(name='las la-exclamation-triangle', size='sm')
q-card-section.text-caption {{ $t('admin.security.warn') }}
q-card-section.text-caption {{ t('admin.security.warn') }}
q-item(tag='label', v-ripple)
blueprint-icon(icon='rfid-signal')
q-item-section
q-item-label {{$t(`admin.security.disallowFloc`)}}
q-item-label(caption) {{$t(`admin.security.disallowFlocHint`)}}
q-item-label {{t(`admin.security.disallowFloc`)}}
q-item-label(caption) {{t(`admin.security.disallowFlocHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.disallowFloc'
v-model='state.config.disallowFloc'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.disallowFloc`)'
:aria-label='t(`admin.security.disallowFloc`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
blueprint-icon(icon='maximize-window')
q-item-section
q-item-label {{$t(`admin.security.disallowIframe`)}}
q-item-label(caption) {{$t(`admin.security.disallowIframeHint`)}}
q-item-label {{t(`admin.security.disallowIframe`)}}
q-item-label(caption) {{t(`admin.security.disallowIframeHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.disallowIframe'
v-model='state.config.disallowIframe'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.disallowIframe`)'
:aria-label='t(`admin.security.disallowIframe`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
blueprint-icon(icon='do-not-touch')
q-item-section
q-item-label {{$t(`admin.security.enforceSameOriginReferrerPolicy`)}}
q-item-label(caption) {{$t(`admin.security.enforceSameOriginReferrerPolicyHint`)}}
q-item-label {{t(`admin.security.enforceSameOriginReferrerPolicy`)}}
q-item-label(caption) {{t(`admin.security.enforceSameOriginReferrerPolicyHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.enforceSameOriginReferrerPolicy'
v-model='state.config.enforceSameOriginReferrerPolicy'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.enforceSameOriginReferrerPolicy`)'
:aria-label='t(`admin.security.enforceSameOriginReferrerPolicy`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
blueprint-icon(icon='curly-arrow')
q-item-section
q-item-label {{$t(`admin.security.disallowOpenRedirect`)}}
q-item-label(caption) {{$t(`admin.security.disallowOpenRedirectHint`)}}
q-item-label {{t(`admin.security.disallowOpenRedirect`)}}
q-item-label(caption) {{t(`admin.security.disallowOpenRedirectHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.disallowOpenRedirect'
v-model='state.config.disallowOpenRedirect'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.disallowOpenRedirect`)'
:aria-label='t(`admin.security.disallowOpenRedirect`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
blueprint-icon(icon='download-from-cloud')
q-item-section
q-item-label {{$t(`admin.security.forceAssetDownload`)}}
q-item-label(caption) {{$t(`admin.security.forceAssetDownloadHint`)}}
q-item-label {{t(`admin.security.forceAssetDownload`)}}
q-item-label(caption) {{t(`admin.security.forceAssetDownloadHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.forceAssetDownload'
v-model='state.config.forceAssetDownload'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.forceAssetDownload`)'
:aria-label='t(`admin.security.forceAssetDownload`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
blueprint-icon(icon='door-sensor-alarmed')
q-item-section
q-item-label {{$t(`admin.security.trustProxy`)}}
q-item-label(caption) {{$t(`admin.security.trustProxyHint`)}}
q-item-label {{t(`admin.security.trustProxy`)}}
q-item-label(caption) {{t(`admin.security.trustProxyHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.trustProxy'
v-model='state.config.trustProxy'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.trustProxy`)'
:aria-label='t(`admin.security.trustProxy`)'
)
//- -----------------------
//- HSTS
//- -----------------------
q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section
.text-subtitle1 {{$t('admin.security.hsts')}}
.text-subtitle1 {{t('admin.security.hsts')}}
q-item(tag='label', v-ripple)
blueprint-icon(icon='hips')
q-item-section
q-item-label {{$t(`admin.security.enforceHsts`)}}
q-item-label(caption) {{$t(`admin.security.enforceHstsHint`)}}
q-item-label {{t(`admin.security.enforceHsts`)}}
q-item-label(caption) {{t(`admin.security.enforceHstsHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.enforceHsts'
v-model='state.config.enforceHsts'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.enforceHsts`)'
:aria-label='t(`admin.security.enforceHsts`)'
)
template(v-if='config.enforceHsts')
template(v-if='state.config.enforceHsts')
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='timer')
q-item-section
q-item-label {{$t(`admin.security.hstsDuration`)}}
q-item-label(caption) {{$t(`admin.security.hstsDurationHint`)}}
q-item-label {{t(`admin.security.hstsDuration`)}}
q-item-label(caption) {{t(`admin.security.hstsDurationHint`)}}
q-item-section(style='flex: 0 0 200px;')
q-select(
outlined
v-model='config.hstsDuration'
v-model='state.config.hstsDuration'
:options='hstsDurations'
option-value='value'
option-label='text'
emit-value
map-options
dense
:aria-label='$t(`admin.security.hstsDuration`)'
:aria-label='t(`admin.security.hstsDuration`)'
)
.col-12.col-lg-6
@ -174,53 +174,53 @@ q-page.admin-mail
//- -----------------------
q-card.shadow-1.q-pb-sm
q-card-section
.text-subtitle1 {{$t('admin.security.uploads')}}
.text-subtitle1 {{t('admin.security.uploads')}}
q-item.q-pt-none
q-item-section
q-card.bg-info.text-white.rounded-borders(flat)
q-card-section.items-center(horizontal)
q-card-section.col-auto.q-pr-none
q-icon(name='las la-info-circle', size='sm')
q-card-section.text-caption {{ $t('admin.security.uploadsInfo') }}
q-card-section.text-caption {{ t('admin.security.uploadsInfo') }}
q-item
blueprint-icon(icon='upload-to-the-cloud')
q-item-section
q-item-label {{$t(`admin.security.maxUploadSize`)}}
q-item-label(caption) {{$t(`admin.security.maxUploadSizeHint`)}}
q-item-label {{t(`admin.security.maxUploadSize`)}}
q-item-label(caption) {{t(`admin.security.maxUploadSizeHint`)}}
q-item-section(style='flex: 0 0 200px;')
q-input(
outlined
v-model.number='humanUploadMaxFileSize'
v-model.number='state.humanUploadMaxFileSize'
dense
:aria-label='$t(`admin.security.maxUploadSize`)'
:aria-label='t(`admin.security.maxUploadSize`)'
)
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='upload-to-ftp')
q-item-section
q-item-label {{$t(`admin.security.maxUploadBatch`)}}
q-item-label(caption) {{$t(`admin.security.maxUploadBatchHint`)}}
q-item-label {{t(`admin.security.maxUploadBatch`)}}
q-item-label(caption) {{t(`admin.security.maxUploadBatchHint`)}}
q-item-section(style='flex: 0 0 200px;')
q-input(
outlined
v-model.number='config.uploadMaxFiles'
v-model.number='state.config.uploadMaxFiles'
dense
:suffix='$t(`admin.security.maxUploadBatchSuffix`)'
:aria-label='$t(`admin.security.maxUploadBatch`)'
:suffix='t(`admin.security.maxUploadBatchSuffix`)'
:aria-label='t(`admin.security.maxUploadBatch`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple)
blueprint-icon(icon='scan-stock')
q-item-section
q-item-label {{$t(`admin.security.scanSVG`)}}
q-item-label(caption) {{$t(`admin.security.scanSVGHint`)}}
q-item-label {{t(`admin.security.scanSVG`)}}
q-item-label(caption) {{t(`admin.security.scanSVGHint`)}}
q-item-section(avatar)
q-toggle(
v-model='config.uploadScanSVG'
v-model='state.config.uploadScanSVG'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='$t(`admin.security.scanSVG`)'
:aria-label='t(`admin.security.scanSVG`)'
)
//- -----------------------
@ -228,52 +228,52 @@ q-page.admin-mail
//- -----------------------
q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section
.text-subtitle1 {{$t('admin.security.cors')}}
.text-subtitle1 {{t('admin.security.cors')}}
q-item
blueprint-icon(icon='firewall')
q-item-section
q-item-label {{$t(`admin.security.corsMode`)}}
q-item-label(caption) {{$t(`admin.security.corsModeHint`)}}
q-item-label {{t(`admin.security.corsMode`)}}
q-item-label(caption) {{t(`admin.security.corsModeHint`)}}
q-item-section
q-select(
outlined
v-model='config.corsMode'
v-model='state.config.corsMode'
:options='corsModes'
option-value='value'
option-label='text'
emit-value
map-options
dense
:aria-label='$t(`admin.security.corsMode`)'
:aria-label='t(`admin.security.corsMode`)'
)
template(v-if='config.corsMode === `HOSTNAMES`')
template(v-if='state.config.corsMode === `HOSTNAMES`')
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='todo-list', key='corsHostnames')
q-item-section
q-item-label {{$t(`admin.security.corsHostnames`)}}
q-item-label(caption) {{$t(`admin.security.corsHostnamesHint`)}}
q-item-label {{t(`admin.security.corsHostnames`)}}
q-item-label(caption) {{t(`admin.security.corsHostnamesHint`)}}
q-item-section
q-input(
outlined
v-model='config.corsConfig'
v-model='state.config.corsConfig'
dense
type='textarea'
:aria-label='$t(`admin.security.corsHostnames`)'
:aria-label='t(`admin.security.corsHostnames`)'
)
template(v-else-if='config.corsMode === `REGEX`')
template(v-else-if='state.config.corsMode === `REGEX`')
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='validation', key='corsRegex')
q-item-section
q-item-label {{$t(`admin.security.corsRegex`)}}
q-item-label(caption) {{$t(`admin.security.corsRegexHint`)}}
q-item-label {{t(`admin.security.corsRegex`)}}
q-item-label(caption) {{t(`admin.security.corsRegexHint`)}}
q-item-section
q-input(
outlined
v-model='config.corsConfig'
v-model='state.config.corsConfig'
dense
:aria-label='$t(`admin.security.corsRegex`)'
:aria-label='t(`admin.security.corsRegex`)'
)
//- -----------------------
@ -281,220 +281,241 @@ q-page.admin-mail
//- -----------------------
q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section
.text-subtitle1 {{$t('admin.security.jwt')}}
.text-subtitle1 {{t('admin.security.jwt')}}
q-item
blueprint-icon(icon='ticket')
q-item-section
q-item-label {{$t(`admin.security.jwtAudience`)}}
q-item-label(caption) {{$t(`admin.security.jwtAudienceHint`)}}
q-item-label {{t(`admin.security.jwtAudience`)}}
q-item-label(caption) {{t(`admin.security.jwtAudienceHint`)}}
q-item-section(style='flex: 0 0 250px;')
q-input(
outlined
v-model='config.authJwtAudience'
v-model='state.config.authJwtAudience'
dense
:aria-label='$t(`admin.security.jwtAudience`)'
:aria-label='t(`admin.security.jwtAudience`)'
)
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='expired')
q-item-section
q-item-label {{$t(`admin.security.tokenExpiration`)}}
q-item-label(caption) {{$t(`admin.security.tokenExpirationHint`)}}
q-item-label {{t(`admin.security.tokenExpiration`)}}
q-item-label(caption) {{t(`admin.security.tokenExpirationHint`)}}
q-item-section(style='flex: 0 0 140px;')
q-input(
outlined
v-model='config.authJwtExpiration'
v-model='state.config.authJwtExpiration'
dense
:aria-label='$t(`admin.security.tokenExpiration`)'
:aria-label='t(`admin.security.tokenExpiration`)'
)
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='future')
q-item-section
q-item-label {{$t(`admin.security.tokenRenewalPeriod`)}}
q-item-label(caption) {{$t(`admin.security.tokenRenewalPeriodHint`)}}
q-item-label {{t(`admin.security.tokenRenewalPeriod`)}}
q-item-label(caption) {{t(`admin.security.tokenRenewalPeriodHint`)}}
q-item-section(style='flex: 0 0 140px;')
q-input(
outlined
v-model='config.authJwtRenewablePeriod'
v-model='state.config.authJwtRenewablePeriod'
dense
:aria-label='$t(`admin.security.tokenRenewalPeriod`)'
:aria-label='t(`admin.security.tokenRenewalPeriod`)'
)
</template>
<script>
<script setup>
import cloneDeep from 'lodash/cloneDeep'
import gql from 'graphql-tag'
import _get from 'lodash/get'
import filesize from 'filesize'
import filesizeParser from 'filesize-parser'
import { createMetaMixin } from 'quasar'
export default {
mixins: [
createMetaMixin(function () {
return {
title: this.$t('admin.security.title')
}
})
],
data () {
return {
loading: false,
config: {
corsConfig: '',
corsMode: 'OFF',
cspDirectives: '',
disallowFloc: false,
disallowIframe: false,
disallowOpenRedirect: false,
enforceCsp: false,
enforceHsts: false,
enforceSameOriginReferrerPolicy: false,
forceAssetDownload: false,
hstsDuration: 0,
trustProxy: false,
authJwtAudience: 'urn:wiki.js',
authJwtExpiration: '30m',
authJwtRenewablePeriod: '14d',
uploadMaxFileSize: 0,
uploadMaxFiles: 0,
uploadScanSVG: false
},
humanUploadMaxFileSize: '0',
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: {
corsModes () {
return [
{ value: 'OFF', text: 'Off / Same-Origin' },
{ value: 'REFLECT', text: 'Reflect Request Origin' },
{ value: 'HOSTNAMES', text: 'Hostnames Whitelist' },
{ value: 'REGEX', text: 'Regex Pattern Match' }
]
}
},
mounted () {
this.load()
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
import { computed, onMounted, reactive, watch } from 'vue'
import { useAdminStore } from 'src/stores/admin'
import { useSiteStore } from 'src/stores/site'
import { useDataStore } from 'src/stores/data'
// QUASAR
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
const siteStore = useSiteStore()
const dataStore = useDataStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('admin.security.title')
})
// DATA
const state = reactive({
loading: false,
config: {
corsConfig: '',
corsMode: 'OFF',
cspDirectives: '',
disallowFloc: false,
disallowIframe: false,
disallowOpenRedirect: false,
enforceCsp: false,
enforceHsts: false,
enforceSameOriginReferrerPolicy: false,
forceAssetDownload: false,
hstsDuration: 0,
trustProxy: false,
authJwtAudience: 'urn:wiki.js',
authJwtExpiration: '30m',
authJwtRenewablePeriod: '14d',
uploadMaxFileSize: 0,
uploadMaxFiles: 0,
uploadScanSVG: false
},
methods: {
async load () {
this.loading++
this.$q.loading.show()
const resp = await this.$apollo.query({
query: gql`
query getSecurityConfig {
systemSecurity {
authJwtAudience
authJwtExpiration
authJwtRenewablePeriod
corsConfig
corsMode
cspDirectives
disallowFloc
disallowIframe
disallowOpenRedirect
enforceCsp
enforceHsts
enforceSameOriginReferrerPolicy
forceAssetDownload
hstsDuration
trustProxy
uploadMaxFileSize
uploadMaxFiles
uploadScanSVG
}
}
`,
fetchPolicy: 'network-only'
})
this.config = cloneDeep(resp?.data?.systemSecurity)
this.humanUploadMaxFileSize = filesize(this.config.uploadMaxFileSize ?? 0, { base: 2, standard: 'jedec' })
this.$q.loading.hide()
this.loading--
},
async save () {
this.loading = true
try {
const respRaw = await this.$apollo.mutate({
mutation: gql`
mutation saveSecurityConfig (
$authJwtAudience: String
$authJwtExpiration: String
$authJwtRenewablePeriod: String
$corsConfig: String
$corsMode: SystemSecurityCorsMode
$cspDirectives: String
$disallowFloc: Boolean
$disallowIframe: Boolean
$disallowOpenRedirect: Boolean
$enforceCsp: Boolean
$enforceHsts: Boolean
$enforceSameOriginReferrerPolicy: Boolean
$hstsDuration: Int
$trustProxy: Boolean
$uploadMaxFiles: Int
$uploadMaxFileSize: Int
) {
updateSystemSecurity(
authJwtAudience: $authJwtAudience
authJwtExpiration: $authJwtExpiration
authJwtRenewablePeriod: $authJwtRenewablePeriod
corsConfig: $corsConfig
corsMode: $corsMode
cspDirectives: $cspDirectives
disallowFloc: $disallowFloc
disallowIframe: $disallowIframe
disallowOpenRedirect: $disallowOpenRedirect
enforceCsp: $enforceCsp
enforceHsts: $enforceHsts
enforceSameOriginReferrerPolicy: $enforceSameOriginReferrerPolicy
hstsDuration: $hstsDuration
trustProxy: $trustProxy
uploadMaxFiles: $uploadMaxFiles
uploadMaxFileSize: $uploadMaxFileSize
) {
status {
succeeded
slug
message
}
}
humanUploadMaxFileSize: '0'
})
const 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' }
]
const corsModes = [
{ value: 'OFF', text: 'Off / Same-Origin' },
{ value: 'REFLECT', text: 'Reflect Request Origin' },
{ value: 'HOSTNAMES', text: 'Hostnames Whitelist' },
{ value: 'REGEX', text: 'Regex Pattern Match' }
]
// METHODS
async function load () {
state.loading++
$q.loading.show()
const resp = await APOLLO_CLIENT.query({
query: gql`
query getSecurityConfig {
systemSecurity {
authJwtAudience
authJwtExpiration
authJwtRenewablePeriod
corsConfig
corsMode
cspDirectives
disallowFloc
disallowIframe
disallowOpenRedirect
enforceCsp
enforceHsts
enforceSameOriginReferrerPolicy
forceAssetDownload
hstsDuration
trustProxy
uploadMaxFileSize
uploadMaxFiles
uploadScanSVG
}
}
`,
fetchPolicy: 'network-only'
})
state.config = cloneDeep(resp?.data?.systemSecurity)
state.humanUploadMaxFileSize = filesize(state.config.uploadMaxFileSize ?? 0, { base: 2, standard: 'jedec' })
$q.loading.hide()
state.loading--
}
async function save () {
state.loading++
try {
const respRaw = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation saveSecurityConfig (
$authJwtAudience: String
$authJwtExpiration: String
$authJwtRenewablePeriod: String
$corsConfig: String
$corsMode: SystemSecurityCorsMode
$cspDirectives: String
$disallowFloc: Boolean
$disallowIframe: Boolean
$disallowOpenRedirect: Boolean
$enforceCsp: Boolean
$enforceHsts: Boolean
$enforceSameOriginReferrerPolicy: Boolean
$hstsDuration: Int
$trustProxy: Boolean
$uploadMaxFiles: Int
$uploadMaxFileSize: Int
) {
updateSystemSecurity(
authJwtAudience: $authJwtAudience
authJwtExpiration: $authJwtExpiration
authJwtRenewablePeriod: $authJwtRenewablePeriod
corsConfig: $corsConfig
corsMode: $corsMode
cspDirectives: $cspDirectives
disallowFloc: $disallowFloc
disallowIframe: $disallowIframe
disallowOpenRedirect: $disallowOpenRedirect
enforceCsp: $enforceCsp
enforceHsts: $enforceHsts
enforceSameOriginReferrerPolicy: $enforceSameOriginReferrerPolicy
hstsDuration: $hstsDuration
trustProxy: $trustProxy
uploadMaxFiles: $uploadMaxFiles
uploadMaxFileSize: $uploadMaxFileSize
) {
status {
succeeded
slug
message
}
`,
variables: {
...this.config,
uploadMaxFileSize: filesizeParser(this.humanUploadMaxFileSize || '0')
}
})
const resp = _get(respRaw, 'data.updateSystemSecurity.status', {})
if (resp.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.security.saveSuccess')
})
} else {
throw new Error(resp.message)
}
} catch (err) {
this.$q.notify({
type: 'negative',
message: 'Failed to save security config',
caption: err.message
})
`,
variables: {
...state.config,
uploadMaxFileSize: filesizeParser(state.humanUploadMaxFileSize || '0')
}
this.loading = false
})
const resp = _get(respRaw, 'data.updateSystemSecurity.status', {})
if (resp.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.security.saveSuccess')
})
} else {
throw new Error(resp.message)
}
} catch (err) {
$q.notify({
type: 'negative',
message: 'Failed to save security config',
caption: err.message
})
}
state.loading--
}
// MOUNTED
onMounted(() => {
load()
})
</script>
<style lang='scss'>

@ -45,9 +45,9 @@ const routes = [
// { path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
// -> System
// { path: 'api', component: () => import('../pages/AdminApi.vue') },
// { path: 'extensions', component: () => import('../pages/AdminExtensions.vue') },
{ path: 'extensions', component: () => import('../pages/AdminExtensions.vue') },
{ path: 'mail', component: () => import('../pages/AdminMail.vue') },
// { path: 'security', component: () => import('../pages/AdminSecurity.vue') },
{ path: 'security', component: () => import('../pages/AdminSecurity.vue') },
{ path: 'system', component: () => import('../pages/AdminSystem.vue') },
// { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') },
// { path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') },

Loading…
Cancel
Save