Ver código fonte

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

Nicolas Giard 3 anos atrás
pai
commit
d5fa587c98

+ 2 - 1
.vscode/settings.json

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

+ 114 - 91
ux/src/pages/AdminExtensions.vue

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

+ 265 - 244
ux/src/pages/AdminSecurity.vue

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

+ 2 - 2
ux/src/router/routes.js

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