admin-general.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <template lang='pug'>
  2. v-container(fluid, grid-list-lg)
  3. v-layout(row wrap)
  4. v-flex(xs12)
  5. .admin-header
  6. img.animated.fadeInUp(src='/svg/icon-categorize.svg', alt='General', style='width: 80px;')
  7. .admin-header-title
  8. .headline.primary--text.animated.fadeInLeft {{ $t('admin:general.title') }}
  9. .subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin:general.subtitle') }}
  10. v-spacer
  11. v-btn.animated.fadeInDown(color='success', depressed, @click='save', large)
  12. v-icon(left) mdi-check
  13. span {{$t('common:actions.apply')}}
  14. v-form.pt-3
  15. v-layout(row wrap)
  16. v-flex(lg6 xs12)
  17. v-form
  18. v-card.animated.fadeInUp
  19. v-toolbar(color='primary', dark, dense, flat)
  20. v-toolbar-title.subtitle-1 {{ $t('admin:general.siteInfo') }}
  21. .overline.grey--text.pa-4 {{$t('admin:general.general')}}
  22. .px-3.pb-3
  23. v-text-field(
  24. outlined
  25. :label='$t(`admin:general.siteUrl`)'
  26. required
  27. :counter='255'
  28. v-model='config.host'
  29. prepend-icon='mdi-label-variant-outline'
  30. :hint='$t(`admin:general.siteUrlHint`)'
  31. persistent-hint
  32. )
  33. v-text-field.mt-3(
  34. outlined
  35. :label='$t(`admin:general.siteTitle`)'
  36. required
  37. :counter='50'
  38. v-model='config.title'
  39. prepend-icon='mdi-earth'
  40. :hint='$t(`admin:general.siteTitleHint`)'
  41. persistent-hint
  42. )
  43. v-divider
  44. .overline.grey--text.pa-4 {{$t('admin:general.logo')}}
  45. .pt-2.pb-7.pl-10.pr-3
  46. .d-flex.align-center
  47. v-avatar(size='100', tile)
  48. v-img(
  49. :src='config.logoUrl'
  50. lazy-src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNcWQ8AAdcBKrJda2oAAAAASUVORK5CYII='
  51. aspect-ratio='1'
  52. )
  53. .ml-4(style='flex: 1 1 auto;')
  54. v-text-field(
  55. outlined
  56. :label='$t(`admin:general.logoUrl`)'
  57. v-model='config.logoUrl'
  58. :hint='$t(`admin:general.logoUrlHint`)'
  59. persistent-hint
  60. append-icon='mdi-folder-image'
  61. @click:append='browseLogo'
  62. @keyup.enter='refreshLogo'
  63. )
  64. v-divider
  65. .overline.grey--text.pa-4 {{$t('admin:general.footerCopyright')}}
  66. .px-3.pb-3
  67. v-text-field(
  68. outlined
  69. :label='$t(`admin:general.companyName`)'
  70. v-model='config.company'
  71. :counter='255'
  72. prepend-icon='mdi-domain'
  73. persistent-hint
  74. :hint='$t(`admin:general.companyNameHint`)'
  75. )
  76. v-divider
  77. .overline.grey--text.pa-4 SEO
  78. .px-3.pb-3
  79. v-text-field(
  80. outlined
  81. :label='$t(`admin:general.siteDescription`)'
  82. :counter='255'
  83. v-model='config.description'
  84. prepend-icon='mdi-compass'
  85. :hint='$t(`admin:general.siteDescriptionHint`)'
  86. persistent-hint
  87. )
  88. v-select.mt-3(
  89. outlined
  90. :label='$t(`admin:general.metaRobots`)'
  91. multiple
  92. :items='metaRobots'
  93. v-model='config.robots'
  94. prepend-icon='mdi-compass'
  95. :return-object='false'
  96. :hint='$t(`admin:general.metaRobotsHint`)'
  97. persistent-hint
  98. )
  99. v-flex(lg6 xs12)
  100. v-card.animated.fadeInUp.wait-p4s
  101. v-toolbar(color='indigo', dark, dense, flat)
  102. v-toolbar-title.subtitle-1 Features
  103. v-spacer
  104. v-chip(label, color='white', small).indigo--text coming soon
  105. v-card-text
  106. v-switch(
  107. inset
  108. label='Asset Image Optimization'
  109. color='indigo'
  110. v-model='config.featureTinyPNG'
  111. persistent-hint
  112. hint='Image optimization tool to reduce filesize and bandwidth costs.'
  113. disabled
  114. )
  115. v-text-field.mt-3(
  116. outlined
  117. label='TinyPNG API Key'
  118. :counter='255'
  119. v-model='config.description'
  120. prepend-icon='mdi-subdirectory-arrow-right'
  121. hint='Get your API key at https://tinypng.com/developers'
  122. persistent-hint
  123. disabled
  124. )
  125. v-divider.mt-3
  126. v-switch(
  127. inset
  128. label='Page Ratings'
  129. color='indigo'
  130. v-model='config.featurePageRatings'
  131. persistent-hint
  132. hint='Allow users to rate pages.'
  133. disabled
  134. )
  135. v-divider.mt-3
  136. v-switch(
  137. inset
  138. label='Page Comments'
  139. color='indigo'
  140. v-model='config.featurePageComments'
  141. persistent-hint
  142. hint='Allow users to leave comments on pages.'
  143. disabled
  144. )
  145. v-divider.mt-3
  146. v-switch(
  147. inset
  148. label='Personal Wikis'
  149. color='indigo'
  150. v-model='config.featurePersonalWikis'
  151. persistent-hint
  152. hint='Allow users to have their own personal wiki.'
  153. disabled
  154. )
  155. v-card.mt-5.animated.fadeInUp.wait-p5s
  156. v-toolbar(color='red darken-2', dark, dense, flat)
  157. v-toolbar-title.subtitle-1 Security
  158. v-card-text
  159. v-alert(outlined, color='red darken-2', icon='mdi-information-outline').body-2 Make sure to understand the implications before turning on / off a security feature.
  160. v-switch.mt-3(
  161. inset
  162. label='Block IFrame Embedding'
  163. color='red darken-2'
  164. v-model='config.securityIframe'
  165. persistent-hint
  166. hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'
  167. )
  168. v-divider.mt-3
  169. v-switch(
  170. inset
  171. label='Same Origin Referrer Policy'
  172. color='red darken-2'
  173. v-model='config.securityReferrerPolicy'
  174. persistent-hint
  175. hint='Limits the referrer header to same origin.'
  176. )
  177. v-divider.mt-3
  178. v-switch(
  179. inset
  180. label='Trust X-Forwarded-* Proxy Headers'
  181. color='red darken-2'
  182. v-model='config.securityTrustProxy'
  183. persistent-hint
  184. hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.'
  185. )
  186. v-divider.mt-3
  187. v-switch(
  188. inset
  189. label='Subresource Integrity (SRI)'
  190. color='red darken-2'
  191. v-model='config.securitySRI'
  192. persistent-hint
  193. hint='This ensure that resources such as CSS and JS files are not altered during delivery.'
  194. disabled
  195. )
  196. v-divider.mt-3
  197. v-switch(
  198. inset
  199. label='Enforce HSTS'
  200. color='red darken-2'
  201. v-model='config.securityHSTS'
  202. persistent-hint
  203. hint='This ensures the connection cannot be established through an insecure HTTP connection.'
  204. )
  205. v-select.mt-5(
  206. outlined
  207. label='HSTS Max Age'
  208. :items='hstsDurations'
  209. v-model='config.securityHSTSDuration'
  210. prepend-icon='mdi-subdirectory-arrow-right'
  211. :disabled='!config.securityHSTS'
  212. hide-details
  213. style='max-width: 450px;'
  214. )
  215. .pl-11.mt-3
  216. .caption Defines the duration for which the server should only deliver content through HTTPS.
  217. .caption It's a good idea to start with small values and make sure that nothing breaks on your wiki before moving to longer values.
  218. v-divider.mt-3
  219. v-switch(
  220. inset
  221. label='Enforce CSP'
  222. color='red darken-2'
  223. v-model='config.securityCSP'
  224. persistent-hint
  225. hint='Restricts scripts to pre-approved content sources.'
  226. disabled
  227. )
  228. v-textarea.mt-5(
  229. label='CSP Directives'
  230. outlined
  231. v-model='config.securityCSPDirectives'
  232. prepend-icon='mdi-subdirectory-arrow-right'
  233. persistent-hint
  234. hint='One directive per line.'
  235. disabled
  236. )
  237. component(:is='activeModal')
  238. </template>
  239. <script>
  240. import _ from 'lodash'
  241. import { get, sync } from 'vuex-pathify'
  242. import siteConfigQuery from 'gql/admin/site/site-query-config.gql'
  243. import siteUpdateConfigMutation from 'gql/admin/site/site-mutation-save-config.gql'
  244. import editorStore from '../../store/editor'
  245. /* global WIKI */
  246. WIKI.$store.registerModule('editor', editorStore)
  247. export default {
  248. i18nOptions: { namespaces: 'editor' },
  249. components: {
  250. editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
  251. },
  252. data() {
  253. return {
  254. analyticsServices: [
  255. { text: 'None', value: '' },
  256. { text: 'Elasticsearch APM RUM', value: 'elk' },
  257. { text: 'Google Analytics', value: 'ga' },
  258. { text: 'Google Tag Manager', value: 'gtm' }
  259. ],
  260. config: {
  261. host: '',
  262. title: '',
  263. description: '',
  264. robots: [],
  265. analyticsService: '',
  266. analyticsId: '',
  267. company: '',
  268. logoUrl: '',
  269. featureAnalytics: false,
  270. featurePageRatings: false,
  271. featurePageComments: false,
  272. featurePersonalWikis: false,
  273. featureTinyPNG: false,
  274. securityIframe: true,
  275. securityReferrerPolicy: true,
  276. securityTrustProxy: true,
  277. securitySRI: true,
  278. securityHSTS: false,
  279. securityHSTSDuration: 0,
  280. securityCSP: false,
  281. securityCSPDirectives: ''
  282. },
  283. hstsDurations: [
  284. { value: 300, text: '5 minutes' },
  285. { value: 86400, text: '1 day' },
  286. { value: 604800, text: '1 week' },
  287. { value: 2592000, text: '1 month' },
  288. { value: 31536000, text: '1 year' },
  289. { value: 63072000, text: '2 years' }
  290. ],
  291. metaRobots: [
  292. { text: 'Index', value: 'index' },
  293. { text: 'Follow', value: 'follow' },
  294. { text: 'No Index', value: 'noindex' },
  295. { text: 'No Follow', value: 'nofollow' }
  296. ]
  297. }
  298. },
  299. computed: {
  300. darkMode: get('site/dark'),
  301. siteTitle: sync('site/title'),
  302. logoUrl: sync('site/logoUrl'),
  303. company: sync('site/company'),
  304. activeModal: sync('editor/activeModal')
  305. },
  306. methods: {
  307. async save () {
  308. try {
  309. await this.$apollo.mutate({
  310. mutation: siteUpdateConfigMutation,
  311. variables: {
  312. host: _.get(this.config, 'host', ''),
  313. title: _.get(this.config, 'title', ''),
  314. description: _.get(this.config, 'description', ''),
  315. robots: _.get(this.config, 'robots', []),
  316. analyticsService: _.get(this.config, 'analyticsService', ''),
  317. analyticsId: _.get(this.config, 'analyticsId', ''),
  318. company: _.get(this.config, 'company', ''),
  319. logoUrl: _.get(this.config, 'logoUrl', ''),
  320. featurePageRatings: _.get(this.config, 'featurePageRatings', false),
  321. featurePageComments: _.get(this.config, 'featurePageComments', false),
  322. featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false),
  323. securityIframe: _.get(this.config, 'securityIframe', false),
  324. securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
  325. securityTrustProxy: _.get(this.config, 'securityTrustProxy', false),
  326. securitySRI: _.get(this.config, 'securitySRI', false),
  327. securityHSTS: _.get(this.config, 'securityHSTS', false),
  328. securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),
  329. securityCSP: _.get(this.config, 'securityCSP', false),
  330. securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')
  331. },
  332. watchLoading (isLoading) {
  333. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
  334. }
  335. })
  336. this.$store.commit('showNotification', {
  337. style: 'success',
  338. message: 'Configuration saved successfully.',
  339. icon: 'check'
  340. })
  341. this.siteTitle = this.config.title
  342. this.company = this.config.company
  343. this.logoUrl = this.config.logoUrl
  344. } catch (err) {
  345. this.$store.commit('pushGraphError', err)
  346. }
  347. },
  348. browseLogo () {
  349. this.$store.set('editor/editorKey', 'common')
  350. this.activeModal = 'editorModalMedia'
  351. },
  352. refreshLogo () {
  353. this.$forceUpdate()
  354. }
  355. },
  356. mounted () {
  357. this.$root.$on('editorInsert', opts => {
  358. this.config.logoUrl = opts.path
  359. })
  360. },
  361. beforeDestroy() {
  362. this.$root.$off('editorInsert')
  363. },
  364. apollo: {
  365. config: {
  366. query: siteConfigQuery,
  367. fetchPolicy: 'network-only',
  368. update: (data) => _.cloneDeep(data.site.config),
  369. watchLoading (isLoading) {
  370. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-refresh')
  371. }
  372. }
  373. }
  374. }
  375. </script>
  376. <style lang='scss'>
  377. </style>