123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- <template lang='pug'>
- q-page.admin-mail
- .row.q-pa-md.items-center
- .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') }}
- .col-auto
- q-btn.q-mr-sm.acrylic-btn(
- icon='las la-question-circle'
- flat
- color='grey'
- href='https://docs.js.wiki/admin/security'
- target='_blank'
- type='a'
- )
- q-btn.q-mr-sm.acrylic-btn(
- icon='las la-redo-alt'
- flat
- color='secondary'
- :loading='state.loading > 0'
- @click='load'
- )
- q-btn(
- unelevated
- icon='fa-solid fa-check'
- :label='t(`common.actions.apply`)'
- color='secondary'
- @click='save'
- :loading='state.loading > 0'
- )
- q-separator(inset)
- .row.q-pa-md.q-col-gutter-md
- .col-12.col-lg-6
- //- -----------------------
- //- Security
- //- -----------------------
- q-card.shadow-1.q-pb-sm
- q-card-section
- .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-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-section(avatar)
- q-toggle(
- v-model='state.config.disallowFloc'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :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-section(avatar)
- q-toggle(
- v-model='state.config.disallowIframe'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :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-section(avatar)
- q-toggle(
- v-model='state.config.enforceSameOriginReferrerPolicy'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :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-section(avatar)
- q-toggle(
- v-model='state.config.disallowOpenRedirect'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :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-section(avatar)
- q-toggle(
- v-model='state.config.forceAssetDownload'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :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-section(avatar)
- q-toggle(
- v-model='state.config.trustProxy'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :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')}}
- 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-section(avatar)
- q-toggle(
- v-model='state.config.enforceHsts'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :aria-label='t(`admin.security.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-section(style='flex: 0 0 200px;')
- q-select(
- outlined
- v-model='state.config.hstsDuration'
- :options='hstsDurations'
- option-value='value'
- option-label='text'
- emit-value
- map-options
- dense
- :aria-label='t(`admin.security.hstsDuration`)'
- )
- .col-12.col-lg-6
- //- -----------------------
- //- Uploads
- //- -----------------------
- q-card.shadow-1.q-pb-sm
- q-card-section
- .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-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-section(style='flex: 0 0 200px;')
- q-input(
- outlined
- v-model.number='state.humanUploadMaxFileSize'
- dense
- :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-section(style='flex: 0 0 200px;')
- q-input(
- outlined
- v-model.number='state.config.uploadMaxFiles'
- dense
- :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-section(avatar)
- q-toggle(
- v-model='state.config.uploadScanSVG'
- color='primary'
- checked-icon='las la-check'
- unchecked-icon='las la-times'
- :aria-label='t(`admin.security.scanSVG`)'
- )
- //- -----------------------
- //- CORS
- //- -----------------------
- q-card.shadow-1.q-pb-sm.q-mt-md
- q-card-section
- .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-section
- q-select(
- outlined
- v-model='state.config.corsMode'
- :options='corsModes'
- option-value='value'
- option-label='text'
- emit-value
- map-options
- dense
- :aria-label='t(`admin.security.corsMode`)'
- )
- 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-section
- q-input(
- outlined
- v-model='state.config.corsConfig'
- dense
- type='textarea'
- :aria-label='t(`admin.security.corsHostnames`)'
- )
- 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-section
- q-input(
- outlined
- v-model='state.config.corsConfig'
- dense
- :aria-label='t(`admin.security.corsRegex`)'
- )
- //- -----------------------
- //- JWT
- //- -----------------------
- q-card.shadow-1.q-pb-sm.q-mt-md
- q-card-section
- .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-section(style='flex: 0 0 250px;')
- q-input(
- outlined
- v-model='state.config.authJwtAudience'
- dense
- :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-section(style='flex: 0 0 140px;')
- q-input(
- outlined
- v-model='state.config.authJwtExpiration'
- dense
- :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-section(style='flex: 0 0 140px;')
- q-input(
- outlined
- v-model='state.config.authJwtRenewablePeriod'
- dense
- :aria-label='t(`admin.security.tokenRenewalPeriod`)'
- )
- </template>
- <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 { 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
- },
- 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: {
- ...state.config,
- uploadMaxFileSize: filesizeParser(state.humanUploadMaxFileSize || '0')
- }
- })
- 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'>
- </style>
|