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

parent 3d2e5c9d
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
}, },
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"ux/src/i18n/locales" "ux/src/i18n/locales"
] ],
"i18n-ally.keystyle": "nested"
} }
...@@ -4,8 +4,8 @@ q-page.admin-extensions ...@@ -4,8 +4,8 @@ q-page.admin-extensions
.col-auto .col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-module.svg') img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-module.svg')
.col.q-pl-md .col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft {{ $t('admin.extensions.title') }} .text-h5.text-primary.animated.fadeInLeft {{ t('admin.extensions.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.extensions.subtitle') }} .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.extensions.subtitle') }}
.col-auto .col-auto
q-btn.acrylic-btn.q-mr-sm( q-btn.acrylic-btn.q-mr-sm(
icon='las la-question-circle' icon='las la-question-circle'
...@@ -19,7 +19,7 @@ q-page.admin-extensions ...@@ -19,7 +19,7 @@ q-page.admin-extensions
icon='las la-redo-alt' icon='las la-redo-alt'
flat flat
color='secondary' color='secondary'
:loading='loading > 0' :loading='state.loading > 0'
@click='load' @click='load'
) )
q-separator(inset) q-separator(inset)
...@@ -28,7 +28,7 @@ q-page.admin-extensions ...@@ -28,7 +28,7 @@ q-page.admin-extensions
q-card.shadow-1 q-card.shadow-1
q-list(separator) q-list(separator)
q-item( q-item(
v-for='(ext, idx) of extensions' v-for='(ext, idx) of state.extensions'
:key='`ext-` + ext.key' :key='`ext-` + ext.key'
) )
blueprint-icon(icon='module') blueprint-icon(icon='module')
...@@ -49,9 +49,9 @@ q-page.admin-extensions ...@@ -49,9 +49,9 @@ q-page.admin-extensions
q-tooltip( q-tooltip(
anchor='center left' anchor='center left'
self='center right' self='center right'
) {{$t('admin.extensions.installed')}} ) {{t('admin.extensions.installed')}}
q-btn( q-btn(
:label='$t(`admin.extensions.install`)' :label='t(`admin.extensions.install`)'
color='blue-7' color='blue-7'
v-if='ext.isCompatible && !ext.isInstalled && ext.isInstallable' v-if='ext.isCompatible && !ext.isInstalled && ext.isInstallable'
@click='install(ext)' @click='install(ext)'
...@@ -59,21 +59,21 @@ q-page.admin-extensions ...@@ -59,21 +59,21 @@ q-page.admin-extensions
) )
q-btn( q-btn(
v-else-if='ext.isCompatible && ext.isInstalled && ext.isInstallable' v-else-if='ext.isCompatible && ext.isInstalled && ext.isInstallable'
:label='$t(`admin.extensions.reinstall`)' :label='t(`admin.extensions.reinstall`)'
color='blue-7' color='blue-7'
@click='install(ext)' @click='install(ext)'
no-caps no-caps
) )
q-btn( q-btn(
v-else-if='ext.isCompatible && ext.isInstalled && !ext.isInstallable' v-else-if='ext.isCompatible && ext.isInstalled && !ext.isInstallable'
:label='$t(`admin.extensions.installed`)' :label='t(`admin.extensions.installed`)'
color='positive' color='positive'
no-caps no-caps
:ripple='false' :ripple='false'
) )
q-btn( q-btn(
v-else-if='ext.isCompatible' v-else-if='ext.isCompatible'
:label='$t(`admin.extensions.instructions`)' :label='t(`admin.extensions.instructions`)'
icon='las la-info-circle' icon='las la-info-circle'
color='indigo' color='indigo'
outline outline
...@@ -85,109 +85,132 @@ q-page.admin-extensions ...@@ -85,109 +85,132 @@ q-page.admin-extensions
q-tooltip( q-tooltip(
anchor='center left' anchor='center left'
self='center right' self='center right'
) {{$t('admin.extensions.instructionsHint')}} ) {{t('admin.extensions.instructionsHint')}}
q-btn( q-btn(
v-else v-else
color='negative' color='negative'
outline outline
:label='$t(`admin.extensions.incompatible`)' :label='t(`admin.extensions.incompatible`)'
no-caps no-caps
:ripple='false' :ripple='false'
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep' 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 { import { useAdminStore } from 'src/stores/admin'
mixins: [ import { useSiteStore } from 'src/stores/site'
createMetaMixin(function () { import { useDataStore } from 'src/stores/data'
return {
title: this.$t('admin.extensions.title') // 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
}
} }
}) `,
], fetchPolicy: 'network-only'
data () { })
return { state.extensions = cloneDeep(resp?.data?.systemExtensions)
loading: false, $q.loading.hide()
extensions: [] state.loading--
} }
},
mounted () { async function install (ext) {
this.load() $q.loading.show({
}, message: t('admin.extensions.installing') + '<br>' + t('admin.extensions.installingHint'),
methods: { html: true
async load () { })
this.loading++ try {
this.$q.loading.show() const respRaw = await APOLLO_CLIENT.mutate({
const resp = await this.$apollo.query({ mutation: gql`
query: gql` mutation installExtension (
query fetchExtensions { $key: String!
systemExtensions { ) {
key installExtension (
title key: $key
description ) {
isInstalled status {
isInstallable succeeded
isCompatible message
}
}
`,
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
}
}
} }
`,
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({ variables: {
type: 'negative', key: ext.key
message: this.$t('admin.extensions.installFailed'),
caption: err.message
})
} }
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> </script>
<style lang='scss'> <style lang='scss'>
......
...@@ -4,8 +4,8 @@ q-page.admin-mail ...@@ -4,8 +4,8 @@ q-page.admin-mail
.col-auto .col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-protect.svg') img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-protect.svg')
.col.q-pl-md .col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft {{ $t('admin.security.title') }} .text-h5.text-primary.animated.fadeInLeft {{ t('admin.security.title') }}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.security.subtitle') }} .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.security.subtitle') }}
.col-auto .col-auto
q-btn.q-mr-sm.acrylic-btn( q-btn.q-mr-sm.acrylic-btn(
icon='las la-question-circle' icon='las la-question-circle'
...@@ -19,16 +19,16 @@ q-page.admin-mail ...@@ -19,16 +19,16 @@ q-page.admin-mail
icon='las la-redo-alt' icon='las la-redo-alt'
flat flat
color='secondary' color='secondary'
:loading='loading > 0' :loading='state.loading > 0'
@click='load' @click='load'
) )
q-btn( q-btn(
unelevated unelevated
icon='mdi-check' icon='fa-solid fa-check'
:label='$t(`common.actions.apply`)' :label='t(`common.actions.apply`)'
color='secondary' color='secondary'
@click='save' @click='save'
:loading='loading > 0' :loading='state.loading > 0'
) )
q-separator(inset) q-separator(inset)
.row.q-pa-md.q-col-gutter-md .row.q-pa-md.q-col-gutter-md
...@@ -38,134 +38,134 @@ q-page.admin-mail ...@@ -38,134 +38,134 @@ q-page.admin-mail
//- ----------------------- //- -----------------------
q-card.shadow-1.q-pb-sm q-card.shadow-1.q-pb-sm
q-card-section q-card-section
.text-subtitle1 {{$t('admin.security.title')}} .text-subtitle1 {{t('admin.security.title')}}
q-item.q-pt-none q-item.q-pt-none
q-item-section q-item-section
q-card.bg-negative.text-white.rounded-borders(flat) q-card.bg-negative.text-white.rounded-borders(flat)
q-card-section.items-center(horizontal) q-card-section.items-center(horizontal)
q-card-section.col-auto.q-pr-none q-card-section.col-auto.q-pr-none
q-icon(name='las la-exclamation-triangle', size='sm') 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) q-item(tag='label', v-ripple)
blueprint-icon(icon='rfid-signal') blueprint-icon(icon='rfid-signal')
q-item-section q-item-section
q-item-label {{$t(`admin.security.disallowFloc`)}} q-item-label {{t(`admin.security.disallowFloc`)}}
q-item-label(caption) {{$t(`admin.security.disallowFlocHint`)}} q-item-label(caption) {{t(`admin.security.disallowFlocHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.disallowFloc' v-model='state.config.disallowFloc'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.security.disallowFloc`)' :aria-label='t(`admin.security.disallowFloc`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='maximize-window') blueprint-icon(icon='maximize-window')
q-item-section q-item-section
q-item-label {{$t(`admin.security.disallowIframe`)}} q-item-label {{t(`admin.security.disallowIframe`)}}
q-item-label(caption) {{$t(`admin.security.disallowIframeHint`)}} q-item-label(caption) {{t(`admin.security.disallowIframeHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.disallowIframe' v-model='state.config.disallowIframe'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.security.disallowIframe`)' :aria-label='t(`admin.security.disallowIframe`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='do-not-touch') blueprint-icon(icon='do-not-touch')
q-item-section q-item-section
q-item-label {{$t(`admin.security.enforceSameOriginReferrerPolicy`)}} q-item-label {{t(`admin.security.enforceSameOriginReferrerPolicy`)}}
q-item-label(caption) {{$t(`admin.security.enforceSameOriginReferrerPolicyHint`)}} q-item-label(caption) {{t(`admin.security.enforceSameOriginReferrerPolicyHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.enforceSameOriginReferrerPolicy' v-model='state.config.enforceSameOriginReferrerPolicy'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.security.enforceSameOriginReferrerPolicy`)' :aria-label='t(`admin.security.enforceSameOriginReferrerPolicy`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='curly-arrow') blueprint-icon(icon='curly-arrow')
q-item-section q-item-section
q-item-label {{$t(`admin.security.disallowOpenRedirect`)}} q-item-label {{t(`admin.security.disallowOpenRedirect`)}}
q-item-label(caption) {{$t(`admin.security.disallowOpenRedirectHint`)}} q-item-label(caption) {{t(`admin.security.disallowOpenRedirectHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.disallowOpenRedirect' v-model='state.config.disallowOpenRedirect'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.security.disallowOpenRedirect`)' :aria-label='t(`admin.security.disallowOpenRedirect`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='download-from-cloud') blueprint-icon(icon='download-from-cloud')
q-item-section q-item-section
q-item-label {{$t(`admin.security.forceAssetDownload`)}} q-item-label {{t(`admin.security.forceAssetDownload`)}}
q-item-label(caption) {{$t(`admin.security.forceAssetDownloadHint`)}} q-item-label(caption) {{t(`admin.security.forceAssetDownloadHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.forceAssetDownload' v-model='state.config.forceAssetDownload'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.security.forceAssetDownload`)' :aria-label='t(`admin.security.forceAssetDownload`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='door-sensor-alarmed') blueprint-icon(icon='door-sensor-alarmed')
q-item-section q-item-section
q-item-label {{$t(`admin.security.trustProxy`)}} q-item-label {{t(`admin.security.trustProxy`)}}
q-item-label(caption) {{$t(`admin.security.trustProxyHint`)}} q-item-label(caption) {{t(`admin.security.trustProxyHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.trustProxy' v-model='state.config.trustProxy'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.security.trustProxy`)' :aria-label='t(`admin.security.trustProxy`)'
) )
//- ----------------------- //- -----------------------
//- HSTS //- HSTS
//- ----------------------- //- -----------------------
q-card.shadow-1.q-pb-sm.q-mt-md q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section q-card-section
.text-subtitle1 {{$t('admin.security.hsts')}} .text-subtitle1 {{t('admin.security.hsts')}}
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='hips') blueprint-icon(icon='hips')
q-item-section q-item-section
q-item-label {{$t(`admin.security.enforceHsts`)}} q-item-label {{t(`admin.security.enforceHsts`)}}
q-item-label(caption) {{$t(`admin.security.enforceHstsHint`)}} q-item-label(caption) {{t(`admin.security.enforceHstsHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.enforceHsts' v-model='state.config.enforceHsts'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' 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-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='timer') blueprint-icon(icon='timer')
q-item-section q-item-section
q-item-label {{$t(`admin.security.hstsDuration`)}} q-item-label {{t(`admin.security.hstsDuration`)}}
q-item-label(caption) {{$t(`admin.security.hstsDurationHint`)}} q-item-label(caption) {{t(`admin.security.hstsDurationHint`)}}
q-item-section(style='flex: 0 0 200px;') q-item-section(style='flex: 0 0 200px;')
q-select( q-select(
outlined outlined
v-model='config.hstsDuration' v-model='state.config.hstsDuration'
:options='hstsDurations' :options='hstsDurations'
option-value='value' option-value='value'
option-label='text' option-label='text'
emit-value emit-value
map-options map-options
dense dense
:aria-label='$t(`admin.security.hstsDuration`)' :aria-label='t(`admin.security.hstsDuration`)'
) )
.col-12.col-lg-6 .col-12.col-lg-6
...@@ -174,53 +174,53 @@ q-page.admin-mail ...@@ -174,53 +174,53 @@ q-page.admin-mail
//- ----------------------- //- -----------------------
q-card.shadow-1.q-pb-sm q-card.shadow-1.q-pb-sm
q-card-section q-card-section
.text-subtitle1 {{$t('admin.security.uploads')}} .text-subtitle1 {{t('admin.security.uploads')}}
q-item.q-pt-none q-item.q-pt-none
q-item-section q-item-section
q-card.bg-info.text-white.rounded-borders(flat) q-card.bg-info.text-white.rounded-borders(flat)
q-card-section.items-center(horizontal) q-card-section.items-center(horizontal)
q-card-section.col-auto.q-pr-none q-card-section.col-auto.q-pr-none
q-icon(name='las la-info-circle', size='sm') 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 q-item
blueprint-icon(icon='upload-to-the-cloud') blueprint-icon(icon='upload-to-the-cloud')
q-item-section q-item-section
q-item-label {{$t(`admin.security.maxUploadSize`)}} q-item-label {{t(`admin.security.maxUploadSize`)}}
q-item-label(caption) {{$t(`admin.security.maxUploadSizeHint`)}} q-item-label(caption) {{t(`admin.security.maxUploadSizeHint`)}}
q-item-section(style='flex: 0 0 200px;') q-item-section(style='flex: 0 0 200px;')
q-input( q-input(
outlined outlined
v-model.number='humanUploadMaxFileSize' v-model.number='state.humanUploadMaxFileSize'
dense dense
:aria-label='$t(`admin.security.maxUploadSize`)' :aria-label='t(`admin.security.maxUploadSize`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='upload-to-ftp') blueprint-icon(icon='upload-to-ftp')
q-item-section q-item-section
q-item-label {{$t(`admin.security.maxUploadBatch`)}} q-item-label {{t(`admin.security.maxUploadBatch`)}}
q-item-label(caption) {{$t(`admin.security.maxUploadBatchHint`)}} q-item-label(caption) {{t(`admin.security.maxUploadBatchHint`)}}
q-item-section(style='flex: 0 0 200px;') q-item-section(style='flex: 0 0 200px;')
q-input( q-input(
outlined outlined
v-model.number='config.uploadMaxFiles' v-model.number='state.config.uploadMaxFiles'
dense dense
:suffix='$t(`admin.security.maxUploadBatchSuffix`)' :suffix='t(`admin.security.maxUploadBatchSuffix`)'
:aria-label='$t(`admin.security.maxUploadBatch`)' :aria-label='t(`admin.security.maxUploadBatch`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item(tag='label', v-ripple) q-item(tag='label', v-ripple)
blueprint-icon(icon='scan-stock') blueprint-icon(icon='scan-stock')
q-item-section q-item-section
q-item-label {{$t(`admin.security.scanSVG`)}} q-item-label {{t(`admin.security.scanSVG`)}}
q-item-label(caption) {{$t(`admin.security.scanSVGHint`)}} q-item-label(caption) {{t(`admin.security.scanSVGHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='config.uploadScanSVG' v-model='state.config.uploadScanSVG'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' 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 ...@@ -228,52 +228,52 @@ q-page.admin-mail
//- ----------------------- //- -----------------------
q-card.shadow-1.q-pb-sm.q-mt-md q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section q-card-section
.text-subtitle1 {{$t('admin.security.cors')}} .text-subtitle1 {{t('admin.security.cors')}}
q-item q-item
blueprint-icon(icon='firewall') blueprint-icon(icon='firewall')
q-item-section q-item-section
q-item-label {{$t(`admin.security.corsMode`)}} q-item-label {{t(`admin.security.corsMode`)}}
q-item-label(caption) {{$t(`admin.security.corsModeHint`)}} q-item-label(caption) {{t(`admin.security.corsModeHint`)}}
q-item-section q-item-section
q-select( q-select(
outlined outlined
v-model='config.corsMode' v-model='state.config.corsMode'
:options='corsModes' :options='corsModes'
option-value='value' option-value='value'
option-label='text' option-label='text'
emit-value emit-value
map-options map-options
dense 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-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='todo-list', key='corsHostnames') blueprint-icon(icon='todo-list', key='corsHostnames')
q-item-section q-item-section
q-item-label {{$t(`admin.security.corsHostnames`)}} q-item-label {{t(`admin.security.corsHostnames`)}}
q-item-label(caption) {{$t(`admin.security.corsHostnamesHint`)}} q-item-label(caption) {{t(`admin.security.corsHostnamesHint`)}}
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='config.corsConfig' v-model='state.config.corsConfig'
dense dense
type='textarea' 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-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='validation', key='corsRegex') blueprint-icon(icon='validation', key='corsRegex')
q-item-section q-item-section
q-item-label {{$t(`admin.security.corsRegex`)}} q-item-label {{t(`admin.security.corsRegex`)}}
q-item-label(caption) {{$t(`admin.security.corsRegexHint`)}} q-item-label(caption) {{t(`admin.security.corsRegexHint`)}}
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='config.corsConfig' v-model='state.config.corsConfig'
dense dense
:aria-label='$t(`admin.security.corsRegex`)' :aria-label='t(`admin.security.corsRegex`)'
) )
//- ----------------------- //- -----------------------
...@@ -281,220 +281,241 @@ q-page.admin-mail ...@@ -281,220 +281,241 @@ q-page.admin-mail
//- ----------------------- //- -----------------------
q-card.shadow-1.q-pb-sm.q-mt-md q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section q-card-section
.text-subtitle1 {{$t('admin.security.jwt')}} .text-subtitle1 {{t('admin.security.jwt')}}
q-item q-item
blueprint-icon(icon='ticket') blueprint-icon(icon='ticket')
q-item-section q-item-section
q-item-label {{$t(`admin.security.jwtAudience`)}} q-item-label {{t(`admin.security.jwtAudience`)}}
q-item-label(caption) {{$t(`admin.security.jwtAudienceHint`)}} q-item-label(caption) {{t(`admin.security.jwtAudienceHint`)}}
q-item-section(style='flex: 0 0 250px;') q-item-section(style='flex: 0 0 250px;')
q-input( q-input(
outlined outlined
v-model='config.authJwtAudience' v-model='state.config.authJwtAudience'
dense dense
:aria-label='$t(`admin.security.jwtAudience`)' :aria-label='t(`admin.security.jwtAudience`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='expired') blueprint-icon(icon='expired')
q-item-section q-item-section
q-item-label {{$t(`admin.security.tokenExpiration`)}} q-item-label {{t(`admin.security.tokenExpiration`)}}
q-item-label(caption) {{$t(`admin.security.tokenExpirationHint`)}} q-item-label(caption) {{t(`admin.security.tokenExpirationHint`)}}
q-item-section(style='flex: 0 0 140px;') q-item-section(style='flex: 0 0 140px;')
q-input( q-input(
outlined outlined
v-model='config.authJwtExpiration' v-model='state.config.authJwtExpiration'
dense dense
:aria-label='$t(`admin.security.tokenExpiration`)' :aria-label='t(`admin.security.tokenExpiration`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='future') blueprint-icon(icon='future')
q-item-section q-item-section
q-item-label {{$t(`admin.security.tokenRenewalPeriod`)}} q-item-label {{t(`admin.security.tokenRenewalPeriod`)}}
q-item-label(caption) {{$t(`admin.security.tokenRenewalPeriodHint`)}} q-item-label(caption) {{t(`admin.security.tokenRenewalPeriodHint`)}}
q-item-section(style='flex: 0 0 140px;') q-item-section(style='flex: 0 0 140px;')
q-input( q-input(
outlined outlined
v-model='config.authJwtRenewablePeriod' v-model='state.config.authJwtRenewablePeriod'
dense dense
:aria-label='$t(`admin.security.tokenRenewalPeriod`)' :aria-label='t(`admin.security.tokenRenewalPeriod`)'
) )
</template> </template>
<script> <script setup>
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import _get from 'lodash/get' import _get from 'lodash/get'
import filesize from 'filesize' import filesize from 'filesize'
import filesizeParser from 'filesize-parser' import filesizeParser from 'filesize-parser'
import { createMetaMixin } from 'quasar'
export default { import { useI18n } from 'vue-i18n'
mixins: [ import { useMeta, useQuasar } from 'quasar'
createMetaMixin(function () { import { computed, onMounted, reactive, watch } from 'vue'
return {
title: this.$t('admin.security.title') import { useAdminStore } from 'src/stores/admin'
} import { useSiteStore } from 'src/stores/site'
}) import { useDataStore } from 'src/stores/data'
],
data () { // QUASAR
return {
loading: false, const $q = useQuasar()
config: {
corsConfig: '', // STORES
corsMode: 'OFF',
cspDirectives: '', const adminStore = useAdminStore()
disallowFloc: false, const siteStore = useSiteStore()
disallowIframe: false, const dataStore = useDataStore()
disallowOpenRedirect: false,
enforceCsp: false, // I18N
enforceHsts: false,
enforceSameOriginReferrerPolicy: false, const { t } = useI18n()
forceAssetDownload: false,
hstsDuration: 0, // META
trustProxy: false,
authJwtAudience: 'urn:wiki.js', useMeta({
authJwtExpiration: '30m', title: t('admin.security.title')
authJwtRenewablePeriod: '14d', })
uploadMaxFileSize: 0,
uploadMaxFiles: 0, // DATA
uploadScanSVG: false
}, const state = reactive({
humanUploadMaxFileSize: '0', loading: false,
hstsDurations: [ config: {
{ value: 300, text: '5 minutes' }, corsConfig: '',
{ value: 86400, text: '1 day' }, corsMode: 'OFF',
{ value: 604800, text: '1 week' }, cspDirectives: '',
{ value: 2592000, text: '1 month' }, disallowFloc: false,
{ value: 31536000, text: '1 year' }, disallowIframe: false,
{ value: 63072000, text: '2 years' } disallowOpenRedirect: false,
] enforceCsp: false,
} enforceHsts: false,
}, enforceSameOriginReferrerPolicy: false,
computed: { forceAssetDownload: false,
corsModes () { hstsDuration: 0,
return [ trustProxy: false,
{ value: 'OFF', text: 'Off / Same-Origin' }, authJwtAudience: 'urn:wiki.js',
{ value: 'REFLECT', text: 'Reflect Request Origin' }, authJwtExpiration: '30m',
{ value: 'HOSTNAMES', text: 'Hostnames Whitelist' }, authJwtRenewablePeriod: '14d',
{ value: 'REGEX', text: 'Regex Pattern Match' } uploadMaxFileSize: 0,
] uploadMaxFiles: 0,
} uploadScanSVG: false
},
mounted () {
this.load()
}, },
methods: { humanUploadMaxFileSize: '0'
async load () { })
this.loading++
this.$q.loading.show() const hstsDurations = [
const resp = await this.$apollo.query({ { value: 300, text: '5 minutes' },
query: gql` { value: 86400, text: '1 day' },
query getSecurityConfig { { value: 604800, text: '1 week' },
systemSecurity { { value: 2592000, text: '1 month' },
authJwtAudience { value: 31536000, text: '1 year' },
authJwtExpiration { value: 63072000, text: '2 years' }
authJwtRenewablePeriod ]
corsConfig
corsMode const corsModes = [
cspDirectives { value: 'OFF', text: 'Off / Same-Origin' },
disallowFloc { value: 'REFLECT', text: 'Reflect Request Origin' },
disallowIframe { value: 'HOSTNAMES', text: 'Hostnames Whitelist' },
disallowOpenRedirect { value: 'REGEX', text: 'Regex Pattern Match' }
enforceCsp ]
enforceHsts
enforceSameOriginReferrerPolicy // METHODS
forceAssetDownload
hstsDuration async function load () {
trustProxy state.loading++
uploadMaxFileSize $q.loading.show()
uploadMaxFiles const resp = await APOLLO_CLIENT.query({
uploadScanSVG query: gql`
} query getSecurityConfig {
} systemSecurity {
`, authJwtAudience
fetchPolicy: 'network-only' authJwtExpiration
}) authJwtRenewablePeriod
this.config = cloneDeep(resp?.data?.systemSecurity) corsConfig
this.humanUploadMaxFileSize = filesize(this.config.uploadMaxFileSize ?? 0, { base: 2, standard: 'jedec' }) corsMode
this.$q.loading.hide() cspDirectives
this.loading-- disallowFloc
}, disallowIframe
async save () { disallowOpenRedirect
this.loading = true enforceCsp
try { enforceHsts
const respRaw = await this.$apollo.mutate({ enforceSameOriginReferrerPolicy
mutation: gql` forceAssetDownload
mutation saveSecurityConfig ( hstsDuration
$authJwtAudience: String trustProxy
$authJwtExpiration: String uploadMaxFileSize
$authJwtRenewablePeriod: String uploadMaxFiles
$corsConfig: String uploadScanSVG
$corsMode: SystemSecurityCorsMode }
$cspDirectives: String }
$disallowFloc: Boolean `,
$disallowIframe: Boolean fetchPolicy: 'network-only'
$disallowOpenRedirect: Boolean })
$enforceCsp: Boolean state.config = cloneDeep(resp?.data?.systemSecurity)
$enforceHsts: Boolean state.humanUploadMaxFileSize = filesize(state.config.uploadMaxFileSize ?? 0, { base: 2, standard: 'jedec' })
$enforceSameOriginReferrerPolicy: Boolean $q.loading.hide()
$hstsDuration: Int state.loading--
$trustProxy: Boolean }
$uploadMaxFiles: Int
$uploadMaxFileSize: Int async function save () {
) { state.loading++
updateSystemSecurity( try {
authJwtAudience: $authJwtAudience const respRaw = await APOLLO_CLIENT.mutate({
authJwtExpiration: $authJwtExpiration mutation: gql`
authJwtRenewablePeriod: $authJwtRenewablePeriod mutation saveSecurityConfig (
corsConfig: $corsConfig $authJwtAudience: String
corsMode: $corsMode $authJwtExpiration: String
cspDirectives: $cspDirectives $authJwtRenewablePeriod: String
disallowFloc: $disallowFloc $corsConfig: String
disallowIframe: $disallowIframe $corsMode: SystemSecurityCorsMode
disallowOpenRedirect: $disallowOpenRedirect $cspDirectives: String
enforceCsp: $enforceCsp $disallowFloc: Boolean
enforceHsts: $enforceHsts $disallowIframe: Boolean
enforceSameOriginReferrerPolicy: $enforceSameOriginReferrerPolicy $disallowOpenRedirect: Boolean
hstsDuration: $hstsDuration $enforceCsp: Boolean
trustProxy: $trustProxy $enforceHsts: Boolean
uploadMaxFiles: $uploadMaxFiles $enforceSameOriginReferrerPolicy: Boolean
uploadMaxFileSize: $uploadMaxFileSize $hstsDuration: Int
) { $trustProxy: Boolean
status { $uploadMaxFiles: Int
succeeded $uploadMaxFileSize: Int
slug ) {
message 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({ variables: {
type: 'negative', ...state.config,
message: 'Failed to save security config', uploadMaxFileSize: filesizeParser(state.humanUploadMaxFileSize || '0')
caption: err.message
})
} }
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> </script>
<style lang='scss'> <style lang='scss'>
......
...@@ -45,9 +45,9 @@ const routes = [ ...@@ -45,9 +45,9 @@ const routes = [
// { path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') }, // { path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
// -> System // -> System
// { path: 'api', component: () => import('../pages/AdminApi.vue') }, // { 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: '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: 'system', component: () => import('../pages/AdminSystem.vue') },
// { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') }, // { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') },
// { path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') }, // { path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment