You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

503 lines
19 KiB

  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-select.mt-3(
  77. outlined
  78. :label='$t(`admin:general.contentLicense`)'
  79. :items='contentLicenses'
  80. v-model='config.contentLicense'
  81. prepend-icon='mdi-creative-commons'
  82. :return-object='false'
  83. :hint='$t(`admin:general.contentLicenseHint`)'
  84. persistent-hint
  85. )
  86. v-divider
  87. .overline.grey--text.pa-4 SEO
  88. .px-3.pb-3
  89. v-text-field(
  90. outlined
  91. :label='$t(`admin:general.siteDescription`)'
  92. :counter='255'
  93. v-model='config.description'
  94. prepend-icon='mdi-compass'
  95. :hint='$t(`admin:general.siteDescriptionHint`)'
  96. persistent-hint
  97. )
  98. v-select.mt-3(
  99. outlined
  100. :label='$t(`admin:general.metaRobots`)'
  101. multiple
  102. :items='metaRobots'
  103. v-model='config.robots'
  104. prepend-icon='mdi-compass'
  105. :return-object='false'
  106. :hint='$t(`admin:general.metaRobotsHint`)'
  107. persistent-hint
  108. )
  109. v-flex(lg6 xs12)
  110. v-card.animated.fadeInUp.wait-p4s
  111. v-toolbar(color='indigo', dark, dense, flat)
  112. v-toolbar-title.subtitle-1 Features
  113. v-spacer
  114. v-chip(label, color='white', small).indigo--text coming soon
  115. v-card-text
  116. v-switch(
  117. inset
  118. label='Asset Image Optimization'
  119. color='indigo'
  120. v-model='config.featureTinyPNG'
  121. persistent-hint
  122. hint='Image optimization tool to reduce filesize and bandwidth costs.'
  123. disabled
  124. )
  125. v-text-field.mt-3(
  126. outlined
  127. label='TinyPNG API Key'
  128. :counter='255'
  129. v-model='config.description'
  130. prepend-icon='mdi-subdirectory-arrow-right'
  131. hint='Get your API key at https://tinypng.com/developers'
  132. persistent-hint
  133. disabled
  134. )
  135. v-divider.mt-3
  136. v-switch(
  137. inset
  138. label='Page Ratings'
  139. color='indigo'
  140. v-model='config.featurePageRatings'
  141. persistent-hint
  142. hint='Allow users to rate pages.'
  143. disabled
  144. )
  145. v-divider.mt-3
  146. v-switch(
  147. inset
  148. label='Page Comments'
  149. color='indigo'
  150. v-model='config.featurePageComments'
  151. persistent-hint
  152. hint='Allow users to leave comments on pages.'
  153. disabled
  154. )
  155. v-divider.mt-3
  156. v-switch(
  157. inset
  158. label='Personal Wikis'
  159. color='indigo'
  160. v-model='config.featurePersonalWikis'
  161. persistent-hint
  162. hint='Allow users to have their own personal wiki.'
  163. disabled
  164. )
  165. v-card.mt-5.animated.fadeInUp.wait-p5s
  166. v-toolbar(color='red darken-2', dark, dense, flat)
  167. v-toolbar-title.subtitle-1 Security
  168. v-card-text
  169. 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.
  170. v-switch.mt-3(
  171. inset
  172. label='Block IFrame Embedding'
  173. color='red darken-2'
  174. v-model='config.securityIframe'
  175. persistent-hint
  176. hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'
  177. )
  178. v-divider.mt-3
  179. v-switch(
  180. inset
  181. label='Same Origin Referrer Policy'
  182. color='red darken-2'
  183. v-model='config.securityReferrerPolicy'
  184. persistent-hint
  185. hint='Limits the referrer header to same origin.'
  186. )
  187. v-divider.mt-3
  188. v-switch(
  189. inset
  190. label='Trust X-Forwarded-* Proxy Headers'
  191. color='red darken-2'
  192. v-model='config.securityTrustProxy'
  193. persistent-hint
  194. hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.'
  195. )
  196. v-divider.mt-3
  197. v-switch(
  198. inset
  199. label='Subresource Integrity (SRI)'
  200. color='red darken-2'
  201. v-model='config.securitySRI'
  202. persistent-hint
  203. hint='This ensure that resources such as CSS and JS files are not altered during delivery.'
  204. disabled
  205. )
  206. v-divider.mt-3
  207. v-switch(
  208. inset
  209. label='Enforce HSTS'
  210. color='red darken-2'
  211. v-model='config.securityHSTS'
  212. persistent-hint
  213. hint='This ensures the connection cannot be established through an insecure HTTP connection.'
  214. )
  215. v-select.mt-5(
  216. outlined
  217. label='HSTS Max Age'
  218. :items='hstsDurations'
  219. v-model='config.securityHSTSDuration'
  220. prepend-icon='mdi-subdirectory-arrow-right'
  221. :disabled='!config.securityHSTS'
  222. hide-details
  223. style='max-width: 450px;'
  224. )
  225. .pl-11.mt-3
  226. .caption Defines the duration for which the server should only deliver content through HTTPS.
  227. .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.
  228. v-divider.mt-3
  229. v-switch(
  230. inset
  231. label='Enforce CSP'
  232. color='red darken-2'
  233. v-model='config.securityCSP'
  234. persistent-hint
  235. hint='Restricts scripts to pre-approved content sources.'
  236. disabled
  237. )
  238. v-textarea.mt-5(
  239. label='CSP Directives'
  240. outlined
  241. v-model='config.securityCSPDirectives'
  242. prepend-icon='mdi-subdirectory-arrow-right'
  243. persistent-hint
  244. hint='One directive per line.'
  245. disabled
  246. )
  247. component(:is='activeModal')
  248. </template>
  249. <script>
  250. import _ from 'lodash'
  251. import { get, sync } from 'vuex-pathify'
  252. import gql from 'graphql-tag'
  253. import editorStore from '../../store/editor'
  254. /* global WIKI */
  255. WIKI.$store.registerModule('editor', editorStore)
  256. export default {
  257. i18nOptions: { namespaces: 'editor' },
  258. components: {
  259. editorModalMedia: () => import(/* webpackChunkName: "editor", webpackMode: "lazy" */ '../editor/editor-modal-media.vue')
  260. },
  261. data() {
  262. return {
  263. analyticsServices: [
  264. { text: 'None', value: '' },
  265. { text: 'Elasticsearch APM RUM', value: 'elk' },
  266. { text: 'Google Analytics', value: 'ga' },
  267. { text: 'Google Tag Manager', value: 'gtm' }
  268. ],
  269. config: {
  270. host: '',
  271. title: '',
  272. description: '',
  273. robots: [],
  274. analyticsService: '',
  275. analyticsId: '',
  276. company: '',
  277. contentLicense: '',
  278. logoUrl: '',
  279. featureAnalytics: false,
  280. featurePageRatings: false,
  281. featurePageComments: false,
  282. featurePersonalWikis: false,
  283. featureTinyPNG: false,
  284. securityIframe: true,
  285. securityReferrerPolicy: true,
  286. securityTrustProxy: true,
  287. securitySRI: true,
  288. securityHSTS: false,
  289. securityHSTSDuration: 0,
  290. securityCSP: false,
  291. securityCSPDirectives: ''
  292. },
  293. hstsDurations: [
  294. { value: 300, text: '5 minutes' },
  295. { value: 86400, text: '1 day' },
  296. { value: 604800, text: '1 week' },
  297. { value: 2592000, text: '1 month' },
  298. { value: 31536000, text: '1 year' },
  299. { value: 63072000, text: '2 years' }
  300. ],
  301. metaRobots: [
  302. { text: 'Index', value: 'index' },
  303. { text: 'Follow', value: 'follow' },
  304. { text: 'No Index', value: 'noindex' },
  305. { text: 'No Follow', value: 'nofollow' }
  306. ]
  307. }
  308. },
  309. computed: {
  310. darkMode: get('site/dark'),
  311. siteTitle: sync('site/title'),
  312. logoUrl: sync('site/logoUrl'),
  313. company: sync('site/company'),
  314. contentLicense: sync('site/contentLicense'),
  315. activeModal: sync('editor/activeModal'),
  316. contentLicenses () {
  317. return [
  318. { value: '', text: this.$t('common:license.none') },
  319. { value: 'alr', text: this.$t('common:license.alr') },
  320. { value: 'cc0', text: this.$t('common:license.cc0') },
  321. { value: 'ccby', text: this.$t('common:license.ccby') },
  322. { value: 'ccbysa', text: this.$t('common:license.ccbysa') },
  323. { value: 'ccbynd', text: this.$t('common:license.ccbynd') },
  324. { value: 'ccbync', text: this.$t('common:license.ccbync') },
  325. { value: 'ccbyncsa', text: this.$t('common:license.ccbyncsa') },
  326. { value: 'ccbyncnd', text: this.$t('common:license.ccbyncnd') }
  327. ]
  328. }
  329. },
  330. methods: {
  331. async save () {
  332. try {
  333. await this.$apollo.mutate({
  334. mutation: gql`
  335. mutation (
  336. $host: String!
  337. $title: String!
  338. $description: String!
  339. $robots: [String]!
  340. $analyticsService: String!
  341. $analyticsId: String!
  342. $company: String!
  343. $contentLicense: String!
  344. $logoUrl: String!
  345. $featurePageRatings: Boolean!
  346. $featurePageComments: Boolean!
  347. $featurePersonalWikis: Boolean!
  348. $securityIframe: Boolean!
  349. $securityReferrerPolicy: Boolean!
  350. $securityTrustProxy: Boolean!
  351. $securitySRI: Boolean!
  352. $securityHSTS: Boolean!
  353. $securityHSTSDuration: Int!
  354. $securityCSP: Boolean!
  355. $securityCSPDirectives: String!
  356. ) {
  357. site {
  358. updateConfig(
  359. host: $host,
  360. title: $title,
  361. description: $description,
  362. robots: $robots,
  363. analyticsService: $analyticsService,
  364. analyticsId: $analyticsId,
  365. company: $company,
  366. contentLicense: $contentLicense,
  367. logoUrl: $logoUrl,
  368. featurePageRatings: $featurePageRatings,
  369. featurePageComments: $featurePageComments,
  370. featurePersonalWikis: $featurePersonalWikis,
  371. securityIframe: $securityIframe,
  372. securityReferrerPolicy: $securityReferrerPolicy,
  373. securityTrustProxy: $securityTrustProxy,
  374. securitySRI: $securitySRI,
  375. securityHSTS: $securityHSTS,
  376. securityHSTSDuration: $securityHSTSDuration,
  377. securityCSP: $securityCSP,
  378. securityCSPDirectives: $securityCSPDirectives
  379. ) {
  380. responseResult {
  381. succeeded
  382. errorCode
  383. slug
  384. message
  385. }
  386. }
  387. }
  388. }
  389. `,
  390. variables: {
  391. host: _.get(this.config, 'host', ''),
  392. title: _.get(this.config, 'title', ''),
  393. description: _.get(this.config, 'description', ''),
  394. robots: _.get(this.config, 'robots', []),
  395. analyticsService: _.get(this.config, 'analyticsService', ''),
  396. analyticsId: _.get(this.config, 'analyticsId', ''),
  397. company: _.get(this.config, 'company', ''),
  398. contentLicense: _.get(this.config, 'contentLicense', ''),
  399. logoUrl: _.get(this.config, 'logoUrl', ''),
  400. featurePageRatings: _.get(this.config, 'featurePageRatings', false),
  401. featurePageComments: _.get(this.config, 'featurePageComments', false),
  402. featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false),
  403. securityIframe: _.get(this.config, 'securityIframe', false),
  404. securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
  405. securityTrustProxy: _.get(this.config, 'securityTrustProxy', false),
  406. securitySRI: _.get(this.config, 'securitySRI', false),
  407. securityHSTS: _.get(this.config, 'securityHSTS', false),
  408. securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),
  409. securityCSP: _.get(this.config, 'securityCSP', false),
  410. securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')
  411. },
  412. watchLoading (isLoading) {
  413. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
  414. }
  415. })
  416. this.$store.commit('showNotification', {
  417. style: 'success',
  418. message: 'Configuration saved successfully.',
  419. icon: 'check'
  420. })
  421. this.siteTitle = this.config.title
  422. this.company = this.config.company
  423. this.contentLicense = this.config.contentLicense
  424. this.logoUrl = this.config.logoUrl
  425. } catch (err) {
  426. this.$store.commit('pushGraphError', err)
  427. }
  428. },
  429. browseLogo () {
  430. this.$store.set('editor/editorKey', 'common')
  431. this.activeModal = 'editorModalMedia'
  432. },
  433. refreshLogo () {
  434. this.$forceUpdate()
  435. }
  436. },
  437. mounted () {
  438. this.$root.$on('editorInsert', opts => {
  439. this.config.logoUrl = opts.path
  440. })
  441. },
  442. beforeDestroy() {
  443. this.$root.$off('editorInsert')
  444. },
  445. apollo: {
  446. config: {
  447. query: gql`
  448. {
  449. site {
  450. config {
  451. host
  452. title
  453. description
  454. robots
  455. analyticsService
  456. analyticsId
  457. company
  458. contentLicense
  459. logoUrl
  460. featurePageRatings
  461. featurePageComments
  462. featurePersonalWikis
  463. securityIframe
  464. securityReferrerPolicy
  465. securityTrustProxy
  466. securitySRI
  467. securityHSTS
  468. securityHSTSDuration
  469. securityCSP
  470. securityCSPDirectives
  471. }
  472. }
  473. }
  474. `,
  475. fetchPolicy: 'network-only',
  476. update: (data) => _.cloneDeep(data.site.config),
  477. watchLoading (isLoading) {
  478. this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-refresh')
  479. }
  480. }
  481. }
  482. }
  483. </script>
  484. <style lang='scss'>
  485. </style>