diff --git a/client/components/admin/admin-general.vue b/client/components/admin/admin-general.vue index 50eff359..52b90005 100644 --- a/client/components/admin/admin-general.vue +++ b/client/components/admin/admin-general.vue @@ -163,6 +163,7 @@ persistent-hint hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.' ) + v-divider.mt-3 v-switch( inset @@ -173,6 +174,16 @@ hint='Limits the referrer header to same origin.' ) + v-divider.mt-3 + v-switch( + inset + label='Trust X-Forwarded-* Proxy Headers' + color='red darken-2' + v-model='config.securityTrustProxy' + persistent-hint + hint='Should be enabled when using a reverse-proxy like nginx, apache, CloudFlare, etc in front of Wiki.js. Turn off otherwise.' + ) + v-divider.mt-3 v-switch( inset @@ -250,6 +261,7 @@ export default { featureTinyPNG: false, securityIframe: true, securityReferrerPolicy: true, + securityTrustProxy: true, securityHSTS: false, securityHSTSDuration: 0, securityCSP: false, @@ -296,6 +308,7 @@ export default { featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false), securityIframe: _.get(this.config, 'securityIframe', false), securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false), + securityTrustProxy: _.get(this.config, 'securityTrustProxy', false), securityHSTS: _.get(this.config, 'securityHSTS', false), securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0), securityCSP: _.get(this.config, 'securityCSP', false), diff --git a/client/components/admin/admin-groups-edit.vue b/client/components/admin/admin-groups-edit.vue index 2e647bac..9ee23b45 100644 --- a/client/components/admin/admin-groups-edit.vue +++ b/client/components/admin/admin-groups-edit.vue @@ -18,7 +18,7 @@ v-icon(color='red') mdi-trash-can-outline v-card .dialog-header.is-red Delete Group? - v-card-text Are you sure you want to delete group #[strong {{ group.name }}]? All users will be unassigned from this group. + v-card-text.pa-4 Are you sure you want to delete group #[strong {{ group.name }}]? All users will be unassigned from this group. v-card-actions v-spacer v-btn(text, @click='deleteGroupDialog = false') Cancel diff --git a/client/components/admin/admin-utilities-importv1.vue b/client/components/admin/admin-utilities-importv1.vue index 991e1c8e..fdd2ea64 100644 --- a/client/components/admin/admin-utilities-importv1.vue +++ b/client/components/admin/admin-utilities-importv1.vue @@ -63,7 +63,7 @@ v-col(v-if='gitAuthMode === `ssh`', cols='12') v-textarea( outlined - label='Private Key' + label='Private Key Contents' placeholder='-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----' hide-details v-model='gitPrivKey' @@ -72,7 +72,7 @@ v-col(cols='6') v-text-field( label='Username' - v-model='gitUserEmail' + v-model='gitUsername' outlined hide-details ) @@ -80,7 +80,7 @@ v-text-field( type='password' label='Password / PAT' - v-model='gitUserName' + v-model='gitPassword' outlined hide-details ) @@ -108,10 +108,14 @@ outlined hide-details ) + .caption.mt-2 This folder should be empty or not exist yet. #[strong.deep-orange--text.text--darken-2 DO NOT] point to your existing Wiki.js 1.x repository folder. In most cases, it should be left to the default value. + v-alert(color='deep-orange', outlined, icon='mdi-alert', prominent) + .body-2 - Note that if you already configured the git storage module, its configuration will be replaced with the above. + .body-2 - Although both v1 and v2 installations can use the same remote git repository, you shouldn't make edits to the same pages simultaneously. v-radio-group(v-model='contentMode', hide-details) v-divider v-radio.mt-3( - value='local' + value='disk' color='primary' ) template(v-slot:label) @@ -152,7 +156,7 @@ template(v-slot:label) div span Create groups for each unique user permissions configuration - .caption: em Note that this can result in a large amount of groups being created. + .caption: em #[strong.primary--text Recommended] | Users having identical permission sets will be assigned to the same group. Note that this can potentially result in a large amount of groups being created. v-divider v-radio.mt-3( value='SINGLE' @@ -161,7 +165,7 @@ template(v-slot:label) div span Create a single group with all imported users - .caption: em #[strong.primary--text Recommended] | The new group will have read permissions enabled by default. + .caption: em The new group will have read permissions enabled by default. v-divider v-radio.mt-3( value='NONE' @@ -172,6 +176,10 @@ span Don't create any group .caption: em Users will not be able to access your wiki until they are assigned to a group. + v-alert.mt-5(color='deep-orange', outlined, icon='mdi-alert', prominent) + .body-2 Note that any user that already exists in this installation will not be imported. A list of skipped users will be displayed upon completion. + .caption.grey--text You must first delete from this installation any user you want to migrate over from the old installation. + v-card-chin v-btn.px-3(depressed, color='deep-orange darken-2', :disabled='!wantUsers && !wantContent', @click='startImport').ml-0 v-icon(left, color='white') mdi-database-import @@ -220,9 +228,6 @@ v-icon(left) mdi-alert span {{failedUsers.length}} failed .body-2 #[strong {{successGroups}}] groups created - template(v-if='wantContent') - .body-2 #[strong {{successPages}}] pages - .body-2 #[strong {{successAssets}}] assets v-card-actions.green.darken-1 v-spacer v-btn.px-5( @@ -266,6 +271,10 @@ import _ from 'lodash' import { SemipolarSpinner } from 'epic-spinners' import utilityImportv1UsersMutation from 'gql/admin/utilities/utilities-mutation-importv1-users.gql' +import storageTargetsQuery from 'gql/admin/storage/storage-query-targets.gql' +import storageStatusQuery from 'gql/admin/storage/storage-query-status.gql' +import targetExecuteActionMutation from 'gql/admin/storage/storage-mutation-executeaction.gql' +import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql' export default { components: { @@ -274,7 +283,7 @@ export default { data() { return { importFilters: ['content', 'users'], - groupMode: 'SINGLE', + groupMode: 'MULTI', contentMode: 'git', dbConnStr: 'mongodb://', contentPath: '/wiki-v1/repo', @@ -289,14 +298,14 @@ export default { gitRepoUrl: '', gitRepoBranch: 'master', gitPrivKey: '', + gitUsername: '', + gitPassword: '', gitUserEmail: '', gitUserName: '', gitRepoPath: './data/repo', progress: 0, successUsers: 0, successPages: 0, - successGroups: 0, - successAssets: 0, showFailedUsers: false, failedUsers: [] } @@ -321,40 +330,161 @@ export default { this.progress = 0 this.failedUsers = [] - // -> Import Users + _.delay(async () => { + // -> Import Users - if (this.wantUsers) { - try { - const resp = await this.$apollo.mutate({ - mutation: utilityImportv1UsersMutation, - variables: { - mongoDbConnString: this.dbConnStr, - groupMode: this.groupMode + if (this.wantUsers) { + try { + const resp = await this.$apollo.mutate({ + mutation: utilityImportv1UsersMutation, + variables: { + mongoDbConnString: this.dbConnStr, + groupMode: this.groupMode + } + }) + const respObj = _.get(resp, 'data.system.importUsersFromV1', {}) + if (!_.get(respObj, 'responseResult.succeeded', false)) { + throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured')) } - }) - const respObj = _.get(resp, 'data.system.importUsersFromV1', {}) - if (!_.get(respObj, 'responseResult.succeeded', false)) { - throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured')) + this.successUsers = _.get(respObj, 'usersCount', 0) + this.successGroups = _.get(respObj, 'groupsCount', 0) + this.failedUsers = _.get(respObj, 'failed', []) + this.progress += 50 + } catch (err) { + this.$store.commit('pushGraphError', err) + this.isLoading = false + return } - this.successUsers = _.get(respObj, 'usersCount', 0) - this.successGroups = _.get(respObj, 'groupsCount', 0) - this.failedUsers = _.get(respObj, 'failed', []) - this.progress += 50 - } catch (err) { - this.$store.commit('pushGraphError', err) - this.isLoading = false - return } - } - // -> Import Content + // -> Import Content - if (this.wantContent) { + if (this.wantContent) { + try { + const resp = await this.$apollo.query({ + query: storageTargetsQuery, + fetchPolicy: 'network-only' + }) + if (_.has(resp, 'data.storage.targets')) { + this.progress += 10 + let targets = resp.data.storage.targets.map(str => { + let nStr = { + ...str, + config: _.sortBy(str.config.map(cfg => ({ + ...cfg, + value: JSON.parse(cfg.value) + })), [t => t.value.order]) + } - } + // -> Setup Git Module - this.isLoading = false - this.isSuccess = true + if (this.contentMode === 'git' && nStr.key === 'git') { + nStr.isEnabled = true + nStr.mode = 'sync' + nStr.syncInterval = 'PT5M' + nStr.config = [ + { key: 'authType', value: { value: this.gitAuthMode } }, + { key: 'repoUrl', value: { value: this.gitRepoUrl } }, + { key: 'branch', value: { value: this.gitRepoBranch } }, + { key: 'sshPrivateKeyMode', value: { value: 'contents' } }, + { key: 'sshPrivateKeyPath', value: { value: '' } }, + { key: 'sshPrivateKeyContent', value: { value: this.gitPrivKey } }, + { key: 'verifySSL', value: { value: this.gitVerifySSL } }, + { key: 'basicUsername', value: { value: this.gitUsername } }, + { key: 'basicPassword', value: { value: this.gitPassword } }, + { key: 'defaultEmail', value: { value: this.gitUserEmail } }, + { key: 'defaultName', value: { value: this.gitUserName } }, + { key: 'localRepoPath', value: { value: this.gitRepoPath } }, + { key: 'gitBinaryPath', value: { value: '' } } + ] + } + return nStr + }) + + // -> Save storage modules configuration + + const respSv = await this.$apollo.mutate({ + mutation: targetsSaveMutation, + variables: { + targets: targets.map(tgt => _.pick(tgt, [ + 'isEnabled', + 'key', + 'config', + 'mode', + 'syncInterval' + ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))})) + } + }) + const respObj = _.get(respSv, 'data.storage.updateTargets', {}) + if (!_.get(respObj, 'responseResult.succeeded', false)) { + throw new Error(_.get(respObj, 'responseResult.message', 'An unexpected error occured')) + } + + this.progress += 10 + + // -> Wait for success sync + + let statusAttempts = 0 + while (statusAttempts < 10) { + statusAttempts++ + const respStatus = await this.$apollo.query({ + query: storageStatusQuery, + fetchPolicy: 'network-only' + }) + if (_.has(respStatus, 'data.storage.status[0]')) { + const st = _.find(respStatus.data.storage.status, ['key', this.contentMode]) + if (!st) { + throw new Error('Storage target could not be configured.') + } + switch (st.status) { + case 'pending': + if (statusAttempts >= 10) { + throw new Error('Storage target is stuck in pending state. Try again.') + } else { + continue + } + case 'operational': + statusAttempts = 10 + break + case 'error': + throw new Error(st.message) + } + } else { + throw new Error('Failed to fetch storage sync status.') + } + } + + this.progress += 15 + + // -> Perform import all + + const respImport = await this.$apollo.mutate({ + mutation: targetExecuteActionMutation, + variables: { + targetKey: this.contentMode, + handler: 'importAll' + } + }) + + const respImportObj = _.get(respImport, 'data.storage.executeAction', {}) + if (!_.get(respImportObj, 'responseResult.succeeded', false)) { + throw new Error(_.get(respImportObj, 'responseResult.message', 'An unexpected error occured')) + } + + this.progress += 15 + } else { + throw new Error('Failed to fetch storage targets.') + } + } catch (err) { + this.$store.commit('pushGraphError', err) + this.isLoading = false + return + } + } + + this.isLoading = false + this.isSuccess = true + }, 1500) } } } diff --git a/client/graph/admin/site/site-mutation-save-config.gql b/client/graph/admin/site/site-mutation-save-config.gql index 172eaec2..87730501 100644 --- a/client/graph/admin/site/site-mutation-save-config.gql +++ b/client/graph/admin/site/site-mutation-save-config.gql @@ -13,6 +13,7 @@ mutation ( $featurePersonalWikis: Boolean! $securityIframe: Boolean! $securityReferrerPolicy: Boolean! + $securityTrustProxy: Boolean! $securityHSTS: Boolean! $securityHSTSDuration: Int! $securityCSP: Boolean! @@ -34,6 +35,7 @@ mutation ( featurePersonalWikis: $featurePersonalWikis, securityIframe: $securityIframe, securityReferrerPolicy: $securityReferrerPolicy, + securityTrustProxy: $securityTrustProxy, securityHSTS: $securityHSTS, securityHSTSDuration: $securityHSTSDuration, securityCSP: $securityCSP, diff --git a/client/graph/admin/site/site-query-config.gql b/client/graph/admin/site/site-query-config.gql index 3b62ff0b..5af45d09 100644 --- a/client/graph/admin/site/site-query-config.gql +++ b/client/graph/admin/site/site-query-config.gql @@ -15,6 +15,7 @@ featurePersonalWikis securityIframe securityReferrerPolicy + securityTrustProxy securityHSTS securityHSTSDuration securityCSP diff --git a/server/app/data.yml b/server/app/data.yml index 3ddf1231..2447cd89 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -45,6 +45,7 @@ defaults: security: securityIframe: true securityReferrerPolicy: true + securityTrustProxy: true securityHSTS: false securityHSTSDuration: 300 securityCSP: false diff --git a/server/graph/resolvers/site.js b/server/graph/resolvers/site.js index 94000741..f4cad189 100644 --- a/server/graph/resolvers/site.js +++ b/server/graph/resolvers/site.js @@ -46,6 +46,7 @@ module.exports = { WIKI.config.security = { securityIframe: args.securityIframe, securityReferrerPolicy: args.securityReferrerPolicy, + securityTrustProxy: args.securityTrustProxy, securityHSTS: args.securityHSTS, securityHSTSDuration: args.securityHSTSDuration, securityCSP: args.securityCSP, @@ -53,6 +54,12 @@ module.exports = { } await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'seo', 'logo', 'features', 'security']) + if (WIKI.config.security.securityTrustProxy) { + WIKI.app.enable('trust proxy') + } else { + WIKI.app.disable('trust proxy') + } + return { responseResult: graphHelper.generateSuccess('Site configuration updated successfully') } diff --git a/server/graph/schemas/site.graphql b/server/graph/schemas/site.graphql index 5cc573d6..0ce83ac5 100644 --- a/server/graph/schemas/site.graphql +++ b/server/graph/schemas/site.graphql @@ -38,6 +38,7 @@ type SiteMutation { featurePersonalWikis: Boolean! securityIframe: Boolean! securityReferrerPolicy: Boolean! + securityTrustProxy: Boolean! securityHSTS: Boolean! securityHSTSDuration: Int! securityCSP: Boolean! @@ -64,6 +65,7 @@ type SiteConfig { featurePersonalWikis: Boolean! securityIframe: Boolean! securityReferrerPolicy: Boolean! + securityTrustProxy: Boolean! securityHSTS: Boolean! securityHSTSDuration: Int! securityCSP: Boolean! diff --git a/server/master.js b/server/master.js index 1a9af010..ff4a6b36 100644 --- a/server/master.js +++ b/server/master.js @@ -48,7 +48,7 @@ module.exports = async () => { app.use(mw.security) app.use(cors(WIKI.config.cors)) app.options('*', cors(WIKI.config.cors)) - if (WIKI.config.trustProxy) { + if (WIKI.config.security.securityTrustProxy) { app.enable('trust proxy') }