Commit d3e693ab authored by Nick's avatar Nick

feat: mandatory password change on login + UI fixes

parent 38008f04
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
v-tab-item(:transition='false', :reverse-transition='false') v-tab-item(:transition='false', :reverse-transition='false')
.body-1.pa-3 {{ $t('admin:contribute.tshirts') }} .body-1.pa-3 {{ $t('admin:contribute.tshirts') }}
v-card-actions.ml-2 v-card-actions.ml-2
v-btn(outline, :color='darkMode ? `blue lighten-1` : `primary`', href='https://wikijs.threadless.com', large) v-btn(outlined, :color='darkMode ? `blue lighten-1` : `primary`', href='https://wikijs.threadless.com', large)
v-icon(left) mdi-tshirt-crew v-icon(left) mdi-tshirt-crew
span {{ $t('admin:contribute.shop') }} span {{ $t('admin:contribute.shop') }}
v-divider.mt-3 v-divider.mt-3
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
span {{$t('common:actions.apply')}} span {{$t('common:actions.apply')}}
v-card.mt-3.white.grey--text.text--darken-3 v-card.mt-3.white.grey--text.text--darken-3
v-alert(color='red', value='true', icon='mdi-alert', dark, prominent) v-alert(color='red', :value='true', icon='mdi-alert', dark, prominent)
span Do NOT enable these flags unless you know what you're doing! span Do NOT enable these flags unless you know what you're doing!
.caption Doing so may result in data loss or broken installation! .caption Doing so may result in data loss or broken installation!
v-card-text v-card-text
......
...@@ -92,14 +92,14 @@ ...@@ -92,14 +92,14 @@
v-flex(lg6 xs12) v-flex(lg6 xs12)
v-card.animated.fadeInUp.wait-p4s v-card.animated.fadeInUp.wait-p4s
v-toolbar(color='primary', dark, dense, flat) v-toolbar(color='indigo', dark, dense, flat)
v-toolbar-title.subtitle-1 Features v-toolbar-title.subtitle-1 Features
v-spacer v-spacer
v-chip(label, color='white', small).primary--text coming soon v-chip(label, color='white', small).indigo--text coming soon
v-card-text v-card-text
v-switch( v-switch(
label='Asset Image Optimization' label='Asset Image Optimization'
color='primary' color='indigo'
v-model='config.featureTinyPNG' v-model='config.featureTinyPNG'
persistent-hint persistent-hint
hint='Image optimization tool to reduce filesize and bandwidth costs.' hint='Image optimization tool to reduce filesize and bandwidth costs.'
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
v-divider.mt-3 v-divider.mt-3
v-switch( v-switch(
label='Page Ratings' label='Page Ratings'
color='primary' color='indigo'
v-model='config.featurePageRatings' v-model='config.featurePageRatings'
persistent-hint persistent-hint
hint='Allow users to rate pages.' hint='Allow users to rate pages.'
...@@ -129,7 +129,7 @@ ...@@ -129,7 +129,7 @@
v-divider.mt-3 v-divider.mt-3
v-switch( v-switch(
label='Page Comments' label='Page Comments'
color='primary' color='indigo'
v-model='config.featurePageComments' v-model='config.featurePageComments'
persistent-hint persistent-hint
hint='Allow users to leave comments on pages.' hint='Allow users to leave comments on pages.'
...@@ -139,13 +139,75 @@ ...@@ -139,13 +139,75 @@
v-divider.mt-3 v-divider.mt-3
v-switch( v-switch(
label='Personal Wikis' label='Personal Wikis'
color='primary' color='indigo'
v-model='config.featurePersonalWikis' v-model='config.featurePersonalWikis'
persistent-hint persistent-hint
hint='Allow users to have their own personal wiki.' hint='Allow users to have their own personal wiki.'
disabled disabled
) )
v-card.mt-5.animated.fadeInUp.wait-p5s
v-toolbar(color='red darken-2', dark, dense, flat)
v-toolbar-title.subtitle-1 Security
v-card-text
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.
v-switch.mt-3(
label='Block IFrame Embedding'
color='red darken-2'
v-model='config.securityIframe'
persistent-hint
hint='Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.'
)
v-divider.mt-3
v-switch(
label='Same Origin Referrer Policy'
color='red darken-2'
v-model='config.securityReferrerPolicy'
persistent-hint
hint='Limits the referrer header to same origin.'
)
v-divider.mt-3
v-switch(
label='Enforce HSTS'
color='red darken-2'
v-model='config.securityHSTS'
persistent-hint
hint='This ensures the connection cannot be established through an insecure HTTP connection.'
)
v-select.mt-5(
outlined
label='HSTS Max Age'
:items='hstsDurations'
v-model='config.securityHSTSDuration'
prepend-icon='mdi-subdirectory-arrow-right'
:disabled='!config.securityHSTS'
hide-details
style='max-width: 450px;'
)
.pl-11.mt-3
.caption Defines the duration for which the server should only deliver content through HTTPS.
.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.
v-divider.mt-3
v-switch(
label='Enforce CSP'
color='red darken-2'
v-model='config.securityCSP'
persistent-hint
hint='Restricts scripts to pre-approved content sources.'
disabled
)
v-textarea.mt-5(
label='CSP Directives'
outlined
v-model='config.securityCSPDirectives'
prepend-icon='mdi-subdirectory-arrow-right'
persistent-hint
hint='One directive per line.'
disabled
)
</template> </template>
<script> <script>
...@@ -163,12 +225,6 @@ export default { ...@@ -163,12 +225,6 @@ export default {
{ text: 'Google Analytics', value: 'ga' }, { text: 'Google Analytics', value: 'ga' },
{ text: 'Google Tag Manager', value: 'gtm' } { text: 'Google Tag Manager', value: 'gtm' }
], ],
metaRobots: [
{ text: 'Index', value: 'index' },
{ text: 'Follow', value: 'follow' },
{ text: 'No Index', value: 'noindex' },
{ text: 'No Follow', value: 'nofollow' }
],
config: { config: {
host: '', host: '',
title: '', title: '',
...@@ -183,8 +239,28 @@ export default { ...@@ -183,8 +239,28 @@ export default {
featurePageRatings: false, featurePageRatings: false,
featurePageComments: false, featurePageComments: false,
featurePersonalWikis: false, featurePersonalWikis: false,
featureTinyPNG: false featureTinyPNG: false,
} securityIframe: true,
securityReferrerPolicy: true,
securityHSTS: false,
securityHSTSDuration: 0,
securityCSP: false,
securityCSPDirectives: ''
},
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' }
],
metaRobots: [
{ text: 'Index', value: 'index' },
{ text: 'Follow', value: 'follow' },
{ text: 'No Index', value: 'noindex' },
{ text: 'No Follow', value: 'nofollow' }
]
} }
}, },
computed: { computed: {
...@@ -198,18 +274,24 @@ export default { ...@@ -198,18 +274,24 @@ export default {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: siteUpdateConfigMutation, mutation: siteUpdateConfigMutation,
variables: { variables: {
host: this.config.host || '', host: _.get(this.config, 'host', ''),
title: this.config.title || '', title: _.get(this.config, 'title', ''),
description: this.config.description || '', description: _.get(this.config, 'description', ''),
robots: this.config.robots || [], robots: _.get(this.config, 'robots', []),
analyticsService: this.config.analyticsService || '', analyticsService: _.get(this.config, 'analyticsService', ''),
analyticsId: this.config.analyticsId || '', analyticsId: _.get(this.config, 'analyticsId', ''),
company: this.config.company || '', company: _.get(this.config, 'company', ''),
hasLogo: this.config.hasLogo || false, hasLogo: _.get(this.config, 'hasLogo', false),
logoIsSquare: this.config.logoIsSquare || false, logoIsSquare: _.get(this.config, 'logoIsSquare', false),
featurePageRatings: this.config.featurePageRatings || false, featurePageRatings: _.get(this.config, 'featurePageRatings', false),
featurePageComments: this.config.featurePageComments || false, featurePageComments: _.get(this.config, 'featurePageComments', false),
featurePersonalWikis: this.config.featurePersonalWikis || false featurePersonalWikis: _.get(this.config, 'featurePersonalWikis', false),
securityIframe: _.get(this.config, 'securityIframe', false),
securityReferrerPolicy: _.get(this.config, 'securityReferrerPolicy', false),
securityHSTS: _.get(this.config, 'securityHSTS', false),
securityHSTSDuration: _.get(this.config, 'securityHSTSDuration', 0),
securityCSP: _.get(this.config, 'securityCSP', false),
securityCSPDirectives: _.get(this.config, 'securityCSPDirectives', '')
}, },
watchLoading (isLoading) { watchLoading (isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update') this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')
......
...@@ -23,23 +23,18 @@ ...@@ -23,23 +23,18 @@
must-sort, must-sort,
hide-default-footer hide-default-footer
) )
template(slot='item', slot-scope='props') template(v-slot:item.actions='{ item }')
tr(:active='props.selected')
td.text-xs-right {{ props.item.id }}
td {{ props.item.name }}
td {{ props.item.email }}
td
v-menu(bottom, right, min-width='200') v-menu(bottom, right, min-width='200')
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon, v-on='on', small) v-btn(icon, v-on='on', small)
v-icon.grey--text.text--darken-1 mdi-dots-horizontal v-icon.grey--text.text--darken-1 mdi-dots-horizontal
v-list(dense, nav) v-list(dense, nav)
v-list-item(:to='`/users/` + props.item.id') v-list-item(:to='`/users/` + item.id')
v-list-item-action: v-icon(color='primary') mdi-account-outline v-list-item-action: v-icon(color='primary') mdi-account-outline
v-list-item-content v-list-item-content
v-list-item-title View User Profile v-list-item-title View User Profile
template(v-if='props.item.id !== 2') template(v-if='item.id !== 2')
v-list-item(@click='unassignUser(props.item.id)') v-list-item(@click='unassignUser(item.id)')
v-list-item-action: v-icon(color='orange') mdi-account-remove-outline v-list-item-action: v-icon(color='orange') mdi-account-remove-outline
v-list-item-content v-list-item-content
v-list-item-title Unassign v-list-item-title Unassign
...@@ -70,10 +65,10 @@ export default { ...@@ -70,10 +65,10 @@ export default {
data() { data() {
return { return {
headers: [ headers: [
{ text: 'ID', value: 'id', width: 50, align: 'right' }, { text: 'ID', value: 'id', width: 50 },
{ text: 'Name', value: 'name' }, { text: 'Name', value: 'name' },
{ text: 'Email', value: 'email' }, { text: 'Email', value: 'email' },
{ text: '', value: 'actions', sortable: false, width: 50 } { text: 'Actions', value: 'actions', sortable: false, width: 50 }
], ],
searchUserDialog: false, searchUserDialog: false,
pagination: 1, pagination: 1,
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
span New Group span New Group
v-card v-card
.dialog-header.is-short New Group .dialog-header.is-short New Group
v-card-text v-card-text.pt-5
v-text-field.md2( v-text-field.md2(
outlined outlined
prepend-icon='mdi-account-group' prepend-icon='mdi-account-group'
......
...@@ -30,11 +30,11 @@ ...@@ -30,11 +30,11 @@
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn.mx-1.animated.fadeInDown.wait-p1s(color='red', large, outlined, v-on='on') v-btn.mx-1.animated.fadeInDown.wait-p1s(color='red', large, outlined, v-on='on')
v-icon(color='red') mdi-trash-can-outline v-icon(color='red') mdi-trash-can-outline
v-card.wiki-form v-card
.dialog-header.is-short.is-red .dialog-header.is-short.is-red
v-icon.mr-2(color='white') mdi-file-document-box-remove-outline v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
span {{$t('common:page.delete')}} span {{$t('common:page.delete')}}
v-card-text v-card-text.pt-5
i18next.body-2(path='common:page.deleteTitle', tag='div') i18next.body-2(path='common:page.deleteTitle', tag='div')
span.red--text.text--darken-2(place='title') {{page.title}} span.red--text.text--darken-2(place='title') {{page.title}}
.caption {{$t('common:page.deleteSubtitle')}} .caption {{$t('common:page.deleteSubtitle')}}
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
span.red--text.text--darken-2 /{{page.path}} span.red--text.text--darken-2 /{{page.path}}
v-card-chin v-card-chin
v-spacer v-spacer
v-btn(flat, @click='deletePageDialog = false', :disabled='loading') {{$t('common:actions.cancel')}} v-btn(text, @click='deletePageDialog = false', :disabled='loading') {{$t('common:actions.cancel')}}
v-btn(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}} v-btn(color='red darken-2', @click='deletePage', :loading='loading').white--text {{$t('common:actions.delete')}}
v-btn.ml-1.animated.fadeInDown(color='teal', large, outlined, @click='rerenderPage') v-btn.ml-1.animated.fadeInDown(color='teal', large, outlined, @click='rerenderPage')
v-icon(left) mdi-cube-scan v-icon(left) mdi-cube-scan
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
td {{ props.item.createdAt | moment('calendar') }} td {{ props.item.createdAt | moment('calendar') }}
td {{ props.item.updatedAt | moment('calendar') }} td {{ props.item.updatedAt | moment('calendar') }}
template(slot='no-data') template(slot='no-data')
v-alert.ma-3(icon='warning', :value='true', outline) No pages to display. v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display.
.text-xs-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1') .text-xs-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
v-pagination(v-model='pagination', :length='pageTotal') v-pagination(v-model='pagination', :length='pageTotal')
</template> </template>
......
...@@ -26,8 +26,8 @@ ...@@ -26,8 +26,8 @@
v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable') v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
v-list-item-avatar(size='24') v-list-item-avatar(size='24')
v-icon(color='grey', v-if='!eng.isAvailable') mdi-minus-box-outline v-icon(color='grey', v-if='!eng.isAvailable') mdi-minus-box-outline
v-icon(color='primary', v-else-if='eng.key === selectedEngine') mdi-checkbox-marked-outline v-icon(color='primary', v-else-if='eng.key === selectedEngine') mdi-checkbox-marked-circle-outline
v-icon(color='grey', v-else) mdi-checkbox-blank-outline v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline
v-list-item-content v-list-item-content
v-list-item-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }} v-list-item-title.body-2(:class='!eng.isAvailable ? `grey--text` : (selectedEngine === eng.key ? `primary--text` : ``)') {{ eng.title }}
v-list-item-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }} v-list-item-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}
......
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
v-icon(color='white') mdi-clock-outline v-icon(color='white') mdi-clock-outline
v-list-item-content v-list-item-content
v-list-item-title.body-2 {{tgt.title}} v-list-item-title.body-2 {{tgt.title}}
v-list-item-sub-title.purple--text.caption {{tgt.status}} v-list-item-subtitle.purple--text.caption {{tgt.status}}
v-list-item-action v-list-item-action
v-progress-circular(indeterminate, :size='20', :width='2', color='purple') v-progress-circular(indeterminate, :size='20', :width='2', color='purple')
template(v-else-if='tgt.status === `operational`') template(v-else-if='tgt.status === `operational`')
...@@ -57,13 +57,13 @@ ...@@ -57,13 +57,13 @@
v-icon(color='white') mdi-check-circle v-icon(color='white') mdi-check-circle
v-list-item-content v-list-item-content
v-list-item-title.body-2 {{tgt.title}} v-list-item-title.body-2 {{tgt.title}}
v-list-item-sub-title.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}} v-list-item-subtitle.green--text.caption {{$t('admin:storage.lastSync', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
template(v-else) template(v-else)
v-list-item-avatar(color='red') v-list-item-avatar(color='red')
v-icon(color='white') mdi-close-circle-outline v-icon(color='white') mdi-close-circle-outline
v-list-item-content v-list-item-content
v-list-item-title.body-2 {{tgt.title}} v-list-item-title.body-2 {{tgt.title}}
v-list-item-sub-title.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}} v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
v-list-item-action v-list-item-action
v-menu v-menu
v-btn(slot='activator', icon) v-btn(slot='activator', icon)
...@@ -86,6 +86,10 @@ ...@@ -86,6 +86,10 @@
img(:src='target.logo', :alt='target.title') img(:src='target.logo', :alt='target.title')
.body-2.pt-3 {{target.description}} .body-2.pt-3 {{target.description}}
.body-2.pt-3.pb-5: a(:href='target.website') {{target.website}} .body-2.pt-3.pb-5: a(:href='target.website') {{target.website}}
i18next.body-2(path='admin:storage.targetState', tag='div', v-if='target.isEnabled')
v-chip(color='green', small, dark, label, place='state') {{$t('admin:storage.targetStateActive')}}
i18next.body-2(path='admin:storage.targetState', tag='div', v-else)
v-chip(color='red', small, dark, label, place='state') {{$t('admin:storage.targetStateInactive')}}
v-divider.mt-3 v-divider.mt-3
.overline.my-5 {{$t('admin:storage.targetConfig')}} .overline.my-5 {{$t('admin:storage.targetConfig')}}
.body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}} .body-2.ml-3(v-if='!target.config || target.config.length < 1'): em {{$t('admin:storage.noConfigOption')}}
...@@ -179,6 +183,8 @@ ...@@ -179,6 +183,8 @@
template(v-if='target.actions && target.actions.length > 0') template(v-if='target.actions && target.actions.length > 0')
v-divider.mt-3 v-divider.mt-3
.overline.my-5 {{$t('admin:storage.actions')}} .overline.my-5 {{$t('admin:storage.actions')}}
v-alert(outlined, :value='!target.isEnabled', color='red', icon='mdi-alert')
.body-2 {{$t('admin:storage.actionsInactiveWarn')}}
v-container.pt-0(grid-list-xl, fluid) v-container.pt-0(grid-list-xl, fluid)
v-layout(row, wrap, fill-height) v-layout(row, wrap, fill-height)
v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler') v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
...@@ -190,7 +196,7 @@ ...@@ -190,7 +196,7 @@
@click='executeAction(target.key, act.handler)' @click='executeAction(target.key, act.handler)'
outlined outlined
:color='$vuetify.theme.dark ? `blue` : `primary`' :color='$vuetify.theme.dark ? `blue` : `primary`'
:disabled='runningAction' :disabled='runningAction || !target.isEnabled'
:loading='runningActionHandler === act.handler' :loading='runningActionHandler === act.handler'
) {{$t('admin:storage.actionRun')}} ) {{$t('admin:storage.actionRun')}}
......
...@@ -13,13 +13,13 @@ ...@@ -13,13 +13,13 @@
v-btn.animated.fadeInLeft.wait-p2s.btn-animate-rotate(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, @click='refresh'): v-icon(color='grey') mdi-refresh v-btn.animated.fadeInLeft.wait-p2s.btn-animate-rotate(fab, absolute, :right='!$vuetify.rtl', :left='$vuetify.rtl', top, small, light, @click='refresh'): v-icon(color='grey') mdi-refresh
v-subheader Wiki.js v-subheader Wiki.js
v-list(two-line, dense) v-list(two-line, dense)
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-icon.blue.white--text mdi-application-export v-icon.blue.white--text mdi-application-export
v-list-item-content v-list-item-content
v-list-item-title {{ $t('admin:system.currentVersion') }} v-list-item-title {{ $t('admin:system.currentVersion') }}
v-list-item-subtitle {{ info.currentVersion }} v-list-item-subtitle {{ info.currentVersion }}
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-icon.blue.white--text mdi-inbox-arrow-up v-icon.blue.white--text mdi-inbox-arrow-up
v-list-item-content v-list-item-content
...@@ -31,38 +31,38 @@ ...@@ -31,38 +31,38 @@
v-divider.mt-3 v-divider.mt-3
v-subheader {{ $t('admin:system.hostInfo') }} v-subheader {{ $t('admin:system.hostInfo') }}
v-list(two-line, dense) v-list(two-line, dense)
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-avatar.blue-grey(size='40') v-avatar.blue-grey(size='40')
v-icon(color='white') {{platformLogo}} v-icon(color='white') {{platformLogo}}
v-list-item-content v-list-item-content
v-list-item-title {{ $t('admin:system.os') }} v-list-item-title {{ $t('admin:system.os') }}
v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }} v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-icon.blue-grey.white--text mdi-desktop-classic v-icon.blue-grey.white--text mdi-desktop-classic
v-list-item-content v-list-item-content
v-list-item-title {{ $t('admin:system.hostname') }} v-list-item-title {{ $t('admin:system.hostname') }}
v-list-item-subtitle {{ info.hostname }} v-list-item-subtitle {{ info.hostname }}
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-icon.blue-grey.white--text mdi-cpu-64-bit v-icon.blue-grey.white--text mdi-cpu-64-bit
v-list-item-content v-list-item-content
v-list-item-title {{ $t('admin:system.cpuCores') }} v-list-item-title {{ $t('admin:system.cpuCores') }}
v-list-item-subtitle {{ info.cpuCores }} v-list-item-subtitle {{ info.cpuCores }}
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-icon.blue-grey.white--text mdi-memory v-icon.blue-grey.white--text mdi-memory
v-list-item-content v-list-item-content
v-list-item-title {{ $t('admin:system.totalRAM') }} v-list-item-title {{ $t('admin:system.totalRAM') }}
v-list-item-subtitle {{ info.ramTotal }} v-list-item-subtitle {{ info.ramTotal }}
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-icon.blue-grey.white--text mdi-iframe-outline v-icon.blue-grey.white--text mdi-iframe-outline
v-list-item-content v-list-item-content
v-list-item-title {{ $t('admin:system.workingDirectory') }} v-list-item-title {{ $t('admin:system.workingDirectory') }}
v-list-item-subtitle {{ info.workingDirectory }} v-list-item-subtitle {{ info.workingDirectory }}
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline
v-list-item-content v-list-item-content
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
v-card.pb-3.animated.fadeInUp.wait-p4s v-card.pb-3.animated.fadeInUp.wait-p4s
v-subheader Node.js v-subheader Node.js
v-list(dense) v-list(dense)
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-avatar.light-green(size='40') v-avatar.light-green(size='40')
v-icon(color='white') mdi-nodejs v-icon(color='white') mdi-nodejs
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
v-divider.mt-3 v-divider.mt-3
v-subheader {{ info.dbType }} v-subheader {{ info.dbType }}
v-list(dense) v-list(dense)
v-list-item(avatar) v-list-item
v-list-item-avatar v-list-item-avatar
v-avatar.indigo.darken-1(size='40') v-avatar.indigo.darken-1(size='40')
v-icon(color='white') mdi-database v-icon(color='white') mdi-database
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
v-btn.mx-0(color='white', outlined, disabled, dark) v-btn.mx-0(color='white', outlined, disabled, dark)
v-icon(left) mdi-database-import v-icon(left) mdi-database-import
span Bulk Import span Bulk Import
v-card-text v-card-text.pt-5
v-select( v-select(
:items='providers' :items='providers'
item-text='title' item-text='title'
...@@ -89,6 +89,7 @@ ...@@ -89,6 +89,7 @@
label='Send a welcome email' label='Send a welcome email'
hide-details hide-details
v-model='sendWelcomeEmail' v-model='sendWelcomeEmail'
disabled
) )
v-card-chin v-card-chin
v-spacer v-spacer
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.dialog-header.is-short.is-red .dialog-header.is-short.is-red
v-icon.mr-2(color='white') mdi-file-document-box-remove-outline v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
span {{$t('common:page.delete')}} span {{$t('common:page.delete')}}
v-card-text v-card-text.pt-5
i18next.body-1(path='common:page.deleteTitle', tag='div') i18next.body-1(path='common:page.deleteTitle', tag='div')
span.red--text.text--darken-2(place='title') {{pageTitle}} span.red--text.text--darken-2(place='title') {{pageTitle}}
.caption {{$t('common:page.deleteSubtitle')}} .caption {{$t('common:page.deleteSubtitle')}}
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
v-model='dialogOpen' v-model='dialogOpen'
max-width='650' max-width='650'
) )
v-card.wiki-form v-card
.dialog-header .dialog-header
span {{$t('common:user.search')}} span {{$t('common:user.search')}}
v-spacer v-spacer
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
:width='2' :width='2'
v-show='searchLoading' v-show='searchLoading'
) )
v-card-text v-card-text.pt-5
v-text-field( v-text-field(
outlined outlined
:label='$t(`common:user.searchPlaceholder`)' :label='$t(`common:user.searchPlaceholder`)'
...@@ -56,7 +56,7 @@ import searchUsersQuery from 'gql/common/common-users-query-search.gql' ...@@ -56,7 +56,7 @@ import searchUsersQuery from 'gql/common/common-users-query-search.gql'
export default { export default {
filters: { filters: {
initials(val) { initials(val) {
return val.split(' ').map(v => v.substring(0, 1)).join() return val.split(' ').map(v => v.substring(0, 1)).join('')
} }
}, },
props: { props: {
......
...@@ -11,6 +11,12 @@ mutation ( ...@@ -11,6 +11,12 @@ mutation (
$featurePageRatings: Boolean! $featurePageRatings: Boolean!
$featurePageComments: Boolean! $featurePageComments: Boolean!
$featurePersonalWikis: Boolean! $featurePersonalWikis: Boolean!
$securityIframe: Boolean!
$securityReferrerPolicy: Boolean!
$securityHSTS: Boolean!
$securityHSTSDuration: Int!
$securityCSP: Boolean!
$securityCSPDirectives: String!
) { ) {
site { site {
updateConfig( updateConfig(
...@@ -25,7 +31,13 @@ mutation ( ...@@ -25,7 +31,13 @@ mutation (
logoIsSquare: $logoIsSquare, logoIsSquare: $logoIsSquare,
featurePageRatings: $featurePageRatings, featurePageRatings: $featurePageRatings,
featurePageComments: $featurePageComments, featurePageComments: $featurePageComments,
featurePersonalWikis: $featurePersonalWikis featurePersonalWikis: $featurePersonalWikis,
securityIframe: $securityIframe,
securityReferrerPolicy: $securityReferrerPolicy,
securityHSTS: $securityHSTS,
securityHSTSDuration: $securityHSTSDuration,
securityCSP: $securityCSP,
securityCSPDirectives: $securityCSPDirectives
) { ) {
responseResult { responseResult {
succeeded succeeded
......
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
featurePageRatings featurePageRatings
featurePageComments featurePageComments
featurePersonalWikis featurePersonalWikis
securityIframe
securityReferrerPolicy
securityHSTS
securityHSTSDuration
securityCSP
securityCSPDirectives
} }
} }
} }
...@@ -10,6 +10,8 @@ query ($id: Int!) { ...@@ -10,6 +10,8 @@ query ($id: Int!) {
jobTitle jobTitle
timezone timezone
isSystem isSystem
isActive
isVerified
createdAt createdAt
updatedAt updatedAt
groups { groups {
......
mutation($continuationToken: String!, $newPassword: String!) {
authentication {
loginChangePassword(continuationToken: $continuationToken, newPassword: $newPassword) {
responseResult {
succeeded
errorCode
slug
message
}
jwt
}
}
}
...@@ -8,8 +8,9 @@ mutation($username: String!, $password: String!, $strategy: String!) { ...@@ -8,8 +8,9 @@ mutation($username: String!, $password: String!, $strategy: String!) {
message message
} }
jwt jwt
tfaRequired mustChangePwd
tfaLoginToken mustProvideTFA
continuationToken
} }
} }
} }
mutation($loginToken: String!, $securityCode: String!) { mutation($continuationToken: String!, $securityCode: String!) {
authentication { authentication {
loginTFA(loginToken: $loginToken, securityCode: $securityCode) { loginTFA(continuationToken: $continuationToken, securityCode: $securityCode) {
responseResult { responseResult {
succeeded succeeded
errorCode errorCode
slug slug
message message
} }
jwt
} }
} }
} }
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: mc('grey', '50'); color: mc('grey', '50');
font-family: Roboto, Arial, sans-serif;
img { img {
width: 250px; width: 250px;
...@@ -57,8 +58,20 @@ ...@@ -57,8 +58,20 @@
} }
} }
> strong {
font-size: 1.5rem;
}
> span {
margin-top: 1rem;
}
> pre {
margin-top: 2rem;
code { code {
color: mc('grey', '500'); color: mc('grey', '500');
font-size: .8rem; font-size: .8rem;
} }
}
} }
...@@ -70,20 +70,7 @@ ...@@ -70,20 +70,7 @@
v-list-item-title.px-3.caption.grey--text(:class='darkMode ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}} v-list-item-title.px-3.caption.grey--text(:class='darkMode ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}
//- v-divider(inset, v-if='tocIdx < toc.length - 1') //- v-divider(inset, v-if='tocIdx < toc.length - 1')
v-card.mt-5 v-card.mt-5(v-if='tags.length > 0')
.pa-5.pt-3
.overline.indigo--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span {{$t('common:page.lastEditedBy')}}
v-spacer
v-tooltip(top, v-if='isAuthenticated')
template(v-slot:activator='{ on }')
v-btn.btn-animate-edit(icon, :href='"/h/" + locale + "/" + path', v-on='on', x-small)
v-icon(color='grey', dense) mdi-history
span History
.body-2.grey--text(:class='darkMode ? `` : `text--darken-3`') {{ authorName }}
.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
v-card.mt-5(v-if='tags.length > 0 || true')
.pa-5 .pa-5
.overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') Tags .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') Tags
v-chip.mr-1( v-chip.mr-1(
...@@ -98,6 +85,19 @@ ...@@ -98,6 +85,19 @@
v-card.mt-5 v-card.mt-5
.pa-5 .pa-5
.overline.indigo--text.d-flex.align-center(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span {{$t('common:page.lastEditedBy')}}
v-spacer
v-tooltip(top, v-if='isAuthenticated')
template(v-slot:activator='{ on }')
v-btn.btn-animate-edit(icon, :href='"/h/" + locale + "/" + path', v-on='on', x-small)
v-icon(color='grey', dense) mdi-history
span History
.body-2.grey--text(:class='darkMode ? `` : `text--darken-3`') {{ authorName }}
.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
v-card.mt-5
.pa-5
.overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating .overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating
.text-center .text-center
v-rating( v-rating(
...@@ -108,20 +108,21 @@ ...@@ -108,20 +108,21 @@
hover hover
) )
.caption.grey--text 5 votes .caption.grey--text 5 votes
v-divider
v-toolbar(:color='darkMode ? `grey darken-3` : `grey lighten-4`', flat, dense) v-card.mt-5(flat)
v-toolbar(:color='darkMode ? `grey darken-3` : `grey lighten-3`', flat, dense)
v-spacer v-spacer
v-tooltip(bottom) v-tooltip(bottom)
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon, tile, small, v-on='on'): v-icon(color='grey') mdi-bookmark v-btn(icon, tile, v-on='on'): v-icon(color='grey') mdi-bookmark
span {{$t('common:page.bookmark')}} span {{$t('common:page.bookmark')}}
v-tooltip(bottom) v-tooltip(bottom)
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon, tile, small, v-on='on'): v-icon(color='grey') mdi-share-variant v-btn(icon, tile, v-on='on'): v-icon(color='grey') mdi-share-variant
span {{$t('common:page.share')}} span {{$t('common:page.share')}}
v-tooltip(bottom) v-tooltip(bottom)
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon, tile, small, v-on='on'): v-icon(color='grey') mdi-printer v-btn(icon, tile, v-on='on'): v-icon(color='grey') mdi-printer
span {{$t('common:page.printFormat')}} span {{$t('common:page.printFormat')}}
v-spacer v-spacer
......
...@@ -32,7 +32,7 @@ html ...@@ -32,7 +32,7 @@ html
link( link(
type='text/css' type='text/css'
rel='stylesheet' rel='stylesheet'
href='https://use.fontawesome.com/releases/v5.9.0/css/all.css' href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
) )
else if config.theming.iconset === 'fa4' else if config.theming.iconset === 'fa4'
link( link(
......
...@@ -36,7 +36,7 @@ html(lang=siteConfig.lang) ...@@ -36,7 +36,7 @@ html(lang=siteConfig.lang)
link( link(
type='text/css' type='text/css'
rel='stylesheet' rel='stylesheet'
href='https://use.fontawesome.com/releases/v5.9.0/css/all.css' href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
) )
else if config.theming.iconset === 'fa4' else if config.theming.iconset === 'fa4'
link( link(
......
...@@ -35,13 +35,13 @@ ...@@ -35,13 +35,13 @@
}, },
"dependencies": { "dependencies": {
"@aoberoi/passport-slack": "1.0.5", "@aoberoi/passport-slack": "1.0.5",
"@bugsnag/js": "6.3.2", "@bugsnag/js": "6.4.0",
"algoliasearch": "3.33.0", "algoliasearch": "3.33.0",
"apollo-fetch": "0.7.0", "apollo-fetch": "0.7.0",
"apollo-server": "2.8.1", "apollo-server": "2.9.0",
"apollo-server-express": "2.8.1", "apollo-server-express": "2.9.0",
"auto-load": "3.0.4", "auto-load": "3.0.4",
"aws-sdk": "2.503.0", "aws-sdk": "2.517.0",
"axios": "0.19.0", "axios": "0.19.0",
"azure-search-client": "3.1.5", "azure-search-client": "3.1.5",
"bcryptjs-then": "1.0.1", "bcryptjs-then": "1.0.1",
...@@ -67,18 +67,18 @@ ...@@ -67,18 +67,18 @@
"express": "4.17.1", "express": "4.17.1",
"express-brute": "1.0.1", "express-brute": "1.0.1",
"express-session": "1.16.2", "express-session": "1.16.2",
"file-type": "12.1.0", "file-type": "12.2.0",
"filesize": "4.1.2", "filesize": "4.1.2",
"fs-extra": "8.1.0", "fs-extra": "8.1.0",
"getos": "3.1.1", "getos": "3.1.1",
"graphql": "14.4.2", "graphql": "14.5.3",
"graphql-list-fields": "2.0.2", "graphql-list-fields": "2.0.2",
"graphql-rate-limit-directive": "1.1.0", "graphql-rate-limit-directive": "1.1.0",
"graphql-subscriptions": "1.1.0", "graphql-subscriptions": "1.1.0",
"graphql-tools": "4.0.5", "graphql-tools": "4.0.5",
"highlight.js": "9.15.9", "highlight.js": "9.15.10",
"i18next": "17.0.8", "i18next": "17.0.12",
"i18next-express-middleware": "1.8.0", "i18next-express-middleware": "1.8.1",
"i18next-node-fs-backend": "2.1.3", "i18next-node-fs-backend": "2.1.3",
"image-size": "0.7.4", "image-size": "0.7.4",
"js-base64": "2.5.1", "js-base64": "2.5.1",
...@@ -86,12 +86,12 @@ ...@@ -86,12 +86,12 @@
"js-yaml": "3.13.1", "js-yaml": "3.13.1",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"klaw": "3.0.0", "klaw": "3.0.0",
"knex": "0.19.1", "knex": "0.19.2",
"lodash": "4.17.15", "lodash": "4.17.15",
"markdown-it": "9.0.1", "markdown-it": "9.1.0",
"markdown-it-abbr": "1.0.4", "markdown-it-abbr": "1.0.4",
"markdown-it-anchor": "5.2.4", "markdown-it-anchor": "5.2.4",
"markdown-it-attrs": "3.0.0", "markdown-it-attrs": "3.0.1",
"markdown-it-emoji": "1.4.0", "markdown-it-emoji": "1.4.0",
"markdown-it-expand-tabs": "1.0.13", "markdown-it-expand-tabs": "1.0.13",
"markdown-it-external-links": "0.0.6", "markdown-it-external-links": "0.0.6",
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
"mime-types": "2.1.24", "mime-types": "2.1.24",
"moment": "2.24.0", "moment": "2.24.0",
"moment-timezone": "0.5.26", "moment-timezone": "0.5.26",
"mongodb": "3.2.7", "mongodb": "3.3.1",
"mssql": "5.1.0", "mssql": "5.1.0",
"multer": "1.4.2", "multer": "1.4.2",
"mysql2": "1.6.5", "mysql2": "1.6.5",
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
"nodemailer": "6.3.0", "nodemailer": "6.3.0",
"objection": "1.6.9", "objection": "1.6.9",
"passport": "0.4.0", "passport": "0.4.0",
"passport-auth0": "1.2.0", "passport-auth0": "1.2.1",
"passport-azure-ad": "4.1.0", "passport-azure-ad": "4.1.0",
"passport-cas": "0.1.1", "passport-cas": "0.1.1",
"passport-discord": "0.1.3", "passport-discord": "0.1.3",
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
"passport-saml": "1.1.0", "passport-saml": "1.1.0",
"passport-twitch": "1.0.3", "passport-twitch": "1.0.3",
"pem-jwk": "2.0.0", "pem-jwk": "2.0.0",
"pg": "7.12.0", "pg": "7.12.1",
"pg-hstore": "2.3.3", "pg-hstore": "2.3.3",
"pg-query-stream": "2.0.0", "pg-query-stream": "2.0.0",
"pg-tsquery": "8.0.5", "pg-tsquery": "8.0.5",
...@@ -152,18 +152,18 @@ ...@@ -152,18 +152,18 @@
"serve-favicon": "2.5.0", "serve-favicon": "2.5.0",
"simple-git": "1.124.0", "simple-git": "1.124.0",
"solr-node": "1.2.1", "solr-node": "1.2.1",
"sqlite3": "4.0.9", "sqlite3": "4.1.0",
"striptags": "3.1.1", "striptags": "3.1.1",
"subscriptions-transport-ws": "0.9.16", "subscriptions-transport-ws": "0.9.16",
"tar-fs": "2.0.0", "tar-fs": "2.0.0",
"twemoji": "12.1.2", "twemoji": "12.1.2",
"uslug": "1.0.4", "uslug": "1.0.4",
"uuid": "3.3.2", "uuid": "3.3.3",
"validate.js": "0.13.1", "validate.js": "0.13.1",
"validator": "11.1.0", "validator": "11.1.0",
"validator-as-promised": "1.0.2", "validator-as-promised": "1.0.2",
"winston": "3.2.1", "winston": "3.2.1",
"yargs": "13.3.0" "yargs": "14.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.5.0", "@babel/cli": "^7.5.0",
...@@ -179,13 +179,13 @@ ...@@ -179,13 +179,13 @@
"@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/polyfill": "^7.4.4", "@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.4", "@babel/preset-env": "^7.5.4",
"@mdi/font": "3.8.95", "@mdi/font": "4.1.95",
"@panter/vue-i18next": "0.15.1", "@panter/vue-i18next": "0.15.1",
"@vue/babel-preset-app": "3.10.0", "@vue/babel-preset-app": "3.11.0",
"animate-sass": "0.8.2", "animate-sass": "0.8.2",
"animated-number-vue": "1.0.0", "animated-number-vue": "1.0.0",
"apollo-cache-inmemory": "1.6.2", "apollo-cache-inmemory": "1.6.3",
"apollo-client": "2.6.3", "apollo-client": "2.6.4",
"apollo-link": "1.2.12", "apollo-link": "1.2.12",
"apollo-link-batch-http": "1.2.12", "apollo-link-batch-http": "1.2.12",
"apollo-link-error": "1.1.11", "apollo-link-error": "1.1.11",
...@@ -195,9 +195,9 @@ ...@@ -195,9 +195,9 @@
"apollo-utilities": "1.3.2", "apollo-utilities": "1.3.2",
"autoprefixer": "9.6.1", "autoprefixer": "9.6.1",
"babel-eslint": "10.0.2", "babel-eslint": "10.0.2",
"babel-jest": "24.8.0", "babel-jest": "24.9.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-graphql-tag": "2.4.0", "babel-plugin-graphql-tag": "2.5.0",
"babel-plugin-lodash": "3.3.4", "babel-plugin-lodash": "3.3.4",
"babel-plugin-prismjs": "1.1.1", "babel-plugin-prismjs": "1.1.1",
"babel-plugin-transform-imports": "2.0.0", "babel-plugin-transform-imports": "2.0.0",
...@@ -206,26 +206,26 @@ ...@@ -206,26 +206,26 @@
"chart.js": "2.8.0", "chart.js": "2.8.0",
"clean-webpack-plugin": "3.0.0", "clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "5.0.4", "copy-webpack-plugin": "5.0.4",
"core-js": "3.1.4", "core-js": "3.2.1",
"css-loader": "3.1.0", "css-loader": "3.2.0",
"cssnano": "4.1.10", "cssnano": "4.1.10",
"duplicate-package-checker-webpack-plugin": "3.0.0", "duplicate-package-checker-webpack-plugin": "3.0.0",
"epic-spinners": "1.1.0", "epic-spinners": "1.1.0",
"eslint": "6.1.0", "eslint": "6.2.2",
"eslint-config-requarks": "1.0.7", "eslint-config-requarks": "1.0.7",
"eslint-config-standard": "13.0.1", "eslint-config-standard": "14.0.1",
"eslint-plugin-import": "2.18.2", "eslint-plugin-import": "2.18.2",
"eslint-plugin-node": "9.1.0", "eslint-plugin-node": "9.1.0",
"eslint-plugin-promise": "4.2.1", "eslint-plugin-promise": "4.2.1",
"eslint-plugin-standard": "4.0.0", "eslint-plugin-standard": "4.0.1",
"eslint-plugin-vue": "5.2.3", "eslint-plugin-vue": "5.2.3",
"fibers": "4.0.1", "fibers": "4.0.1",
"file-loader": "4.1.0", "file-loader": "4.2.0",
"filepond": "4.4.12", "filepond": "4.5.0",
"filepond-plugin-file-validate-type": "1.2.4", "filepond-plugin-file-validate-type": "1.2.4",
"filesize.js": "1.0.2", "filesize.js": "1.0.2",
"grapesjs": "0.14.62", "grapesjs": "0.15.3",
"graphiql": "0.13.2", "graphiql": "0.14.2",
"graphql-persisted-document-loader": "1.0.1", "graphql-persisted-document-loader": "1.0.1",
"graphql-tag": "^2.10.1", "graphql-tag": "^2.10.1",
"graphql-voyager": "1.0.0-rc.27", "graphql-voyager": "1.0.0-rc.27",
...@@ -234,10 +234,10 @@ ...@@ -234,10 +234,10 @@
"html-webpack-pug-plugin": "2.0.0", "html-webpack-pug-plugin": "2.0.0",
"i18next-chained-backend": "2.0.0", "i18next-chained-backend": "2.0.0",
"i18next-localstorage-backend": "3.0.0", "i18next-localstorage-backend": "3.0.0",
"i18next-xhr-backend": "3.0.1", "i18next-xhr-backend": "3.1.2",
"ignore-loader": "0.1.2", "ignore-loader": "0.1.2",
"jest": "24.8.0", "jest": "24.9.0",
"js-cookie": "2.2.0", "js-cookie": "2.2.1",
"mini-css-extract-plugin": "0.8.0", "mini-css-extract-plugin": "0.8.0",
"moment-duration-format": "2.3.2", "moment-duration-format": "2.3.2",
"offline-plugin": "5.0.7", "offline-plugin": "5.0.7",
...@@ -254,19 +254,19 @@ ...@@ -254,19 +254,19 @@
"pug-loader": "2.4.0", "pug-loader": "2.4.0",
"pug-plain-loader": "1.0.0", "pug-plain-loader": "1.0.0",
"raw-loader": "3.1.0", "raw-loader": "3.1.0",
"react": "16.8.6", "react": "16.9.0",
"react-dom": "16.8.6", "react-dom": "16.9.0",
"resolve-url-loader": "3.1.0", "resolve-url-loader": "3.1.0",
"sass": "1.22.9", "sass": "1.22.10",
"sass-loader": "7.1.0", "sass-loader": "7.3.1",
"sass-resources-loader": "2.0.1", "sass-resources-loader": "2.0.1",
"script-ext-html-webpack-plugin": "2.1.4", "script-ext-html-webpack-plugin": "2.1.4",
"simple-progress-webpack-plugin": "1.1.2", "simple-progress-webpack-plugin": "1.1.2",
"style-loader": "0.23.1", "style-loader": "1.0.0",
"terser": "4.1.3", "terser": "4.2.1",
"twemoji-awesome": "1.0.6", "twemoji-awesome": "1.0.6",
"url-loader": "2.1.0", "url-loader": "2.1.0",
"vee-validate": "2.2.13", "vee-validate": "2.2.15",
"velocity-animate": "1.5.2", "velocity-animate": "1.5.2",
"viz.js": "2.1.2", "viz.js": "2.1.2",
"vue": "2.6.10", "vue": "2.6.10",
...@@ -274,32 +274,32 @@ ...@@ -274,32 +274,32 @@
"vue-chartjs": "3.4.2", "vue-chartjs": "3.4.2",
"vue-clipboards": "1.3.0", "vue-clipboards": "1.3.0",
"vue-codemirror": "4.0.6", "vue-codemirror": "4.0.6",
"vue-filepond": "5.1.2", "vue-filepond": "5.1.3",
"vue-hot-reload-api": "2.3.3", "vue-hot-reload-api": "2.3.3",
"vue-loader": "15.7.1", "vue-loader": "15.7.1",
"vue-material-design-icons": "3.3.1", "vue-material-design-icons": "3.4.0",
"vue-moment": "4.0.0", "vue-moment": "4.0.0",
"vue-router": "3.0.7", "vue-router": "3.1.2",
"vue-simple-breakpoints": "1.0.3", "vue-simple-breakpoints": "1.0.3",
"vue-status-indicator": "1.2.1", "vue-status-indicator": "1.2.1",
"vue-template-compiler": "2.6.10", "vue-template-compiler": "2.6.10",
"vue-tour": "1.1.0", "vue-tour": "1.1.0",
"vue2-animate": "2.1.0", "vue2-animate": "2.1.2",
"vuedraggable": "2.23.0", "vuedraggable": "2.23.0",
"vuescroll": "4.13.1", "vuescroll": "4.14.0",
"vuetify": "2.0.4", "vuetify": "2.0.10",
"vuetify-loader": "1.3.0", "vuetify-loader": "1.3.0",
"vuex": "3.1.1", "vuex": "3.1.1",
"vuex-pathify": "1.2.4", "vuex-pathify": "1.2.4",
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"webpack": "4.39.1", "webpack": "4.39.2",
"webpack-bundle-analyzer": "3.4.1", "webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.6", "webpack-cli": "3.3.7",
"webpack-dev-middleware": "3.7.0", "webpack-dev-middleware": "3.7.0",
"webpack-hot-middleware": "2.25.0", "webpack-hot-middleware": "2.25.0",
"webpack-merge": "4.2.1", "webpack-merge": "4.2.1",
"webpack-subresource-integrity": "1.3.2", "webpack-subresource-integrity": "1.3.2",
"webpackbar": "3.2.0", "webpackbar": "4.0.0",
"whatwg-fetch": "3.0.0", "whatwg-fetch": "3.0.0",
"write-file-webpack-plugin": "4.5.1", "write-file-webpack-plugin": "4.5.1",
"xterm": "3.14.5", "xterm": "3.14.5",
...@@ -344,7 +344,10 @@ ...@@ -344,7 +344,10 @@
"requireSpaceAfterCodeOperator": true, "requireSpaceAfterCodeOperator": true,
"requireStrictEqualityOperators": true, "requireStrictEqualityOperators": true,
"validateAttributeQuoteMarks": "'", "validateAttributeQuoteMarks": "'",
"validateAttributeSeparator": { "separator": ", ", "multiLineSeparator": "\n " }, "validateAttributeSeparator": {
"separator": ", ",
"multiLineSeparator": "\n "
},
"validateDivTags": true, "validateDivTags": true,
"validateIndentation": 2, "validateIndentation": 2,
"excludeFiles": [ "excludeFiles": [
......
...@@ -42,6 +42,13 @@ defaults: ...@@ -42,6 +42,13 @@ defaults:
theme: 'default' theme: 'default'
iconset: 'md' iconset: 'md'
darkMode: false darkMode: false
security:
securityIframe: true
securityReferrerPolicy: true
securityHSTS: false
securityHSTSDuration: 300
securityCSP: false
securityCSPDirectives: ''
flags: flags:
ldapdebug: false ldapdebug: false
sqllog: false sqllog: false
......
...@@ -7,16 +7,16 @@ const graphHelper = require('../../helpers/graph') ...@@ -7,16 +7,16 @@ const graphHelper = require('../../helpers/graph')
module.exports = { module.exports = {
Query: { Query: {
async authentication() { return {} } async authentication () { return {} }
}, },
Mutation: { Mutation: {
async authentication() { return {} } async authentication () { return {} }
}, },
AuthenticationQuery: { AuthenticationQuery: {
/** /**
* Fetch active authentication strategies * Fetch active authentication strategies
*/ */
async strategies(obj, args, context, info) { async strategies (obj, args, context, info) {
let strategies = await WIKI.models.authentication.getStrategies(args.isEnabled) let strategies = await WIKI.models.authentication.getStrategies(args.isEnabled)
strategies = strategies.map(stg => { strategies = strategies.map(stg => {
const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {} const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {}
...@@ -44,7 +44,7 @@ module.exports = { ...@@ -44,7 +44,7 @@ module.exports = {
/** /**
* Perform Login * Perform Login
*/ */
async login(obj, args, context) { async login (obj, args, context) {
try { try {
const authResult = await WIKI.models.users.login(args, context) const authResult = await WIKI.models.users.login(args, context)
return { return {
...@@ -63,7 +63,7 @@ module.exports = { ...@@ -63,7 +63,7 @@ module.exports = {
/** /**
* Perform 2FA Login * Perform 2FA Login
*/ */
async loginTFA(obj, args, context) { async loginTFA (obj, args, context) {
try { try {
const authResult = await WIKI.models.users.loginTFA(args, context) const authResult = await WIKI.models.users.loginTFA(args, context)
return { return {
...@@ -75,9 +75,23 @@ module.exports = { ...@@ -75,9 +75,23 @@ module.exports = {
} }
}, },
/** /**
* Perform Mandatory Password Change after Login
*/
async loginChangePassword (obj, args, context) {
try {
const authResult = await WIKI.models.users.loginChangePassword(args, context)
return {
...authResult,
responseResult: graphHelper.generateSuccess('Password changed successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/**
* Register a new account * Register a new account
*/ */
async register(obj, args, context) { async register (obj, args, context) {
try { try {
await WIKI.models.users.register({ ...args, verify: true }, context) await WIKI.models.users.register({ ...args, verify: true }, context)
return { return {
...@@ -90,7 +104,7 @@ module.exports = { ...@@ -90,7 +104,7 @@ module.exports = {
/** /**
* Update Authentication Strategies * Update Authentication Strategies
*/ */
async updateStrategies(obj, args, context) { async updateStrategies (obj, args, context) {
try { try {
WIKI.config.auth = { WIKI.config.auth = {
audience: _.get(args, 'config.audience', WIKI.config.auth.audience), audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
...@@ -122,7 +136,7 @@ module.exports = { ...@@ -122,7 +136,7 @@ module.exports = {
/** /**
* Generate New Authentication Public / Private Key Certificates * Generate New Authentication Public / Private Key Certificates
*/ */
async regenerateCertificates(obj, args, context) { async regenerateCertificates (obj, args, context) {
try { try {
await WIKI.auth.regenerateCertificates() await WIKI.auth.regenerateCertificates()
return { return {
...@@ -135,7 +149,7 @@ module.exports = { ...@@ -135,7 +149,7 @@ module.exports = {
/** /**
* Reset Guest User * Reset Guest User
*/ */
async resetGuestUser(obj, args, context) { async resetGuestUser (obj, args, context) {
try { try {
await WIKI.auth.resetGuestUser() await WIKI.auth.resetGuestUser()
return { return {
......
...@@ -17,7 +17,8 @@ module.exports = { ...@@ -17,7 +17,8 @@ module.exports = {
company: WIKI.config.company, company: WIKI.config.company,
...WIKI.config.seo, ...WIKI.config.seo,
...WIKI.config.logo, ...WIKI.config.logo,
...WIKI.config.features ...WIKI.config.features,
...WIKI.config.security
} }
} }
}, },
...@@ -42,7 +43,15 @@ module.exports = { ...@@ -42,7 +43,15 @@ module.exports = {
featurePageComments: args.featurePageComments, featurePageComments: args.featurePageComments,
featurePersonalWikis: args.featurePersonalWikis featurePersonalWikis: args.featurePersonalWikis
} }
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'seo', 'logo', 'features']) WIKI.config.security = {
securityIframe: args.securityIframe,
securityReferrerPolicy: args.securityReferrerPolicy,
securityHSTS: args.securityHSTS,
securityHSTSDuration: args.securityHSTSDuration,
securityCSP: args.securityCSP,
securityCSPDirectives: args.securityCSPDirectives
}
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'seo', 'logo', 'features', 'security'])
return { return {
responseResult: graphHelper.generateSuccess('Site configuration updated successfully') responseResult: graphHelper.generateSuccess('Site configuration updated successfully')
......
...@@ -32,9 +32,14 @@ type AuthenticationMutation { ...@@ -32,9 +32,14 @@ type AuthenticationMutation {
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
loginTFA( loginTFA(
loginToken: String! continuationToken: String!
securityCode: String! securityCode: String!
): DefaultResponse @rateLimit(limit: 5, duration: 60) ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
loginChangePassword(
continuationToken: String!
newPassword: String!
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
register( register(
email: String! email: String!
...@@ -76,8 +81,9 @@ type AuthenticationStrategy { ...@@ -76,8 +81,9 @@ type AuthenticationStrategy {
type AuthenticationLoginResponse { type AuthenticationLoginResponse {
responseResult: ResponseStatus responseResult: ResponseStatus
jwt: String jwt: String
tfaRequired: Boolean mustChangePwd: Boolean
tfaLoginToken: String mustProvideTFA: Boolean
continuationToken: String
} }
type AuthenticationRegisterResponse { type AuthenticationRegisterResponse {
......
...@@ -36,6 +36,12 @@ type SiteMutation { ...@@ -36,6 +36,12 @@ type SiteMutation {
featurePageRatings: Boolean! featurePageRatings: Boolean!
featurePageComments: Boolean! featurePageComments: Boolean!
featurePersonalWikis: Boolean! featurePersonalWikis: Boolean!
securityIframe: Boolean!
securityReferrerPolicy: Boolean!
securityHSTS: Boolean!
securityHSTSDuration: Int!
securityCSP: Boolean!
securityCSPDirectives: String!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse @auth(requires: ["manage:system"])
} }
...@@ -56,4 +62,10 @@ type SiteConfig { ...@@ -56,4 +62,10 @@ type SiteConfig {
featurePageRatings: Boolean! featurePageRatings: Boolean!
featurePageComments: Boolean! featurePageComments: Boolean!
featurePersonalWikis: Boolean! featurePersonalWikis: Boolean!
securityIframe: Boolean!
securityReferrerPolicy: Boolean!
securityHSTS: Boolean!
securityHSTSDuration: Int!
securityCSP: Boolean!
securityCSPDirectives: String!
} }
...@@ -89,6 +89,8 @@ type User { ...@@ -89,6 +89,8 @@ type User {
providerKey: String! providerKey: String!
providerId: String providerId: String
isSystem: Boolean! isSystem: Boolean!
isActive: Boolean!
isVerified: Boolean!
location: String! location: String!
jobTitle: String! jobTitle: String!
timezone: String! timezone: String!
......
'use strict' /* global WIKI */
/** /**
* Security Middleware * Security Middleware
...@@ -13,7 +13,9 @@ module.exports = function (req, res, next) { ...@@ -13,7 +13,9 @@ module.exports = function (req, res, next) {
req.app.disable('x-powered-by') req.app.disable('x-powered-by')
// -> Disable Frame Embedding // -> Disable Frame Embedding
if (WIKI.config.securityIframe) {
res.set('X-Frame-Options', 'deny') res.set('X-Frame-Options', 'deny')
}
// -> Re-enable XSS Fitler if disabled // -> Re-enable XSS Fitler if disabled
res.set('X-XSS-Protection', '1; mode=block') res.set('X-XSS-Protection', '1; mode=block')
...@@ -25,7 +27,14 @@ module.exports = function (req, res, next) { ...@@ -25,7 +27,14 @@ module.exports = function (req, res, next) {
res.set('X-UA-Compatible', 'IE=edge') res.set('X-UA-Compatible', 'IE=edge')
// -> Disables referrer header when navigating to a different origin // -> Disables referrer header when navigating to a different origin
if (WIKI.config.securityReferrerPolicy) {
res.set('Referrer-Policy', 'same-origin') res.set('Referrer-Policy', 'same-origin')
}
// -> Enforce HSTS
if (WIKI.config.securityHSTS) {
res.set('Strict-Transport-Security', `max-age=${WIKI.config.securityHSTSDuration}; includeSubDomains`)
}
return next() return next()
} }
...@@ -45,7 +45,7 @@ module.exports = class UserKey extends Model { ...@@ -45,7 +45,7 @@ module.exports = class UserKey extends Model {
} }
static async generateToken ({ userId, kind }, context) { static async generateToken ({ userId, kind }, context) {
const token = await nanoid() const token = nanoid()
await WIKI.models.userKeys.query().insert({ await WIKI.models.userKeys.query().insert({
kind, kind,
token, token,
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
const bcrypt = require('bcryptjs-then') const bcrypt = require('bcryptjs-then')
const _ = require('lodash') const _ = require('lodash')
const tfa = require('node-2fa') const tfa = require('node-2fa')
const securityHelper = require('../helpers/security')
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
const Model = require('objection').Model const Model = require('objection').Model
const validate = require('validate.js') const validate = require('validate.js')
...@@ -280,30 +279,46 @@ module.exports = class User extends Model { ...@@ -280,30 +279,46 @@ module.exports = class User extends Model {
if (err) { return reject(err) } if (err) { return reject(err) }
if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) } if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
// Must Change Password?
if (user.mustChangePwd) {
try {
const pwdChangeToken = await WIKI.models.userKeys.generateToken({
kind: 'changePwd',
userId: user.id
})
return resolve({
mustChangePwd: true,
continuationToken: pwdChangeToken
})
} catch (err) {
WIKI.logger.warn(err)
return reject(new WIKI.Error.AuthGenericError())
}
}
// Is 2FA required? // Is 2FA required?
if (user.tfaIsActive) { if (user.tfaIsActive) {
try { try {
let loginToken = await securityHelper.generateToken(32) const tfaToken = await WIKI.models.userKeys.generateToken({
await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600) kind: 'tfa',
userId: user.id
})
return resolve({ return resolve({
tfaRequired: true, tfaRequired: true,
tfaLoginToken: loginToken continuationToken: tfaToken
}) })
} catch (err) { } catch (err) {
WIKI.logger.warn(err) WIKI.logger.warn(err)
return reject(new WIKI.Error.AuthGenericError()) return reject(new WIKI.Error.AuthGenericError())
} }
} else { }
// No 2FA, log in user
return context.req.logIn(user, { session: !strInfo.useForm }, async err => { context.req.logIn(user, { session: !strInfo.useForm }, async err => {
if (err) { return reject(err) } if (err) { return reject(err) }
const jwtToken = await WIKI.models.users.refreshToken(user) const jwtToken = await WIKI.models.users.refreshToken(user)
resolve({ resolve({ jwt: jwtToken.token })
jwt: jwtToken.token,
tfaRequired: false
})
}) })
}
})(context.req, context.res, () => {}) })(context.req, context.res, () => {})
}) })
} else { } else {
...@@ -348,7 +363,7 @@ module.exports = class User extends Model { ...@@ -348,7 +363,7 @@ module.exports = class User extends Model {
} }
} }
static async loginTFA(opts, context) { static async loginTFA (opts, context) {
if (opts.securityCode.length === 6 && opts.loginToken.length === 64) { if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
let result = await WIKI.redis.get(`tfa:${opts.loginToken}`) let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
if (result) { if (result) {
...@@ -375,6 +390,36 @@ module.exports = class User extends Model { ...@@ -375,6 +390,36 @@ module.exports = class User extends Model {
} }
/** /**
* Change Password from a Mandatory Password Change after Login
*/
static async loginChangePassword ({ continuationToken, newPassword }, context) {
if (!newPassword || newPassword.length < 6) {
throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
}
const usr = await WIKI.models.userKeys.validateToken({
kind: 'changePwd',
token: continuationToken
})
if (usr) {
await WIKI.models.users.query().patch({
password: newPassword,
mustChangePwd: false
}).findById(usr.id)
return new Promise((resolve, reject) => {
context.req.logIn(usr, { session: false }, async err => {
if (err) { return reject(err) }
const jwtToken = await WIKI.models.users.refreshToken(usr)
resolve({ jwt: jwtToken.token })
})
})
} else {
throw new WIKI.Error.UserNotFound()
}
}
/**
* Create a new user * Create a new user
* *
* @param {Object} param0 User Fields * @param {Object} param0 User Fields
...@@ -520,7 +565,7 @@ module.exports = class User extends Model { ...@@ -520,7 +565,7 @@ module.exports = class User extends Model {
} }
usrData.password = newPassword usrData.password = newPassword
} }
if (!_.isEmpty(groups)) { if (_.isArray(groups)) {
const usrGroupsRaw = await usr.$relatedQuery('groups') const usrGroupsRaw = await usr.$relatedQuery('groups')
const usrGroups = _.map(usrGroupsRaw, 'id') const usrGroups = _.map(usrGroupsRaw, 'id')
// Relate added groups // Relate added groups
......
...@@ -3,7 +3,7 @@ title: Local ...@@ -3,7 +3,7 @@ title: Local
description: Built-in authentication for Wiki.js description: Built-in authentication for Wiki.js
author: requarks.io author: requarks.io
logo: https://static.requarks.io/logo/wikijs.svg logo: https://static.requarks.io/logo/wikijs.svg
color: yellow darken-3 color: primary
website: https://wiki.js.org website: https://wiki.js.org
isAvailable: true isAvailable: true
useForm: true useForm: true
......
...@@ -2,25 +2,11 @@ extends master.pug ...@@ -2,25 +2,11 @@ extends master.pug
block body block body
#root.is-fullscreen #root.is-fullscreen
v-app(dark)
.app-error .app-error
v-container a(href='/')
.pt-5 img(src='/svg/logo-wikijs.svg')
v-layout(row) strong Oops, something went wrong...
v-flex(xs10) span= message
a(href='/'): img(src='/svg/logo-wikijs.svg')
v-flex.text-right(xs2)
v-btn(href='/', depressed, color='red darken-3')
v-icon(left) home
span Home
v-alert(color='grey', outline, :value='true', icon='error')
strong.red--text.text--lighten-3 Oops, something went wrong...
.body-1.red--text.text--lighten-2= message
if error.stack if error.stack
v-expansion-panel.mt-5
v-expansion-panel-content.red.darken-3(:value='true')
div(slot='header') View Debug Trace
v-card(color='grey darken-4')
v-card-text
pre: code #{error.stack} pre: code #{error.stack}
...@@ -32,7 +32,7 @@ html ...@@ -32,7 +32,7 @@ html
link( link(
type='text/css' type='text/css'
rel='stylesheet' rel='stylesheet'
href='https://use.fontawesome.com/releases/v5.9.0/css/all.css' href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
) )
else if config.theming.iconset === 'fa4' else if config.theming.iconset === 'fa4'
link( link(
......
...@@ -36,7 +36,7 @@ html(lang=siteConfig.lang) ...@@ -36,7 +36,7 @@ html(lang=siteConfig.lang)
link( link(
type='text/css' type='text/css'
rel='stylesheet' rel='stylesheet'
href='https://use.fontawesome.com/releases/v5.9.0/css/all.css' href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
) )
else if config.theming.iconset === 'fa4' else if config.theming.iconset === 'fa4'
link( link(
......
This diff was suppressed by a .gitattributes entry.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment