feat: mandatory password change on login + UI fixes

pull/1004/head
Nick 5 years ago
parent 38008f0460
commit d3e693ab46

@ -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 @@
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 @@
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 @@
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 @@
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 @@
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 {
{ 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 {
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 {
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,26 +23,21 @@
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') v-menu(bottom, right, min-width='200')
td.text-xs-right {{ props.item.id }} template(v-slot:activator='{ on }')
td {{ props.item.name }} v-btn(icon, v-on='on', small)
td {{ props.item.email }} v-icon.grey--text.text--darken-1 mdi-dots-horizontal
td v-list(dense, nav)
v-menu(bottom, right, min-width='200') v-list-item(:to='`/users/` + item.id')
template(v-slot:activator='{ on }') v-list-item-action: v-icon(color='primary') mdi-account-outline
v-btn(icon, v-on='on', small) v-list-item-content
v-icon.grey--text.text--darken-1 mdi-dots-horizontal v-list-item-title View User Profile
v-list(dense, nav) template(v-if='item.id !== 2')
v-list-item(:to='`/users/` + props.item.id') v-list-item(@click='unassignUser(item.id)')
v-list-item-action: v-icon(color='primary') mdi-account-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 View User Profile v-list-item-title Unassign
template(v-if='props.item.id !== 2')
v-list-item(@click='unassignUser(props.item.id)')
v-list-item-action: v-icon(color='orange') mdi-account-remove-outline
v-list-item-content
v-list-item-title Unassign
template(slot='no-data') template(slot='no-data')
v-alert.ma-3(icon='warning', outlined) No users to display. v-alert.ma-3(icon='warning', outlined) No users to display.
.text-center.py-2(v-if='group.users.length > 15') .text-center.py-2(v-if='group.users.length > 15')
@ -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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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

@ -3,12 +3,26 @@
v-layout(row, wrap) v-layout(row, wrap)
v-flex(xs12) v-flex(xs12)
.admin-header .admin-header
img.animated.fadeInUp(src='/svg/icon-male-user.svg', alt='Edit User', style='width: 80px;') img.animated.fadeInUp(src='/svg/icon-male-user.svg', :alt='$t(`admin:users.edit`)', style='width: 80px;')
.admin-header-title .admin-header-title
.headline.blue--text.text--darken-2.animated.fadeInLeft Edit User .headline.blue--text.text--darken-2.animated.fadeInLeft {{$t('admin:users.edit')}}
.subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{user.name}} .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{user.name}}
v-spacer v-spacer
.caption.grey--text.animated.fadeInRight.wait-p5s ID #[strong {{user.id}}] template(v-if='user.isActive')
status-indicator.mr-3(positive, pulse)
.caption.green--text {{$t('admin:users.active')}}
template(v-else)
status-indicator.mr-3(negative, pulse)
.caption.red--text {{$t('admin:users.inactive')}}
template(v-if='user.isVerified')
status-indicator.mr-3.ml-4(active, pulse)
.caption.blue--text {{$t('admin:users.verified')}}
template(v-else)
status-indicator.mr-3.ml-4(intermediary, pulse)
.caption.deep-orange--text {{$t('admin:users.unverified')}}
v-spacer
i18next.caption.grey--text.animated.fadeInRight.wait-p5s(path='admin:users.id', tag='div')
strong(place='id') {{user.id}}
v-divider.animated.fadeInRight.wait-p3s.ml-3(vertical) v-divider.animated.fadeInRight.wait-p3s.ml-3(vertical)
v-btn.ml-3.animated.fadeInDown.wait-p2s(color='grey', large, outlined, to='/users') v-btn.ml-3.animated.fadeInDown.wait-p2s(color='grey', large, outlined, to='/users')
v-icon mdi-arrow-left v-icon mdi-arrow-left
@ -30,15 +44,15 @@
v-card.animated.fadeInUp v-card.animated.fadeInUp
v-toolbar(color='primary', dense, dark, flat) v-toolbar(color='primary', dense, dark, flat)
v-icon.mr-2 mdi-information-variant v-icon.mr-2 mdi-information-variant
span Basic Info span {{$t('admin:users.basicInfo')}}
v-list.py-0(two-line, dense) v-list.py-0(two-line, dense)
v-list-item v-list-item
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-email-variant v-icon mdi-email-variant
v-list-item-content v-list-item-content
v-list-item-title Email v-list-item-title {{$t('admin:users.email')}}
v-list-item-subtitle {{ user.email }} v-list-item-subtitle {{ user.email }}
v-list-item-action(v-if='!user.isSystem') v-list-item-action(v-if='!user.isSystem && user.providerKey === `local`')
v-menu( v-menu(
v-model='editPop.email' v-model='editPop.email'
:close-on-content-click='false' :close-on-content-click='false'
@ -52,7 +66,7 @@
v-text-field( v-text-field(
ref='iptEmail' ref='iptEmail'
v-model='user.email' v-model='user.email'
label='Email' :label='$t(`admin:users.email`)'
solo solo
hide-details hide-details
append-icon='mdi-check' append-icon='mdi-check'
@ -66,7 +80,7 @@
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-account v-icon mdi-account
v-list-item-content v-list-item-content
v-list-item-title Display Name v-list-item-title {{$t('admin:users.displayName')}}
v-list-item-subtitle {{ user.name }} v-list-item-subtitle {{ user.name }}
v-list-item-action v-list-item-action
v-menu( v-menu(
@ -82,7 +96,7 @@
v-text-field( v-text-field(
ref='iptDisplayName' ref='iptDisplayName'
v-model='user.name' v-model='user.name'
label='Display Name' :label='$t(`admin:users.displayName`)'
solo solo
hide-details hide-details
append-icon='mdi-check' append-icon='mdi-check'
@ -94,13 +108,13 @@
v-card.mt-3.animated.fadeInUp.wait-p2s(v-if='!user.isSystem') v-card.mt-3.animated.fadeInUp.wait-p2s(v-if='!user.isSystem')
v-toolbar(color='primary', dense, dark, flat) v-toolbar(color='primary', dense, dark, flat)
v-icon.mr-2 mdi-lock-outline v-icon.mr-2 mdi-lock-outline
span Authentication span {{$t('admin:users.authentication')}}
v-list.py-0(two-line, dense) v-list.py-0(two-line, dense)
v-list-item v-list-item
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-domain v-icon mdi-domain
v-list-item-content v-list-item-content
v-list-item-title Provider v-list-item-title {{$t('admin:users.authProvider')}}
v-list-item-subtitle {{ user.providerKey }} v-list-item-subtitle {{ user.providerKey }}
//- v-list-item-action //- v-list-item-action
//- v-img(src='https://static.requarks.io/logo/wikijs.svg', alt='', contain, max-height='32', position='center right') //- v-img(src='https://static.requarks.io/logo/wikijs.svg', alt='', contain, max-height='32', position='center right')
@ -110,7 +124,7 @@
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-textbox-password v-icon mdi-textbox-password
v-list-item-content v-list-item-content
v-list-item-title Password v-list-item-title {{$t('admin:users.password')}}
v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull; v-list-item-subtitle &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
v-list-item-action v-list-item-action
v-menu( v-menu(
@ -124,12 +138,12 @@
template(v-slot:activator='{ on: tooltip }') template(v-slot:activator='{ on: tooltip }')
v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)') v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)')
v-icon mdi-cached v-icon mdi-cached
span Change Password span {{$t('admin:users.changePassword')}}
v-card v-card
v-text-field( v-text-field(
ref='iptNewPassword' ref='iptNewPassword'
v-model='newPassword' v-model='newPassword'
label='New Password' :label='$t(`admin:users.newPassword`)'
solo solo
hide-details hide-details
append-icon='mdi-check' append-icon='mdi-check'
@ -149,26 +163,26 @@
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-two-factor-authentication v-icon mdi-two-factor-authentication
v-list-item-content v-list-item-content
v-list-item-title Two Factor Authentication (2FA) v-list-item-title {{$t('admin:users.tfa')}}
v-list-item-subtitle.red--text Inactive v-list-item-subtitle.red--text Inactive
v-list-item-action v-list-item-action
v-tooltip(top) v-tooltip(top)
template(v-slot:activator='{ on }') template(v-slot:activator='{ on }')
v-btn(icon, color='grey', x-small, v-on='on', disabled) v-btn(icon, color='grey', x-small, v-on='on', disabled)
v-icon mdi-power v-icon mdi-power
span Toggle 2FA span {{$t('admin:users.toggle2FA')}}
template(v-if='user.providerId') template(v-if='user.providerId')
v-divider v-divider
v-list-item v-list-item
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-account v-icon mdi-music-accidental-sharp
v-list-item-content v-list-item-content
v-list-item-title Provider Id v-list-item-title {{$t('admin:users.authProviderId')}}
v-list-item-subtitle {{ user.providerId }} v-list-item-subtitle {{ user.providerId }}
v-card.mt-3.animated.fadeInUp.wait-p4s v-card.mt-3.animated.fadeInUp.wait-p4s
v-toolbar(color='primary', dense, dark, flat) v-toolbar(color='primary', dense, dark, flat)
v-icon.mr-2 mdi-account-group v-icon.mr-2 mdi-account-group
span User Groups span {{$t('admin:users.groups')}}
v-list(dense) v-list(dense)
template(v-for='(group, idx) in user.groups') template(v-for='(group, idx) in user.groups')
v-list-item(:key='`group-` + group.id') v-list-item(:key='`group-` + group.id')
@ -181,14 +195,14 @@
v-icon mdi-close v-icon mdi-close
v-divider(v-if='idx < user.groups.length - 1') v-divider(v-if='idx < user.groups.length - 1')
v-alert.mx-3(v-if='user.groups.length < 1', outlined, color='grey darken-1', icon='mdi-alert') v-alert.mx-3(v-if='user.groups.length < 1', outlined, color='grey darken-1', icon='mdi-alert')
.caption This user is not assigned to any group yet. You must assign at least 1 group to a user. .caption {{$t('admin:users.noGroupAssigned')}}
v-card-chin(v-if='!user.isSystem') v-card-chin(v-if='!user.isSystem')
v-spacer v-spacer
v-select( v-select(
ref='iptAssignGroup' ref='iptAssignGroup'
:items='groups' :items='groups'
v-model='newGroup' v-model='newGroup'
label='Select Group...' :label='$t(`admin:users.selectGroup`)'
item-value='id' item-value='id'
item-text='name' item-text='name'
item-disabled='isSystem' item-disabled='isSystem'
@ -201,18 +215,18 @@
) )
v-btn.ml-2.px-4(depressed, color='primary', height='48', @click='assignGroup', :disabled='newGroup === 0') v-btn.ml-2.px-4(depressed, color='primary', height='48', @click='assignGroup', :disabled='newGroup === 0')
v-icon(left) mdi-clipboard-account-outline v-icon(left) mdi-clipboard-account-outline
span Assign span {{$t('admin:users.groupAssign')}}
v-flex(xs6) v-flex(xs6)
v-card.animated.fadeInUp.wait-p2s v-card.animated.fadeInUp.wait-p2s
v-toolbar(color='primary', dense, dark, flat) v-toolbar(color='primary', dense, dark, flat)
v-icon.mr-2 mdi-account-badge-outline v-icon.mr-2 mdi-account-badge-outline
span Extended Metadata span {{$t('admin:users.extendedMetadata')}}
v-list.py-0(two-line, dense) v-list.py-0(two-line, dense)
v-list-item v-list-item
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-map-marker v-icon mdi-map-marker
v-list-item-content v-list-item-content
v-list-item-title Location v-list-item-title {{$t('admin:users.location')}}
v-list-item-subtitle {{ user.location }} v-list-item-subtitle {{ user.location }}
v-list-item-action v-list-item-action
v-menu( v-menu(
@ -228,7 +242,7 @@
v-text-field( v-text-field(
ref='iptLocation' ref='iptLocation'
v-model='user.location' v-model='user.location'
label='Location' :label='$t(`admin:users.location`)'
solo solo
hide-details hide-details
append-icon='mdi-check' append-icon='mdi-check'
@ -241,7 +255,7 @@
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-account-badge-horizontal-outline v-icon mdi-account-badge-horizontal-outline
v-list-item-content v-list-item-content
v-list-item-title Job Title v-list-item-title {{$t('admin:users.jobTitle')}}
v-list-item-subtitle {{ user.jobTitle }} v-list-item-subtitle {{ user.jobTitle }}
v-list-item-action v-list-item-action
v-menu( v-menu(
@ -257,7 +271,7 @@
v-text-field( v-text-field(
ref='iptJobTitle' ref='iptJobTitle'
v-model='user.jobTitle' v-model='user.jobTitle'
label='Job Title' :label='$t(`admin:users.jobTitle`)'
solo solo
hide-details hide-details
append-icon='mdi-check' append-icon='mdi-check'
@ -270,7 +284,7 @@
v-list-item-avatar(size='32') v-list-item-avatar(size='32')
v-icon mdi-map-clock-outline v-icon mdi-map-clock-outline
v-list-item-content v-list-item-content
v-list-item-title Timezone v-list-item-title {{$t('admin:users.timezone')}}
v-list-item-subtitle {{ user.timezone }} v-list-item-subtitle {{ user.timezone }}
v-list-item-action v-list-item-action
v-menu( v-menu(
@ -287,7 +301,7 @@
ref='iptTimezone' ref='iptTimezone'
:items='timezones' :items='timezones'
v-model='user.timezone' v-model='user.timezone'
label='Timezone' :label='$t(`admin:users.timezone`)'
solo solo
dense dense
hide-details hide-details
@ -308,11 +322,16 @@
import _ from 'lodash' import _ from 'lodash'
import { get } from 'vuex-pathify' import { get } from 'vuex-pathify'
import { StatusIndicator } from 'vue-status-indicator'
import userQuery from 'gql/admin/users/users-query-single.gql' import userQuery from 'gql/admin/users/users-query-single.gql'
import groupsQuery from 'gql/admin/users/users-query-groups.gql' import groupsQuery from 'gql/admin/users/users-query-groups.gql'
import updateUserMutation from 'gql/admin/users/users-mutation-update.gql' import updateUserMutation from 'gql/admin/users/users-mutation-update.gql'
export default { export default {
components: {
StatusIndicator
},
data() { data() {
return { return {
deleteUserDialog: false, deleteUserDialog: false,
@ -334,7 +353,9 @@ export default {
location: '', location: '',
jobTitle: '', jobTitle: '',
timezone: '', timezone: '',
groups: [] groups: [],
isActive: false,
isVerified: false
}, },
timezones: [ timezones: [
{ text: '(GMT-11:00) Niue', value: 'Pacific/Niue' }, { text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
@ -613,7 +634,7 @@ export default {
if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) { if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) {
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
style: 'success', style: 'success',
message: 'User updated successfully.', message: this.$t('admin:users.userUpdateSuccess'),
icon: 'check' icon: 'check'
}) })
this.$router.push('/users') this.$router.push('/users')
@ -636,7 +657,7 @@ export default {
assignGroup() { assignGroup() {
if (_.some(this.user.groups, ['id', this.newGroup])) { if (_.some(this.user.groups, ['id', this.newGroup])) {
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
message: 'User is already assigned to this group!', message: this.$t('admin:users.userAlreadyAssignedToGroup'),
style: 'error', style: 'error',
icon: 'alert' icon: 'alert'
}) })

@ -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 @@
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 @@
: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'
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,17 +11,18 @@
offset-xl4, xl4 offset-xl4, xl4
) )
transition(name='fadeUp') transition(name='fadeUp')
v-card.elevation-5.md2(v-show='isShown') v-card.elevation-5(v-show='isShown')
v-toolbar(color='primary', flat, dense, dark) v-toolbar(color='primary', flat, dense, dark)
v-spacer v-spacer
.subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }} .subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
.subheading(v-if='screen === "changePwd"') {{ $t('auth:changePwd.subtitle') }}
.subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title, interpolation: { escapeValue: false } }) }} .subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title, interpolation: { escapeValue: false } }) }}
.subheading(v-else) {{ $t('auth:loginRequired') }} .subheading(v-else) {{ $t('auth:loginRequired') }}
v-spacer v-spacer
v-card-text.text-center v-card-text.text-center
h1.display-1.primary--text.py-2 {{ siteTitle }} h1.display-1.primary--text.py-2 {{ siteTitle }}
template(v-if='screen === "login"') template(v-if='screen === "login"')
v-text-field.md2.mt-3( v-text-field.mt-3(
solo solo
flat flat
prepend-icon='mdi-clipboard-account' prepend-icon='mdi-clipboard-account'
@ -31,7 +32,7 @@
v-model='username' v-model='username'
:placeholder='$t("auth:fields.emailUser")' :placeholder='$t("auth:fields.emailUser")'
) )
v-text-field.md2.mt-2( v-text-field.mt-2(
solo solo
flat flat
prepend-icon='mdi-textbox-password' prepend-icon='mdi-textbox-password'
@ -47,7 +48,7 @@
) )
template(v-else-if='screen === "tfa"') template(v-else-if='screen === "tfa"')
.body-2 Enter the security code generated from your trusted device: .body-2 Enter the security code generated from your trusted device:
v-text-field.md2.centered.mt-2( v-text-field.centered.mt-2(
solo solo
flat flat
background-color='grey lighten-4' background-color='grey lighten-4'
@ -57,12 +58,34 @@
:placeholder='$t("auth:tfa.placeholder")' :placeholder='$t("auth:tfa.placeholder")'
@keyup.enter='verifySecurityCode' @keyup.enter='verifySecurityCode'
) )
template(v-else-if='screen === "changePwd"')
.body-2 {{$t('auth:changePwd.instructions')}}
v-text-field.mt-2(
type='password'
solo
flat
background-color='grey lighten-4'
hide-details
ref='iptNewPassword'
v-model='newPassword'
:placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'
)
v-text-field.mt-2(
type='password'
solo
flat
background-color='grey lighten-4'
hide-details
v-model='newPasswordVerify'
:placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'
@keyup.enter='changePassword'
)
template(v-else-if='screen === "forgot"') template(v-else-if='screen === "forgot"')
.body-2 {{ $t('auth:forgotPasswordSubtitle') }} .body-2 {{ $t('auth:forgotPasswordSubtitle') }}
v-text-field.md2.mt-3( v-text-field.mt-3(
solo solo
flat flat
prepend-icon='email' prepend-icon='mdi-email'
background-color='grey lighten-4' background-color='grey lighten-4'
hide-details hide-details
ref='iptEmailForgot' ref='iptEmailForgot'
@ -71,31 +94,48 @@
) )
v-card-actions.pb-4 v-card-actions.pb-4
v-spacer v-spacer
v-btn.md2( v-btn(
width='100%'
max-width='250px'
v-if='screen === "login"' v-if='screen === "login"'
block
large large
color='primary' color='teal'
dark
@click='login' @click='login'
round rounded
:loading='isLoading' :loading='isLoading'
) {{ $t('auth:actions.login') }} ) {{ $t('auth:actions.login') }}
v-btn.md2( v-btn(
width='100%'
max-width='250px'
v-else-if='screen === "tfa"' v-else-if='screen === "tfa"'
block
large large
color='primary' color='teal'
dark
@click='verifySecurityCode' @click='verifySecurityCode'
round rounded
:loading='isLoading' :loading='isLoading'
) {{ $t('auth:tfa.verifyToken') }} ) {{ $t('auth:tfa.verifyToken') }}
v-btn.md2( v-btn(
width='100%'
max-width='250px'
v-else-if='screen === "changePwd"'
large
color='teal'
dark
@click='changePassword'
rounded
:loading='isLoading'
) {{ $t('auth:changePwd.proceed') }}
v-btn(
width='100%'
max-width='250px'
v-else-if='screen === "forgot"' v-else-if='screen === "forgot"'
block
large large
color='primary' color='teal'
dark
@click='forgotPasswordSubmit' @click='forgotPasswordSubmit'
round rounded
:loading='isLoading' :loading='isLoading'
) {{ $t('auth:sendResetPassword') }} ) {{ $t('auth:sendResetPassword') }}
v-spacer v-spacer
@ -111,15 +151,16 @@
v-divider v-divider
v-card-text.grey.lighten-4.text-center v-card-text.grey.lighten-4.text-center
.pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }} .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
v-tooltip(top, v-for='strategy in strategies', :key='strategy.key') v-btn.mx-1.social-login-btn(
.social-login-btn.mr-2( v-for='strategy in strategies', :key='strategy.key'
slot='activator' large
v-ripple @click='selectStrategy(strategy)'
v-html='strategy.icon' dark
:class='strategy.color + " elevation-" + (strategy.key === selectedStrategy.key ? "0" : "4")' :color='strategy.color'
@click='selectStrategy(strategy)' :depressed='strategy.key === selectedStrategy.key'
) )
span {{ strategy.title }} v-avatar.mr-3(tile, :class='strategy.color', size='24', v-html='strategy.icon')
span(style='text-transform: none;') {{ strategy.title }}
template(v-if='screen === "login" && selectedStrategy.selfRegistration') template(v-if='screen === "login" && selectedStrategy.selfRegistration')
v-divider v-divider
v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"') v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
@ -142,6 +183,7 @@ import Cookies from 'js-cookie'
import strategiesQuery from 'gql/login/login-query-strategies.gql' import strategiesQuery from 'gql/login/login-query-strategies.gql'
import loginMutation from 'gql/login/login-mutation-login.gql' import loginMutation from 'gql/login/login-mutation-login.gql'
import tfaMutation from 'gql/login/login-mutation-tfa.gql' import tfaMutation from 'gql/login/login-mutation-tfa.gql'
import changePasswordMutation from 'gql/login/login-mutation-changepassword.gql'
export default { export default {
i18nOptions: { namespaces: 'auth' }, i18nOptions: { namespaces: 'auth' },
@ -155,11 +197,13 @@ export default {
password: '', password: '',
hidePassword: true, hidePassword: true,
securityCode: '', securityCode: '',
loginToken: '', continuationToken: '',
isLoading: false, isLoading: false,
loaderColor: 'grey darken-4', loaderColor: 'grey darken-4',
loaderTitle: 'Working...', loaderTitle: 'Working...',
isShown: false isShown: false,
newPassword: '',
newPasswordVerify: ''
} }
}, },
computed: { computed: {
@ -205,14 +249,14 @@ export default {
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
style: 'red', style: 'red',
message: this.$t('auth:invalidEmailUsername'), message: this.$t('auth:invalidEmailUsername'),
icon: 'warning' icon: 'alert'
}) })
this.$refs.iptEmail.focus() this.$refs.iptEmail.focus()
} else if (this.password.length < 2) { } else if (this.password.length < 2) {
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
style: 'red', style: 'red',
message: this.$t('auth:invalidPassword'), message: this.$t('auth:invalidPassword'),
icon: 'warning' icon: 'alert'
}) })
this.$refs.iptPassword.focus() this.$refs.iptPassword.focus()
} else { } else {
@ -231,10 +275,16 @@ export default {
if (_.has(resp, 'data.authentication.login')) { if (_.has(resp, 'data.authentication.login')) {
let respObj = _.get(resp, 'data.authentication.login', {}) let respObj = _.get(resp, 'data.authentication.login', {})
if (respObj.responseResult.succeeded === true) { if (respObj.responseResult.succeeded === true) {
if (respObj.tfaRequired === true) { this.continuationToken = respObj.continuationToken
if (respObj.mustChangePwd === true) {
this.screen = 'changePwd'
this.$nextTick(() => {
this.$refs.iptNewPassword.focus()
})
this.isLoading = false
} else if (respObj.mustProvideTFA === true) {
this.screen = 'tfa' this.screen = 'tfa'
this.securityCode = '' this.securityCode = ''
this.loginToken = respObj.tfaLoginToken
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.iptTFA.focus() this.$refs.iptTFA.focus()
}) })
@ -258,7 +308,7 @@ export default {
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
style: 'red', style: 'red',
message: err.message, message: err.message,
icon: 'warning' icon: 'alert'
}) })
this.isLoading = false this.isLoading = false
} }
@ -280,7 +330,7 @@ export default {
this.$apollo.mutate({ this.$apollo.mutate({
mutation: tfaMutation, mutation: tfaMutation,
variables: { variables: {
loginToken: this.loginToken, continuationToken: this.continuationToken,
securityCode: this.securityCode securityCode: this.securityCode
} }
}).then(resp => { }).then(resp => {
@ -307,23 +357,59 @@ export default {
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
style: 'red', style: 'red',
message: err.message, message: err.message,
icon: 'warning' icon: 'alert'
}) })
this.isLoading = false this.isLoading = false
}) })
} }
}, },
forgotPassword() { /**
* CHANGE PASSWORD
*/
async changePassword () {
this.loaderColor = 'grey darken-4'
this.loaderTitle = this.$t('auth:changePwd.loading')
this.isLoading = true
const resp = await this.$apollo.mutate({
mutation: changePasswordMutation,
variables: {
continuationToken: this.continuationToken,
newPassword: this.newPassword
}
})
if (_.get(resp, 'data.authentication.loginChangePassword.responseResult.succeeded', false) === true) {
this.loaderColor = 'green darken-1'
this.loaderTitle = this.$t('auth:loginSuccess')
Cookies.set('jwt', _.get(resp, 'data.authentication.loginChangePassword.jwt', ''), { expires: 365 })
_.delay(() => {
window.location.replace('/') // TEMPORARY - USE RETURNURL
}, 1000)
} else {
this.$store.commit('showNotification', {
style: 'red',
message: _.get(resp, 'data.authentication.loginChangePassword.responseResult.message', false),
icon: 'alert'
})
this.isLoading = false
}
},
/**
* SWITCH TO FORGOT PASSWORD SCREEN
*/
forgotPassword () {
this.screen = 'forgot' this.screen = 'forgot'
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.iptEmailForgot.focus() this.$refs.iptEmailForgot.focus()
}) })
}, },
async forgotPasswordSubmit() { /**
* FORGOT PASSWORD SUBMIT
*/
async forgotPasswordSubmit () {
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
style: 'pink', style: 'pink',
message: 'Coming soon!', message: 'Coming soon!',
icon: 'free_breakfast' icon: 'ferry'
}) })
} }
}, },
@ -378,18 +464,12 @@ export default {
} }
.social-login-btn { .social-login-btn {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 54px;
height: 54px;
cursor: pointer; cursor: pointer;
transition: opacity .2s ease; transition: opacity .2s ease;
&:hover { &:hover {
opacity: .8; opacity: .8;
} }
margin: .5rem 0; margin: .25rem 0;
svg { svg {
width: 24px; width: 24px;
height: 24px; height: 24px;

@ -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 (
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 @@
featurePageRatings featurePageRatings
featurePageComments featurePageComments
featurePersonalWikis featurePersonalWikis
securityIframe
securityReferrerPolicy
securityHSTS
securityHSTSDuration
securityCSP
securityCSPDirectives
} }
} }
} }

