AdminSecurity.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <template lang='pug'>
  2. q-page.admin-mail
  3. .row.q-pa-md.items-center
  4. .col-auto
  5. img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-protect.svg')
  6. .col.q-pl-md
  7. .text-h5.text-primary.animated.fadeInLeft {{ t('admin.security.title') }}
  8. .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.security.subtitle') }}
  9. .col-auto
  10. q-btn.q-mr-sm.acrylic-btn(
  11. icon='las la-question-circle'
  12. flat
  13. color='grey'
  14. href='https://docs.js.wiki/admin/security'
  15. target='_blank'
  16. type='a'
  17. )
  18. q-btn.q-mr-sm.acrylic-btn(
  19. icon='las la-redo-alt'
  20. flat
  21. color='secondary'
  22. :loading='state.loading > 0'
  23. @click='load'
  24. )
  25. q-btn(
  26. unelevated
  27. icon='fa-solid fa-check'
  28. :label='t(`common.actions.apply`)'
  29. color='secondary'
  30. @click='save'
  31. :loading='state.loading > 0'
  32. )
  33. q-separator(inset)
  34. .row.q-pa-md.q-col-gutter-md
  35. .col-12.col-lg-6
  36. //- -----------------------
  37. //- Security
  38. //- -----------------------
  39. q-card.shadow-1.q-pb-sm
  40. q-card-section
  41. .text-subtitle1 {{t('admin.security.title')}}
  42. q-item.q-pt-none
  43. q-item-section
  44. q-card.bg-negative.text-white.rounded-borders(flat)
  45. q-card-section.items-center(horizontal)
  46. q-card-section.col-auto.q-pr-none
  47. q-icon(name='las la-exclamation-triangle', size='sm')
  48. q-card-section.text-caption {{ t('admin.security.warn') }}
  49. q-item(tag='label', v-ripple)
  50. blueprint-icon(icon='rfid-signal')
  51. q-item-section
  52. q-item-label {{t(`admin.security.disallowFloc`)}}
  53. q-item-label(caption) {{t(`admin.security.disallowFlocHint`)}}
  54. q-item-section(avatar)
  55. q-toggle(
  56. v-model='state.config.disallowFloc'
  57. color='primary'
  58. checked-icon='las la-check'
  59. unchecked-icon='las la-times'
  60. :aria-label='t(`admin.security.disallowFloc`)'
  61. )
  62. q-separator.q-my-sm(inset)
  63. q-item(tag='label', v-ripple)
  64. blueprint-icon(icon='maximize-window')
  65. q-item-section
  66. q-item-label {{t(`admin.security.disallowIframe`)}}
  67. q-item-label(caption) {{t(`admin.security.disallowIframeHint`)}}
  68. q-item-section(avatar)
  69. q-toggle(
  70. v-model='state.config.disallowIframe'
  71. color='primary'
  72. checked-icon='las la-check'
  73. unchecked-icon='las la-times'
  74. :aria-label='t(`admin.security.disallowIframe`)'
  75. )
  76. q-separator.q-my-sm(inset)
  77. q-item(tag='label', v-ripple)
  78. blueprint-icon(icon='do-not-touch')
  79. q-item-section
  80. q-item-label {{t(`admin.security.enforceSameOriginReferrerPolicy`)}}
  81. q-item-label(caption) {{t(`admin.security.enforceSameOriginReferrerPolicyHint`)}}
  82. q-item-section(avatar)
  83. q-toggle(
  84. v-model='state.config.enforceSameOriginReferrerPolicy'
  85. color='primary'
  86. checked-icon='las la-check'
  87. unchecked-icon='las la-times'
  88. :aria-label='t(`admin.security.enforceSameOriginReferrerPolicy`)'
  89. )
  90. q-separator.q-my-sm(inset)
  91. q-item(tag='label', v-ripple)
  92. blueprint-icon(icon='curly-arrow')
  93. q-item-section
  94. q-item-label {{t(`admin.security.disallowOpenRedirect`)}}
  95. q-item-label(caption) {{t(`admin.security.disallowOpenRedirectHint`)}}
  96. q-item-section(avatar)
  97. q-toggle(
  98. v-model='state.config.disallowOpenRedirect'
  99. color='primary'
  100. checked-icon='las la-check'
  101. unchecked-icon='las la-times'
  102. :aria-label='t(`admin.security.disallowOpenRedirect`)'
  103. )
  104. q-separator.q-my-sm(inset)
  105. q-item(tag='label', v-ripple)
  106. blueprint-icon(icon='download-from-cloud')
  107. q-item-section
  108. q-item-label {{t(`admin.security.forceAssetDownload`)}}
  109. q-item-label(caption) {{t(`admin.security.forceAssetDownloadHint`)}}
  110. q-item-section(avatar)
  111. q-toggle(
  112. v-model='state.config.forceAssetDownload'
  113. color='primary'
  114. checked-icon='las la-check'
  115. unchecked-icon='las la-times'
  116. :aria-label='t(`admin.security.forceAssetDownload`)'
  117. )
  118. q-separator.q-my-sm(inset)
  119. q-item(tag='label', v-ripple)
  120. blueprint-icon(icon='door-sensor-alarmed')
  121. q-item-section
  122. q-item-label {{t(`admin.security.trustProxy`)}}
  123. q-item-label(caption) {{t(`admin.security.trustProxyHint`)}}
  124. q-item-section(avatar)
  125. q-toggle(
  126. v-model='state.config.trustProxy'
  127. color='primary'
  128. checked-icon='las la-check'
  129. unchecked-icon='las la-times'
  130. :aria-label='t(`admin.security.trustProxy`)'
  131. )
  132. //- -----------------------
  133. //- HSTS
  134. //- -----------------------
  135. q-card.shadow-1.q-pb-sm.q-mt-md
  136. q-card-section
  137. .text-subtitle1 {{t('admin.security.hsts')}}
  138. q-item(tag='label', v-ripple)
  139. blueprint-icon(icon='hips')
  140. q-item-section
  141. q-item-label {{t(`admin.security.enforceHsts`)}}
  142. q-item-label(caption) {{t(`admin.security.enforceHstsHint`)}}
  143. q-item-section(avatar)
  144. q-toggle(
  145. v-model='state.config.enforceHsts'
  146. color='primary'
  147. checked-icon='las la-check'
  148. unchecked-icon='las la-times'
  149. :aria-label='t(`admin.security.enforceHsts`)'
  150. )
  151. template(v-if='state.config.enforceHsts')
  152. q-separator.q-my-sm(inset)
  153. q-item
  154. blueprint-icon(icon='timer')
  155. q-item-section
  156. q-item-label {{t(`admin.security.hstsDuration`)}}
  157. q-item-label(caption) {{t(`admin.security.hstsDurationHint`)}}
  158. q-item-section(style='flex: 0 0 200px;')
  159. q-select(
  160. outlined
  161. v-model='state.config.hstsDuration'
  162. :options='hstsDurations'
  163. option-value='value'
  164. option-label='text'
  165. emit-value
  166. map-options
  167. dense
  168. :aria-label='t(`admin.security.hstsDuration`)'
  169. )
  170. .col-12.col-lg-6
  171. //- -----------------------
  172. //- Uploads
  173. //- -----------------------
  174. q-card.shadow-1.q-pb-sm
  175. q-card-section
  176. .text-subtitle1 {{t('admin.security.uploads')}}
  177. q-item.q-pt-none
  178. q-item-section
  179. q-card.bg-info.text-white.rounded-borders(flat)
  180. q-card-section.items-center(horizontal)
  181. q-card-section.col-auto.q-pr-none
  182. q-icon(name='las la-info-circle', size='sm')
  183. q-card-section.text-caption {{ t('admin.security.uploadsInfo') }}
  184. q-item
  185. blueprint-icon(icon='upload-to-the-cloud')
  186. q-item-section
  187. q-item-label {{t(`admin.security.maxUploadSize`)}}
  188. q-item-label(caption) {{t(`admin.security.maxUploadSizeHint`)}}
  189. q-item-section(style='flex: 0 0 200px;')
  190. q-input(
  191. outlined
  192. v-model.number='state.humanUploadMaxFileSize'
  193. dense
  194. :aria-label='t(`admin.security.maxUploadSize`)'
  195. )
  196. q-separator.q-my-sm(inset)
  197. q-item
  198. blueprint-icon(icon='upload-to-ftp')
  199. q-item-section
  200. q-item-label {{t(`admin.security.maxUploadBatch`)}}
  201. q-item-label(caption) {{t(`admin.security.maxUploadBatchHint`)}}
  202. q-item-section(style='flex: 0 0 200px;')
  203. q-input(
  204. outlined
  205. v-model.number='state.config.uploadMaxFiles'
  206. dense
  207. :suffix='t(`admin.security.maxUploadBatchSuffix`)'
  208. :aria-label='t(`admin.security.maxUploadBatch`)'
  209. )
  210. q-separator.q-my-sm(inset)
  211. q-item(tag='label', v-ripple)
  212. blueprint-icon(icon='scan-stock')
  213. q-item-section
  214. q-item-label {{t(`admin.security.scanSVG`)}}
  215. q-item-label(caption) {{t(`admin.security.scanSVGHint`)}}
  216. q-item-section(avatar)
  217. q-toggle(
  218. v-model='state.config.uploadScanSVG'
  219. color='primary'
  220. checked-icon='las la-check'
  221. unchecked-icon='las la-times'
  222. :aria-label='t(`admin.security.scanSVG`)'
  223. )
  224. //- -----------------------
  225. //- CORS
  226. //- -----------------------
  227. q-card.shadow-1.q-pb-sm.q-mt-md
  228. q-card-section
  229. .text-subtitle1 {{t('admin.security.cors')}}
  230. q-item
  231. blueprint-icon(icon='firewall')
  232. q-item-section
  233. q-item-label {{t(`admin.security.corsMode`)}}
  234. q-item-label(caption) {{t(`admin.security.corsModeHint`)}}
  235. q-item-section
  236. q-select(
  237. outlined
  238. v-model='state.config.corsMode'
  239. :options='corsModes'
  240. option-value='value'
  241. option-label='text'
  242. emit-value
  243. map-options
  244. dense
  245. :aria-label='t(`admin.security.corsMode`)'
  246. )
  247. template(v-if='state.config.corsMode === `HOSTNAMES`')
  248. q-separator.q-my-sm(inset)
  249. q-item
  250. blueprint-icon(icon='todo-list', key='corsHostnames')
  251. q-item-section
  252. q-item-label {{t(`admin.security.corsHostnames`)}}
  253. q-item-label(caption) {{t(`admin.security.corsHostnamesHint`)}}
  254. q-item-section
  255. q-input(
  256. outlined
  257. v-model='state.config.corsConfig'
  258. dense
  259. type='textarea'
  260. :aria-label='t(`admin.security.corsHostnames`)'
  261. )
  262. template(v-else-if='state.config.corsMode === `REGEX`')
  263. q-separator.q-my-sm(inset)
  264. q-item
  265. blueprint-icon(icon='validation', key='corsRegex')
  266. q-item-section
  267. q-item-label {{t(`admin.security.corsRegex`)}}
  268. q-item-label(caption) {{t(`admin.security.corsRegexHint`)}}
  269. q-item-section
  270. q-input(
  271. outlined
  272. v-model='state.config.corsConfig'
  273. dense
  274. :aria-label='t(`admin.security.corsRegex`)'
  275. )
  276. //- -----------------------
  277. //- JWT
  278. //- -----------------------
  279. q-card.shadow-1.q-pb-sm.q-mt-md
  280. q-card-section
  281. .text-subtitle1 {{t('admin.security.jwt')}}
  282. q-item
  283. blueprint-icon(icon='ticket')
  284. q-item-section
  285. q-item-label {{t(`admin.security.jwtAudience`)}}
  286. q-item-label(caption) {{t(`admin.security.jwtAudienceHint`)}}
  287. q-item-section(style='flex: 0 0 250px;')
  288. q-input(
  289. outlined
  290. v-model='state.config.authJwtAudience'
  291. dense
  292. :aria-label='t(`admin.security.jwtAudience`)'
  293. )
  294. q-separator.q-my-sm(inset)
  295. q-item
  296. blueprint-icon(icon='expired')
  297. q-item-section
  298. q-item-label {{t(`admin.security.tokenExpiration`)}}
  299. q-item-label(caption) {{t(`admin.security.tokenExpirationHint`)}}
  300. q-item-section(style='flex: 0 0 140px;')
  301. q-input(
  302. outlined
  303. v-model='state.config.authJwtExpiration'
  304. dense
  305. :aria-label='t(`admin.security.tokenExpiration`)'
  306. )
  307. q-separator.q-my-sm(inset)
  308. q-item
  309. blueprint-icon(icon='future')
  310. q-item-section
  311. q-item-label {{t(`admin.security.tokenRenewalPeriod`)}}
  312. q-item-label(caption) {{t(`admin.security.tokenRenewalPeriodHint`)}}
  313. q-item-section(style='flex: 0 0 140px;')
  314. q-input(
  315. outlined
  316. v-model='state.config.authJwtRenewablePeriod'
  317. dense
  318. :aria-label='t(`admin.security.tokenRenewalPeriod`)'
  319. )
  320. </template>
  321. <script setup>
  322. import cloneDeep from 'lodash/cloneDeep'
  323. import gql from 'graphql-tag'
  324. import _get from 'lodash/get'
  325. import filesize from 'filesize'
  326. import filesizeParser from 'filesize-parser'
  327. import { useI18n } from 'vue-i18n'
  328. import { useMeta, useQuasar } from 'quasar'
  329. import { computed, onMounted, reactive, watch } from 'vue'
  330. import { useAdminStore } from 'src/stores/admin'
  331. import { useSiteStore } from 'src/stores/site'
  332. import { useDataStore } from 'src/stores/data'
  333. // QUASAR
  334. const $q = useQuasar()
  335. // STORES
  336. const adminStore = useAdminStore()
  337. const siteStore = useSiteStore()
  338. const dataStore = useDataStore()
  339. // I18N
  340. const { t } = useI18n()
  341. // META
  342. useMeta({
  343. title: t('admin.security.title')
  344. })
  345. // DATA
  346. const state = reactive({
  347. loading: false,
  348. config: {
  349. corsConfig: '',
  350. corsMode: 'OFF',
  351. cspDirectives: '',
  352. disallowFloc: false,
  353. disallowIframe: false,
  354. disallowOpenRedirect: false,
  355. enforceCsp: false,
  356. enforceHsts: false,
  357. enforceSameOriginReferrerPolicy: false,
  358. forceAssetDownload: false,
  359. hstsDuration: 0,
  360. trustProxy: false,
  361. authJwtAudience: 'urn:wiki.js',
  362. authJwtExpiration: '30m',
  363. authJwtRenewablePeriod: '14d',
  364. uploadMaxFileSize: 0,
  365. uploadMaxFiles: 0,
  366. uploadScanSVG: false
  367. },
  368. humanUploadMaxFileSize: '0'
  369. })
  370. const hstsDurations = [
  371. { value: 300, text: '5 minutes' },
  372. { value: 86400, text: '1 day' },
  373. { value: 604800, text: '1 week' },
  374. { value: 2592000, text: '1 month' },
  375. { value: 31536000, text: '1 year' },
  376. { value: 63072000, text: '2 years' }
  377. ]
  378. const corsModes = [
  379. { value: 'OFF', text: 'Off / Same-Origin' },
  380. { value: 'REFLECT', text: 'Reflect Request Origin' },
  381. { value: 'HOSTNAMES', text: 'Hostnames Whitelist' },
  382. { value: 'REGEX', text: 'Regex Pattern Match' }
  383. ]
  384. // METHODS
  385. async function load () {
  386. state.loading++
  387. $q.loading.show()
  388. const resp = await APOLLO_CLIENT.query({
  389. query: gql`
  390. query getSecurityConfig {
  391. systemSecurity {
  392. authJwtAudience
  393. authJwtExpiration
  394. authJwtRenewablePeriod
  395. corsConfig
  396. corsMode
  397. cspDirectives
  398. disallowFloc
  399. disallowIframe
  400. disallowOpenRedirect
  401. enforceCsp
  402. enforceHsts
  403. enforceSameOriginReferrerPolicy
  404. forceAssetDownload
  405. hstsDuration
  406. trustProxy
  407. uploadMaxFileSize
  408. uploadMaxFiles
  409. uploadScanSVG
  410. }
  411. }
  412. `,
  413. fetchPolicy: 'network-only'
  414. })
  415. state.config = cloneDeep(resp?.data?.systemSecurity)
  416. state.humanUploadMaxFileSize = filesize(state.config.uploadMaxFileSize ?? 0, { base: 2, standard: 'jedec' })
  417. $q.loading.hide()
  418. state.loading--
  419. }
  420. async function save () {
  421. state.loading++
  422. try {
  423. const respRaw = await APOLLO_CLIENT.mutate({
  424. mutation: gql`
  425. mutation saveSecurityConfig (
  426. $authJwtAudience: String
  427. $authJwtExpiration: String
  428. $authJwtRenewablePeriod: String
  429. $corsConfig: String
  430. $corsMode: SystemSecurityCorsMode
  431. $cspDirectives: String
  432. $disallowFloc: Boolean
  433. $disallowIframe: Boolean
  434. $disallowOpenRedirect: Boolean
  435. $enforceCsp: Boolean
  436. $enforceHsts: Boolean
  437. $enforceSameOriginReferrerPolicy: Boolean
  438. $hstsDuration: Int
  439. $trustProxy: Boolean
  440. $uploadMaxFiles: Int
  441. $uploadMaxFileSize: Int
  442. ) {
  443. updateSystemSecurity(
  444. authJwtAudience: $authJwtAudience
  445. authJwtExpiration: $authJwtExpiration
  446. authJwtRenewablePeriod: $authJwtRenewablePeriod
  447. corsConfig: $corsConfig
  448. corsMode: $corsMode
  449. cspDirectives: $cspDirectives
  450. disallowFloc: $disallowFloc
  451. disallowIframe: $disallowIframe
  452. disallowOpenRedirect: $disallowOpenRedirect
  453. enforceCsp: $enforceCsp
  454. enforceHsts: $enforceHsts
  455. enforceSameOriginReferrerPolicy: $enforceSameOriginReferrerPolicy
  456. hstsDuration: $hstsDuration
  457. trustProxy: $trustProxy
  458. uploadMaxFiles: $uploadMaxFiles
  459. uploadMaxFileSize: $uploadMaxFileSize
  460. ) {
  461. status {
  462. succeeded
  463. slug
  464. message
  465. }
  466. }
  467. }
  468. `,
  469. variables: {
  470. ...state.config,
  471. uploadMaxFileSize: filesizeParser(state.humanUploadMaxFileSize || '0')
  472. }
  473. })
  474. const resp = _get(respRaw, 'data.updateSystemSecurity.status', {})
  475. if (resp.succeeded) {
  476. $q.notify({
  477. type: 'positive',
  478. message: t('admin.security.saveSuccess')
  479. })
  480. } else {
  481. throw new Error(resp.message)
  482. }
  483. } catch (err) {
  484. $q.notify({
  485. type: 'negative',
  486. message: 'Failed to save security config',
  487. caption: err.message
  488. })
  489. }
  490. state.loading--
  491. }
  492. // MOUNTED
  493. onMounted(() => {
  494. load()
  495. })
  496. </script>
  497. <style lang='scss'>
  498. </style>