@ -10,6 +10,8 @@ query ($id: Int!) {
jobTitle jobTitle
timezone timezone
isSystem isSystem
isActive
isVerified
createdAt createdAt
updatedAt updatedAt
groups { groups {

@ -0,0 +1,13 @@
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!) {
message message
} }
jwt jwt
tfaRequired mustChangePwd
tfaLoginToken mustProvideTFA
continuationToken
} }
} }
} }

@ -1,12 +1,13 @@
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 @@
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 @@
} }
} }
code { > strong {
color: mc('grey', '500'); font-size: 1.5rem;
font-size: .8rem; }
> span {
margin-top: 1rem;
}
> pre {
margin-top: 2rem;
code {
color: mc('grey', '500');
font-size: .8rem;
}
} }
} }

@ -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(
@ -96,6 +83,19 @@
v-icon(color='teal', left, small) mdi-label v-icon(color='teal', left, small) mdi-label
span.teal--text.text--darken-2 {{tag.text}} span.teal--text.text--darken-2 {{tag.text}}
v-card.mt-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 v-card.mt-5
.pa-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
@ -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
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)
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 @@
}, },
"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 @@
"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 @@
"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 @@
"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 @@
"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 @@
"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 @@
"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 @@
"@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 @@
"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 @@
"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 @@
"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 @@
"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 @@
"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 @@
"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:
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')
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 = {
/** /**
* 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 = {
/** /**
* 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 {
@ -74,10 +74,24 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
/**
* 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 = {
/** /**
* 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 = {
/** /**
* 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 = {
/** /**
* 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 = {
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 = {
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 {
): 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 {
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 {
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 {
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 {
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!

@ -1,4 +1,4 @@
'use strict' /* global WIKI */
/** /**
* Security Middleware * Security Middleware
@ -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
res.set('X-Frame-Options', 'deny') if (WIKI.config.securityIframe) {
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) {
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
res.set('Referrer-Policy', 'same-origin') if (WIKI.config.securityReferrerPolicy) {
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 {
} }
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 @@
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 {
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 => {
if (err) { return reject(err) }
const jwtToken = await WIKI.models.users.refreshToken(user)
resolve({
jwt: jwtToken.token,
tfaRequired: false
})
})
} }
context.req.logIn(user, { session: !strInfo.useForm }, async err => {
if (err) { return reject(err) }
const jwtToken = await WIKI.models.users.refreshToken(user)
resolve({ jwt: jwtToken.token })
})
})(context.req, context.res, () => {}) })(context.req, context.res, () => {})
}) })
} else { } else {
@ -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) {
@ -374,6 +389,36 @@ module.exports = class User extends Model {
throw new WIKI.Error.AuthTFAInvalid() throw new WIKI.Error.AuthTFAInvalid()
} }
/**
* 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
* *
@ -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
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
block body block body
#root.is-fullscreen #root.is-fullscreen
v-app(dark) .app-error
.app-error a(href='/')
v-container img(src='/svg/logo-wikijs.svg')
.pt-5 strong Oops, something went wrong...
v-layout(row) span= message
v-flex(xs10)
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 pre: code #{error.stack}
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}

@ -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)
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(

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save