feat: mandatory password change on login + UI fixes

This commit is contained in:
Nick 2019-08-24 22:19:35 -04:00
parent 38008f0460
commit d3e693ab46
40 changed files with 1468 additions and 1064 deletions

View File

@ -66,7 +66,7 @@
v-tab-item(:transition='false', :reverse-transition='false')
.body-1.pa-3 {{ $t('admin:contribute.tshirts') }}
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
span {{ $t('admin:contribute.shop') }}
v-divider.mt-3

View File

@ -13,7 +13,7 @@
span {{$t('common:actions.apply')}}
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!
.caption Doing so may result in data loss or broken installation!
v-card-text

View File

@ -92,14 +92,14 @@
v-flex(lg6 xs12)
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-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-switch(
label='Asset Image Optimization'
color='primary'
color='indigo'
v-model='config.featureTinyPNG'
persistent-hint
hint='Image optimization tool to reduce filesize and bandwidth costs.'
@ -119,7 +119,7 @@
v-divider.mt-3
v-switch(
label='Page Ratings'
color='primary'
color='indigo'
v-model='config.featurePageRatings'
persistent-hint
hint='Allow users to rate pages.'
@ -129,7 +129,7 @@
v-divider.mt-3
v-switch(
label='Page Comments'
color='primary'
color='indigo'
v-model='config.featurePageComments'
persistent-hint
hint='Allow users to leave comments on pages.'
@ -139,13 +139,75 @@
v-divider.mt-3
v-switch(
label='Personal Wikis'
color='primary'
color='indigo'
v-model='config.featurePersonalWikis'
persistent-hint
hint='Allow users to have their own personal wiki.'
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>
<script>
@ -163,12 +225,6 @@ export default {
{ text: 'Google Analytics', value: 'ga' },
{ 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: {
host: '',
title: '',
@ -183,8 +239,28 @@ export default {
featurePageRatings: false,
featurePageComments: 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: {
@ -198,18 +274,24 @@ export default {
await this.$apollo.mutate({
mutation: siteUpdateConfigMutation,
variables: {
host: this.config.host || '',
title: this.config.title || '',
description: this.config.description || '',
robots: this.config.robots || [],
analyticsService: this.config.analyticsService || '',
analyticsId: this.config.analyticsId || '',
company: this.config.company || '',
hasLogo: this.config.hasLogo || false,
logoIsSquare: this.config.logoIsSquare || false,
featurePageRatings: this.config.featurePageRatings || false,
featurePageComments: this.config.featurePageComments || false,
featurePersonalWikis: this.config.featurePersonalWikis || false
host: _.get(this.config, 'host', ''),
title: _.get(this.config, 'title', ''),
description: _.get(this.config, 'description', ''),
robots: _.get(this.config, 'robots', []),
analyticsService: _.get(this.config, 'analyticsService', ''),
analyticsId: _.get(this.config, 'analyticsId', ''),
company: _.get(this.config, 'company', ''),
hasLogo: _.get(this.config, 'hasLogo', false),
logoIsSquare: _.get(this.config, 'logoIsSquare', false),
featurePageRatings: _.get(this.config, 'featurePageRatings', false),
featurePageComments: _.get(this.config, 'featurePageComments', 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) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-site-update')

View File

@ -23,26 +23,21 @@
must-sort,
hide-default-footer
)
template(slot='item', slot-scope='props')
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')
template(v-slot:activator='{ on }')
v-btn(icon, v-on='on', small)
v-icon.grey--text.text--darken-1 mdi-dots-horizontal
v-list(dense, nav)
v-list-item(:to='`/users/` + props.item.id')
v-list-item-action: v-icon(color='primary') mdi-account-outline
v-list-item-content
v-list-item-title View User Profile
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(v-slot:item.actions='{ item }')
v-menu(bottom, right, min-width='200')
template(v-slot:activator='{ on }')
v-btn(icon, v-on='on', small)
v-icon.grey--text.text--darken-1 mdi-dots-horizontal
v-list(dense, nav)
v-list-item(:to='`/users/` + item.id')
v-list-item-action: v-icon(color='primary') mdi-account-outline
v-list-item-content
v-list-item-title View User Profile
template(v-if='item.id !== 2')
v-list-item(@click='unassignUser(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')
v-alert.ma-3(icon='warning', outlined) No users to display.
.text-center.py-2(v-if='group.users.length > 15')
@ -70,10 +65,10 @@ export default {
data() {
return {
headers: [
{ text: 'ID', value: 'id', width: 50, align: 'right' },
{ text: 'ID', value: 'id', width: 50 },
{ text: 'Name', value: 'name' },
{ text: 'Email', value: 'email' },
{ text: '', value: 'actions', sortable: false, width: 50 }
{ text: 'Actions', value: 'actions', sortable: false, width: 50 }
],
searchUserDialog: false,
pagination: 1,

View File

@ -17,7 +17,7 @@
span New Group
v-card
.dialog-header.is-short New Group
v-card-text
v-card-text.pt-5
v-text-field.md2(
outlined
prepend-icon='mdi-account-group'

View File

@ -30,11 +30,11 @@
template(v-slot:activator='{ 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-card.wiki-form
v-card
.dialog-header.is-short.is-red
v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
span {{$t('common:page.delete')}}
v-card-text
v-card-text.pt-5
i18next.body-2(path='common:page.deleteTitle', tag='div')
span.red--text.text--darken-2(place='title') {{page.title}}
.caption {{$t('common:page.deleteSubtitle')}}
@ -44,7 +44,7 @@
span.red--text.text--darken-2 /{{page.path}}
v-card-chin
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.ml-1.animated.fadeInDown(color='teal', large, outlined, @click='rerenderPage')
v-icon(left) mdi-cube-scan

View File

@ -64,7 +64,7 @@
td {{ props.item.createdAt | moment('calendar') }}
td {{ props.item.updatedAt | moment('calendar') }}
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')
v-pagination(v-model='pagination', :length='pageTotal')
</template>

View File

@ -26,8 +26,8 @@
v-list-item(:key='eng.key', @click='selectedEngine = eng.key', :disabled='!eng.isAvailable')
v-list-item-avatar(size='24')
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='grey', v-else) mdi-checkbox-blank-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-circle-outline
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-subtitle: .caption(:class='!eng.isAvailable ? `grey--text text--lighten-1` : (selectedEngine === eng.key ? `blue--text ` : ``)') {{ eng.description }}

View File

@ -49,7 +49,7 @@
v-icon(color='white') mdi-clock-outline
v-list-item-content
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-progress-circular(indeterminate, :size='20', :width='2', color='purple')
template(v-else-if='tgt.status === `operational`')
@ -57,13 +57,13 @@
v-icon(color='white') mdi-check-circle
v-list-item-content
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)
v-list-item-avatar(color='red')
v-icon(color='white') mdi-close-circle-outline
v-list-item-content
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-menu
v-btn(slot='activator', icon)
@ -86,6 +86,10 @@
img(:src='target.logo', :alt='target.title')
.body-2.pt-3 {{target.description}}
.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
.overline.my-5 {{$t('admin:storage.targetConfig')}}
.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')
v-divider.mt-3
.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-layout(row, wrap, fill-height)
v-flex(xs12, lg6, xl4, v-for='act of target.actions', :key='act.handler')
@ -190,7 +196,7 @@
@click='executeAction(target.key, act.handler)'
outlined
:color='$vuetify.theme.dark ? `blue` : `primary`'
:disabled='runningAction'
:disabled='runningAction || !target.isEnabled'
:loading='runningActionHandler === act.handler'
) {{$t('admin:storage.actionRun')}}

View File

@ -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-subheader Wiki.js
v-list(two-line, dense)
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-icon.blue.white--text mdi-application-export
v-list-item-content
v-list-item-title {{ $t('admin:system.currentVersion') }}
v-list-item-subtitle {{ info.currentVersion }}
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-icon.blue.white--text mdi-inbox-arrow-up
v-list-item-content
@ -31,38 +31,38 @@
v-divider.mt-3
v-subheader {{ $t('admin:system.hostInfo') }}
v-list(two-line, dense)
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-avatar.blue-grey(size='40')
v-icon(color='white') {{platformLogo}}
v-list-item-content
v-list-item-title {{ $t('admin:system.os') }}
v-list-item-subtitle {{ (info.platform === 'docker') ? 'Docker Container (Linux)' : info.operatingSystem }}
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-icon.blue-grey.white--text mdi-desktop-classic
v-list-item-content
v-list-item-title {{ $t('admin:system.hostname') }}
v-list-item-subtitle {{ info.hostname }}
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-icon.blue-grey.white--text mdi-cpu-64-bit
v-list-item-content
v-list-item-title {{ $t('admin:system.cpuCores') }}
v-list-item-subtitle {{ info.cpuCores }}
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-icon.blue-grey.white--text mdi-memory
v-list-item-content
v-list-item-title {{ $t('admin:system.totalRAM') }}
v-list-item-subtitle {{ info.ramTotal }}
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-icon.blue-grey.white--text mdi-iframe-outline
v-list-item-content
v-list-item-title {{ $t('admin:system.workingDirectory') }}
v-list-item-subtitle {{ info.workingDirectory }}
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-icon.blue-grey.white--text mdi-card-bulleted-settings-outline
v-list-item-content
@ -73,7 +73,7 @@
v-card.pb-3.animated.fadeInUp.wait-p4s
v-subheader Node.js
v-list(dense)
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-avatar.light-green(size='40')
v-icon(color='white') mdi-nodejs
@ -83,7 +83,7 @@
v-divider.mt-3
v-subheader {{ info.dbType }}
v-list(dense)
v-list-item(avatar)
v-list-item
v-list-item-avatar
v-avatar.indigo.darken-1(size='40')
v-icon(color='white') mdi-database

View File

@ -8,7 +8,7 @@
v-btn.mx-0(color='white', outlined, disabled, dark)
v-icon(left) mdi-database-import
span Bulk Import
v-card-text
v-card-text.pt-5
v-select(
:items='providers'
item-text='title'
@ -89,6 +89,7 @@
label='Send a welcome email'
hide-details
v-model='sendWelcomeEmail'
disabled
)
v-card-chin
v-spacer

View File

@ -3,12 +3,26 @@
v-layout(row, wrap)
v-flex(xs12)
.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
.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}}
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-btn.ml-3.animated.fadeInDown.wait-p2s(color='grey', large, outlined, to='/users')
v-icon mdi-arrow-left
@ -30,15 +44,15 @@
v-card.animated.fadeInUp
v-toolbar(color='primary', dense, dark, flat)
v-icon.mr-2 mdi-information-variant
span Basic Info
span {{$t('admin:users.basicInfo')}}
v-list.py-0(two-line, dense)
v-list-item
v-list-item-avatar(size='32')
v-icon mdi-email-variant
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-action(v-if='!user.isSystem')
v-list-item-action(v-if='!user.isSystem && user.providerKey === `local`')
v-menu(
v-model='editPop.email'
:close-on-content-click='false'
@ -52,7 +66,7 @@
v-text-field(
ref='iptEmail'
v-model='user.email'
label='Email'
:label='$t(`admin:users.email`)'
solo
hide-details
append-icon='mdi-check'
@ -66,7 +80,7 @@
v-list-item-avatar(size='32')
v-icon mdi-account
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-action
v-menu(
@ -82,7 +96,7 @@
v-text-field(
ref='iptDisplayName'
v-model='user.name'
label='Display Name'
:label='$t(`admin:users.displayName`)'
solo
hide-details
append-icon='mdi-check'
@ -94,13 +108,13 @@
v-card.mt-3.animated.fadeInUp.wait-p2s(v-if='!user.isSystem')
v-toolbar(color='primary', dense, dark, flat)
v-icon.mr-2 mdi-lock-outline
span Authentication
span {{$t('admin:users.authentication')}}
v-list.py-0(two-line, dense)
v-list-item
v-list-item-avatar(size='32')
v-icon mdi-domain
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-action
//- 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-icon mdi-textbox-password
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-action
v-menu(
@ -124,12 +138,12 @@
template(v-slot:activator='{ on: tooltip }')
v-btn(icon, color='grey', x-small, v-on='{ ...menu, ...tooltip }', @click='focusField(`iptNewPassword`)')
v-icon mdi-cached
span Change Password
span {{$t('admin:users.changePassword')}}
v-card
v-text-field(
ref='iptNewPassword'
v-model='newPassword'
label='New Password'
:label='$t(`admin:users.newPassword`)'
solo
hide-details
append-icon='mdi-check'
@ -149,26 +163,26 @@
v-list-item-avatar(size='32')
v-icon mdi-two-factor-authentication
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-action
v-tooltip(top)
template(v-slot:activator='{ on }')
v-btn(icon, color='grey', x-small, v-on='on', disabled)
v-icon mdi-power
span Toggle 2FA
template(v-if='user.providerId')
v-divider
v-list-item
v-list-item-avatar(size='32')
v-icon mdi-account
v-list-item-content
v-list-item-title Provider Id
v-list-item-subtitle {{ user.providerId }}
span {{$t('admin:users.toggle2FA')}}
template(v-if='user.providerId')
v-divider
v-list-item
v-list-item-avatar(size='32')
v-icon mdi-music-accidental-sharp
v-list-item-content
v-list-item-title {{$t('admin:users.authProviderId')}}
v-list-item-subtitle {{ user.providerId }}
v-card.mt-3.animated.fadeInUp.wait-p4s
v-toolbar(color='primary', dense, dark, flat)
v-icon.mr-2 mdi-account-group
span User Groups
span {{$t('admin:users.groups')}}
v-list(dense)
template(v-for='(group, idx) in user.groups')
v-list-item(:key='`group-` + group.id')
@ -181,14 +195,14 @@
v-icon mdi-close
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')
.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-spacer
v-select(
ref='iptAssignGroup'
:items='groups'
v-model='newGroup'
label='Select Group...'
:label='$t(`admin:users.selectGroup`)'
item-value='id'
item-text='name'
item-disabled='isSystem'
@ -201,18 +215,18 @@
)
v-btn.ml-2.px-4(depressed, color='primary', height='48', @click='assignGroup', :disabled='newGroup === 0')
v-icon(left) mdi-clipboard-account-outline
span Assign
span {{$t('admin:users.groupAssign')}}
v-flex(xs6)
v-card.animated.fadeInUp.wait-p2s
v-toolbar(color='primary', dense, dark, flat)
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-item
v-list-item-avatar(size='32')
v-icon mdi-map-marker
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-action
v-menu(
@ -228,7 +242,7 @@
v-text-field(
ref='iptLocation'
v-model='user.location'
label='Location'
:label='$t(`admin:users.location`)'
solo
hide-details
append-icon='mdi-check'
@ -241,7 +255,7 @@
v-list-item-avatar(size='32')
v-icon mdi-account-badge-horizontal-outline
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-action
v-menu(
@ -257,7 +271,7 @@
v-text-field(
ref='iptJobTitle'
v-model='user.jobTitle'
label='Job Title'
:label='$t(`admin:users.jobTitle`)'
solo
hide-details
append-icon='mdi-check'
@ -270,7 +284,7 @@
v-list-item-avatar(size='32')
v-icon mdi-map-clock-outline
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-action
v-menu(
@ -287,7 +301,7 @@
ref='iptTimezone'
:items='timezones'
v-model='user.timezone'
label='Timezone'
:label='$t(`admin:users.timezone`)'
solo
dense
hide-details
@ -308,11 +322,16 @@
import _ from 'lodash'
import { get } from 'vuex-pathify'
import { StatusIndicator } from 'vue-status-indicator'
import userQuery from 'gql/admin/users/users-query-single.gql'
import groupsQuery from 'gql/admin/users/users-query-groups.gql'
import updateUserMutation from 'gql/admin/users/users-mutation-update.gql'
export default {
components: {
StatusIndicator
},
data() {
return {
deleteUserDialog: false,
@ -334,7 +353,9 @@ export default {
location: '',
jobTitle: '',
timezone: '',
groups: []
groups: [],
isActive: false,
isVerified: false
},
timezones: [
{ text: '(GMT-11:00) Niue', value: 'Pacific/Niue' },
@ -613,7 +634,7 @@ export default {
if (_.get(resp, 'data.users.update.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
style: 'success',
message: 'User updated successfully.',
message: this.$t('admin:users.userUpdateSuccess'),
icon: 'check'
})
this.$router.push('/users')
@ -636,7 +657,7 @@ export default {
assignGroup() {
if (_.some(this.user.groups, ['id', this.newGroup])) {
this.$store.commit('showNotification', {
message: 'User is already assigned to this group!',
message: this.$t('admin:users.userAlreadyAssignedToGroup'),
style: 'error',
icon: 'alert'
})

View File

@ -4,7 +4,7 @@
.dialog-header.is-short.is-red
v-icon.mr-2(color='white') mdi-file-document-box-remove-outline
span {{$t('common:page.delete')}}
v-card-text
v-card-text.pt-5
i18next.body-1(path='common:page.deleteTitle', tag='div')
span.red--text.text--darken-2(place='title') {{pageTitle}}
.caption {{$t('common:page.deleteSubtitle')}}

View File

@ -3,7 +3,7 @@
v-model='dialogOpen'
max-width='650'
)
v-card.wiki-form
v-card
.dialog-header
span {{$t('common:user.search')}}
v-spacer
@ -14,7 +14,7 @@
:width='2'
v-show='searchLoading'
)
v-card-text
v-card-text.pt-5
v-text-field(
outlined
:label='$t(`common:user.searchPlaceholder`)'
@ -56,7 +56,7 @@ import searchUsersQuery from 'gql/common/common-users-query-search.gql'
export default {
filters: {
initials(val) {
return val.split(' ').map(v => v.substring(0, 1)).join()
return val.split(' ').map(v => v.substring(0, 1)).join('')
}
},
props: {

View File

@ -11,17 +11,18 @@
offset-xl4, xl4
)
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-spacer
.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) {{ $t('auth:loginRequired') }}
v-spacer
v-card-text.text-center
h1.display-1.primary--text.py-2 {{ siteTitle }}
template(v-if='screen === "login"')
v-text-field.md2.mt-3(
v-text-field.mt-3(
solo
flat
prepend-icon='mdi-clipboard-account'
@ -31,7 +32,7 @@
v-model='username'
:placeholder='$t("auth:fields.emailUser")'
)
v-text-field.md2.mt-2(
v-text-field.mt-2(
solo
flat
prepend-icon='mdi-textbox-password'
@ -47,7 +48,7 @@
)
template(v-else-if='screen === "tfa"')
.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
flat
background-color='grey lighten-4'
@ -57,12 +58,34 @@
:placeholder='$t("auth:tfa.placeholder")'
@keyup.enter='verifySecurityCode'
)
template(v-else-if='screen === "forgot"')
.body-2 {{ $t('auth:forgotPasswordSubtitle') }}
v-text-field.md2.mt-3(
template(v-else-if='screen === "changePwd"')
.body-2 {{$t('auth:changePwd.instructions')}}
v-text-field.mt-2(
type='password'
solo
flat
prepend-icon='email'
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"')
.body-2 {{ $t('auth:forgotPasswordSubtitle') }}
v-text-field.mt-3(
solo
flat
prepend-icon='mdi-email'
background-color='grey lighten-4'
hide-details
ref='iptEmailForgot'
@ -71,31 +94,48 @@
)
v-card-actions.pb-4
v-spacer
v-btn.md2(
v-btn(
width='100%'
max-width='250px'
v-if='screen === "login"'
block
large
color='primary'
color='teal'
dark
@click='login'
round
rounded
:loading='isLoading'
) {{ $t('auth:actions.login') }}
v-btn.md2(
v-btn(
width='100%'
max-width='250px'
v-else-if='screen === "tfa"'
block
large
color='primary'
color='teal'
dark
@click='verifySecurityCode'
round
rounded
:loading='isLoading'
) {{ $t('auth:tfa.verifyToken') }}
v-btn.md2(
v-else-if='screen === "forgot"'
block
v-btn(
width='100%'
max-width='250px'
v-else-if='screen === "changePwd"'
large
color='primary'
color='teal'
dark
@click='changePassword'
rounded
:loading='isLoading'
) {{ $t('auth:changePwd.proceed') }}
v-btn(
width='100%'
max-width='250px'
v-else-if='screen === "forgot"'
large
color='teal'
dark
@click='forgotPasswordSubmit'
round
rounded
:loading='isLoading'
) {{ $t('auth:sendResetPassword') }}
v-spacer
@ -111,15 +151,16 @@
v-divider
v-card-text.grey.lighten-4.text-center
.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')
.social-login-btn.mr-2(
slot='activator'
v-ripple
v-html='strategy.icon'
:class='strategy.color + " elevation-" + (strategy.key === selectedStrategy.key ? "0" : "4")'
@click='selectStrategy(strategy)'
)
span {{ strategy.title }}
v-btn.mx-1.social-login-btn(
v-for='strategy in strategies', :key='strategy.key'
large
@click='selectStrategy(strategy)'
dark
:color='strategy.color'
:depressed='strategy.key === selectedStrategy.key'
)
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')
v-divider
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 loginMutation from 'gql/login/login-mutation-login.gql'
import tfaMutation from 'gql/login/login-mutation-tfa.gql'
import changePasswordMutation from 'gql/login/login-mutation-changepassword.gql'
export default {
i18nOptions: { namespaces: 'auth' },
@ -155,11 +197,13 @@ export default {
password: '',
hidePassword: true,
securityCode: '',
loginToken: '',
continuationToken: '',
isLoading: false,
loaderColor: 'grey darken-4',
loaderTitle: 'Working...',
isShown: false
isShown: false,
newPassword: '',
newPasswordVerify: ''
}
},
computed: {
@ -205,14 +249,14 @@ export default {
this.$store.commit('showNotification', {
style: 'red',
message: this.$t('auth:invalidEmailUsername'),
icon: 'warning'
icon: 'alert'
})
this.$refs.iptEmail.focus()
} else if (this.password.length < 2) {
this.$store.commit('showNotification', {
style: 'red',
message: this.$t('auth:invalidPassword'),
icon: 'warning'
icon: 'alert'
})
this.$refs.iptPassword.focus()
} else {
@ -231,10 +275,16 @@ export default {
if (_.has(resp, 'data.authentication.login')) {
let respObj = _.get(resp, 'data.authentication.login', {})
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.securityCode = ''
this.loginToken = respObj.tfaLoginToken
this.$nextTick(() => {
this.$refs.iptTFA.focus()
})
@ -258,7 +308,7 @@ export default {
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'warning'
icon: 'alert'
})
this.isLoading = false
}
@ -280,7 +330,7 @@ export default {
this.$apollo.mutate({
mutation: tfaMutation,
variables: {
loginToken: this.loginToken,
continuationToken: this.continuationToken,
securityCode: this.securityCode
}
}).then(resp => {
@ -307,23 +357,59 @@ export default {
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'warning'
icon: 'alert'
})
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.$nextTick(() => {
this.$refs.iptEmailForgot.focus()
})
},
async forgotPasswordSubmit() {
/**
* FORGOT PASSWORD SUBMIT
*/
async forgotPasswordSubmit () {
this.$store.commit('showNotification', {
style: 'pink',
message: 'Coming soon!',
icon: 'free_breakfast'
icon: 'ferry'
})
}
},
@ -378,18 +464,12 @@ export default {
}
.social-login-btn {
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 54px;
height: 54px;
cursor: pointer;
transition: opacity .2s ease;
&:hover {
opacity: .8;
}
margin: .5rem 0;
margin: .25rem 0;
svg {
width: 24px;
height: 24px;

View File

@ -11,6 +11,12 @@ mutation (
$featurePageRatings: Boolean!
$featurePageComments: Boolean!
$featurePersonalWikis: Boolean!
$securityIframe: Boolean!
$securityReferrerPolicy: Boolean!
$securityHSTS: Boolean!
$securityHSTSDuration: Int!
$securityCSP: Boolean!
$securityCSPDirectives: String!
) {
site {
updateConfig(
@ -25,7 +31,13 @@ mutation (
logoIsSquare: $logoIsSquare,
featurePageRatings: $featurePageRatings,
featurePageComments: $featurePageComments,
featurePersonalWikis: $featurePersonalWikis
featurePersonalWikis: $featurePersonalWikis,
securityIframe: $securityIframe,
securityReferrerPolicy: $securityReferrerPolicy,
securityHSTS: $securityHSTS,
securityHSTSDuration: $securityHSTSDuration,
securityCSP: $securityCSP,
securityCSPDirectives: $securityCSPDirectives
) {
responseResult {
succeeded

View File

@ -13,6 +13,12 @@
featurePageRatings
featurePageComments
featurePersonalWikis
securityIframe
securityReferrerPolicy
securityHSTS
securityHSTSDuration
securityCSP
securityCSPDirectives
}
}
}

View File

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

View File

@ -0,0 +1,13 @@
mutation($continuationToken: String!, $newPassword: String!) {
authentication {
loginChangePassword(continuationToken: $continuationToken, newPassword: $newPassword) {
responseResult {
succeeded
errorCode
slug
message
}
jwt
}
}
}

View File

@ -8,8 +8,9 @@ mutation($username: String!, $password: String!, $strategy: String!) {
message
}
jwt
tfaRequired
tfaLoginToken
mustChangePwd
mustProvideTFA
continuationToken
}
}
}

View File

@ -1,12 +1,13 @@
mutation($loginToken: String!, $securityCode: String!) {
mutation($continuationToken: String!, $securityCode: String!) {
authentication {
loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
loginTFA(continuationToken: $continuationToken, securityCode: $securityCode) {
responseResult {
succeeded
errorCode
slug
message
}
jwt
}
}
}

View File

@ -6,6 +6,7 @@
justify-content: center;
align-items: center;
color: mc('grey', '50');
font-family: Roboto, Arial, sans-serif;
img {
width: 250px;
@ -57,8 +58,20 @@
}
}
code {
color: mc('grey', '500');
font-size: .8rem;
> strong {
font-size: 1.5rem;
}
> span {
margin-top: 1rem;
}
> pre {
margin-top: 2rem;
code {
color: mc('grey', '500');
font-size: .8rem;
}
}
}

View File

@ -70,20 +70,7 @@
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-card.mt-5
.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')
v-card.mt-5(v-if='tags.length > 0')
.pa-5
.overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') Tags
v-chip.mr-1(
@ -96,6 +83,19 @@
v-icon(color='teal', left, small) mdi-label
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
.pa-5
.overline.pb-2.yellow--text(:class='$vuetify.theme.dark ? `text--darken-3` : `text--darken-4`') Rating
@ -108,20 +108,21 @@
hover
)
.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-tooltip(bottom)
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')}}
v-tooltip(bottom)
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')}}
v-tooltip(bottom)
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')}}
v-spacer

View File

@ -32,7 +32,7 @@ html
link(
type='text/css'
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'
link(

View File

@ -36,7 +36,7 @@ html(lang=siteConfig.lang)
link(
type='text/css'
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'
link(

View File

@ -35,13 +35,13 @@
},
"dependencies": {
"@aoberoi/passport-slack": "1.0.5",
"@bugsnag/js": "6.3.2",
"@bugsnag/js": "6.4.0",
"algoliasearch": "3.33.0",
"apollo-fetch": "0.7.0",
"apollo-server": "2.8.1",
"apollo-server-express": "2.8.1",
"apollo-server": "2.9.0",
"apollo-server-express": "2.9.0",
"auto-load": "3.0.4",
"aws-sdk": "2.503.0",
"aws-sdk": "2.517.0",
"axios": "0.19.0",
"azure-search-client": "3.1.5",
"bcryptjs-then": "1.0.1",
@ -67,18 +67,18 @@
"express": "4.17.1",
"express-brute": "1.0.1",
"express-session": "1.16.2",
"file-type": "12.1.0",
"file-type": "12.2.0",
"filesize": "4.1.2",
"fs-extra": "8.1.0",
"getos": "3.1.1",
"graphql": "14.4.2",
"graphql": "14.5.3",
"graphql-list-fields": "2.0.2",
"graphql-rate-limit-directive": "1.1.0",
"graphql-subscriptions": "1.1.0",
"graphql-tools": "4.0.5",
"highlight.js": "9.15.9",
"i18next": "17.0.8",
"i18next-express-middleware": "1.8.0",
"highlight.js": "9.15.10",
"i18next": "17.0.12",
"i18next-express-middleware": "1.8.1",
"i18next-node-fs-backend": "2.1.3",
"image-size": "0.7.4",
"js-base64": "2.5.1",
@ -86,12 +86,12 @@
"js-yaml": "3.13.1",
"jsonwebtoken": "8.5.1",
"klaw": "3.0.0",
"knex": "0.19.1",
"knex": "0.19.2",
"lodash": "4.17.15",
"markdown-it": "9.0.1",
"markdown-it": "9.1.0",
"markdown-it-abbr": "1.0.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-expand-tabs": "1.0.13",
"markdown-it-external-links": "0.0.6",
@ -106,7 +106,7 @@
"mime-types": "2.1.24",
"moment": "2.24.0",
"moment-timezone": "0.5.26",
"mongodb": "3.2.7",
"mongodb": "3.3.1",
"mssql": "5.1.0",
"multer": "1.4.2",
"mysql2": "1.6.5",
@ -116,7 +116,7 @@
"nodemailer": "6.3.0",
"objection": "1.6.9",
"passport": "0.4.0",
"passport-auth0": "1.2.0",
"passport-auth0": "1.2.1",
"passport-azure-ad": "4.1.0",
"passport-cas": "0.1.1",
"passport-discord": "0.1.3",
@ -135,7 +135,7 @@
"passport-saml": "1.1.0",
"passport-twitch": "1.0.3",
"pem-jwk": "2.0.0",
"pg": "7.12.0",
"pg": "7.12.1",
"pg-hstore": "2.3.3",
"pg-query-stream": "2.0.0",
"pg-tsquery": "8.0.5",
@ -152,18 +152,18 @@
"serve-favicon": "2.5.0",
"simple-git": "1.124.0",
"solr-node": "1.2.1",
"sqlite3": "4.0.9",
"sqlite3": "4.1.0",
"striptags": "3.1.1",
"subscriptions-transport-ws": "0.9.16",
"tar-fs": "2.0.0",
"twemoji": "12.1.2",
"uslug": "1.0.4",
"uuid": "3.3.2",
"uuid": "3.3.3",
"validate.js": "0.13.1",
"validator": "11.1.0",
"validator-as-promised": "1.0.2",
"winston": "3.2.1",
"yargs": "13.3.0"
"yargs": "14.0.0"
},
"devDependencies": {
"@babel/cli": "^7.5.0",
@ -179,13 +179,13 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.4",
"@mdi/font": "3.8.95",
"@mdi/font": "4.1.95",
"@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",
"animated-number-vue": "1.0.0",
"apollo-cache-inmemory": "1.6.2",
"apollo-client": "2.6.3",
"apollo-cache-inmemory": "1.6.3",
"apollo-client": "2.6.4",
"apollo-link": "1.2.12",
"apollo-link-batch-http": "1.2.12",
"apollo-link-error": "1.1.11",
@ -195,9 +195,9 @@
"apollo-utilities": "1.3.2",
"autoprefixer": "9.6.1",
"babel-eslint": "10.0.2",
"babel-jest": "24.8.0",
"babel-jest": "24.9.0",
"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-prismjs": "1.1.1",
"babel-plugin-transform-imports": "2.0.0",
@ -206,26 +206,26 @@
"chart.js": "2.8.0",
"clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "5.0.4",
"core-js": "3.1.4",
"css-loader": "3.1.0",
"core-js": "3.2.1",
"css-loader": "3.2.0",
"cssnano": "4.1.10",
"duplicate-package-checker-webpack-plugin": "3.0.0",
"epic-spinners": "1.1.0",
"eslint": "6.1.0",
"eslint": "6.2.2",
"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-node": "9.1.0",
"eslint-plugin-promise": "4.2.1",
"eslint-plugin-standard": "4.0.0",
"eslint-plugin-standard": "4.0.1",
"eslint-plugin-vue": "5.2.3",
"fibers": "4.0.1",
"file-loader": "4.1.0",
"filepond": "4.4.12",
"file-loader": "4.2.0",
"filepond": "4.5.0",
"filepond-plugin-file-validate-type": "1.2.4",
"filesize.js": "1.0.2",
"grapesjs": "0.14.62",
"graphiql": "0.13.2",
"grapesjs": "0.15.3",
"graphiql": "0.14.2",
"graphql-persisted-document-loader": "1.0.1",
"graphql-tag": "^2.10.1",
"graphql-voyager": "1.0.0-rc.27",
@ -234,10 +234,10 @@
"html-webpack-pug-plugin": "2.0.0",
"i18next-chained-backend": "2.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",
"jest": "24.8.0",
"js-cookie": "2.2.0",
"jest": "24.9.0",
"js-cookie": "2.2.1",
"mini-css-extract-plugin": "0.8.0",
"moment-duration-format": "2.3.2",
"offline-plugin": "5.0.7",
@ -254,19 +254,19 @@
"pug-loader": "2.4.0",
"pug-plain-loader": "1.0.0",
"raw-loader": "3.1.0",
"react": "16.8.6",
"react-dom": "16.8.6",
"react": "16.9.0",
"react-dom": "16.9.0",
"resolve-url-loader": "3.1.0",
"sass": "1.22.9",
"sass-loader": "7.1.0",
"sass": "1.22.10",
"sass-loader": "7.3.1",
"sass-resources-loader": "2.0.1",
"script-ext-html-webpack-plugin": "2.1.4",
"simple-progress-webpack-plugin": "1.1.2",
"style-loader": "0.23.1",
"terser": "4.1.3",
"style-loader": "1.0.0",
"terser": "4.2.1",
"twemoji-awesome": "1.0.6",
"url-loader": "2.1.0",
"vee-validate": "2.2.13",
"vee-validate": "2.2.15",
"velocity-animate": "1.5.2",
"viz.js": "2.1.2",
"vue": "2.6.10",
@ -274,32 +274,32 @@
"vue-chartjs": "3.4.2",
"vue-clipboards": "1.3.0",
"vue-codemirror": "4.0.6",
"vue-filepond": "5.1.2",
"vue-filepond": "5.1.3",
"vue-hot-reload-api": "2.3.3",
"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-router": "3.0.7",
"vue-router": "3.1.2",
"vue-simple-breakpoints": "1.0.3",
"vue-status-indicator": "1.2.1",
"vue-template-compiler": "2.6.10",
"vue-tour": "1.1.0",
"vue2-animate": "2.1.0",
"vue2-animate": "2.1.2",
"vuedraggable": "2.23.0",
"vuescroll": "4.13.1",
"vuetify": "2.0.4",
"vuescroll": "4.14.0",
"vuetify": "2.0.10",
"vuetify-loader": "1.3.0",
"vuex": "3.1.1",
"vuex-pathify": "1.2.4",
"vuex-persistedstate": "2.5.4",
"webpack": "4.39.1",
"webpack": "4.39.2",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.6",
"webpack-cli": "3.3.7",
"webpack-dev-middleware": "3.7.0",
"webpack-hot-middleware": "2.25.0",
"webpack-merge": "4.2.1",
"webpack-subresource-integrity": "1.3.2",
"webpackbar": "3.2.0",
"webpackbar": "4.0.0",
"whatwg-fetch": "3.0.0",
"write-file-webpack-plugin": "4.5.1",
"xterm": "3.14.5",
@ -344,7 +344,10 @@
"requireSpaceAfterCodeOperator": true,
"requireStrictEqualityOperators": true,
"validateAttributeQuoteMarks": "'",
"validateAttributeSeparator": { "separator": ", ", "multiLineSeparator": "\n " },
"validateAttributeSeparator": {
"separator": ", ",
"multiLineSeparator": "\n "
},
"validateDivTags": true,
"validateIndentation": 2,
"excludeFiles": [

View File

@ -42,6 +42,13 @@ defaults:
theme: 'default'
iconset: 'md'
darkMode: false
security:
securityIframe: true
securityReferrerPolicy: true
securityHSTS: false
securityHSTSDuration: 300
securityCSP: false
securityCSPDirectives: ''
flags:
ldapdebug: false
sqllog: false

View File

@ -7,16 +7,16 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async authentication() { return {} }
async authentication () { return {} }
},
Mutation: {
async authentication() { return {} }
async authentication () { return {} }
},
AuthenticationQuery: {
/**
* 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)
strategies = strategies.map(stg => {
const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {}
@ -44,7 +44,7 @@ module.exports = {
/**
* Perform Login
*/
async login(obj, args, context) {
async login (obj, args, context) {
try {
const authResult = await WIKI.models.users.login(args, context)
return {
@ -63,7 +63,7 @@ module.exports = {
/**
* Perform 2FA Login
*/
async loginTFA(obj, args, context) {
async loginTFA (obj, args, context) {
try {
const authResult = await WIKI.models.users.loginTFA(args, context)
return {
@ -74,10 +74,24 @@ module.exports = {
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
*/
async register(obj, args, context) {
async register (obj, args, context) {
try {
await WIKI.models.users.register({ ...args, verify: true }, context)
return {
@ -90,7 +104,7 @@ module.exports = {
/**
* Update Authentication Strategies
*/
async updateStrategies(obj, args, context) {
async updateStrategies (obj, args, context) {
try {
WIKI.config.auth = {
audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
@ -122,7 +136,7 @@ module.exports = {
/**
* Generate New Authentication Public / Private Key Certificates
*/
async regenerateCertificates(obj, args, context) {
async regenerateCertificates (obj, args, context) {
try {
await WIKI.auth.regenerateCertificates()
return {
@ -135,7 +149,7 @@ module.exports = {
/**
* Reset Guest User
*/
async resetGuestUser(obj, args, context) {
async resetGuestUser (obj, args, context) {
try {
await WIKI.auth.resetGuestUser()
return {

View File

@ -17,7 +17,8 @@ module.exports = {
company: WIKI.config.company,
...WIKI.config.seo,
...WIKI.config.logo,
...WIKI.config.features
...WIKI.config.features,
...WIKI.config.security
}
}
},
@ -42,7 +43,15 @@ module.exports = {
featurePageComments: args.featurePageComments,
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 {
responseResult: graphHelper.generateSuccess('Site configuration updated successfully')

View File

@ -32,9 +32,14 @@ type AuthenticationMutation {
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
loginTFA(
loginToken: String!
continuationToken: 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(
email: String!
@ -76,8 +81,9 @@ type AuthenticationStrategy {
type AuthenticationLoginResponse {
responseResult: ResponseStatus
jwt: String
tfaRequired: Boolean
tfaLoginToken: String
mustChangePwd: Boolean
mustProvideTFA: Boolean
continuationToken: String
}
type AuthenticationRegisterResponse {

View File

@ -36,6 +36,12 @@ type SiteMutation {
featurePageRatings: Boolean!
featurePageComments: Boolean!
featurePersonalWikis: Boolean!
securityIframe: Boolean!
securityReferrerPolicy: Boolean!
securityHSTS: Boolean!
securityHSTSDuration: Int!
securityCSP: Boolean!
securityCSPDirectives: String!
): DefaultResponse @auth(requires: ["manage:system"])
}
@ -56,4 +62,10 @@ type SiteConfig {
featurePageRatings: Boolean!
featurePageComments: Boolean!
featurePersonalWikis: Boolean!
securityIframe: Boolean!
securityReferrerPolicy: Boolean!
securityHSTS: Boolean!
securityHSTSDuration: Int!
securityCSP: Boolean!
securityCSPDirectives: String!
}

View File

@ -89,6 +89,8 @@ type User {
providerKey: String!
providerId: String
isSystem: Boolean!
isActive: Boolean!
isVerified: Boolean!
location: String!
jobTitle: String!
timezone: String!

View File

@ -1,4 +1,4 @@
'use strict'
/* global WIKI */
/**
* Security Middleware
@ -13,7 +13,9 @@ module.exports = function (req, res, next) {
req.app.disable('x-powered-by')
// -> 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
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')
// -> 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()
}

View File

@ -45,7 +45,7 @@ module.exports = class UserKey extends Model {
}
static async generateToken ({ userId, kind }, context) {
const token = await nanoid()
const token = nanoid()
await WIKI.models.userKeys.query().insert({
kind,
token,

View File

@ -3,7 +3,6 @@
const bcrypt = require('bcryptjs-then')
const _ = require('lodash')
const tfa = require('node-2fa')
const securityHelper = require('../helpers/security')
const jwt = require('jsonwebtoken')
const Model = require('objection').Model
const validate = require('validate.js')
@ -280,30 +279,46 @@ module.exports = class User extends Model {
if (err) { return reject(err) }
if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
// Is 2FA required?
if (user.tfaIsActive) {
// Must Change Password?
if (user.mustChangePwd) {
try {
let loginToken = await securityHelper.generateToken(32)
await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
const pwdChangeToken = await WIKI.models.userKeys.generateToken({
kind: 'changePwd',
userId: user.id
})
return resolve({
tfaRequired: true,
tfaLoginToken: loginToken
mustChangePwd: true,
continuationToken: pwdChangeToken
})
} catch (err) {
WIKI.logger.warn(err)
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
})
})
}
// Is 2FA required?
if (user.tfaIsActive) {
try {
const tfaToken = await WIKI.models.userKeys.generateToken({
kind: 'tfa',
userId: user.id
})
return resolve({
tfaRequired: true,
continuationToken: tfaToken
})
} catch (err) {
WIKI.logger.warn(err)
return reject(new WIKI.Error.AuthGenericError())
}
}
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, () => {})
})
} 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) {
let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
if (result) {
@ -374,6 +389,36 @@ module.exports = class User extends Model {
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
*
@ -520,7 +565,7 @@ module.exports = class User extends Model {
}
usrData.password = newPassword
}
if (!_.isEmpty(groups)) {
if (_.isArray(groups)) {
const usrGroupsRaw = await usr.$relatedQuery('groups')
const usrGroups = _.map(usrGroupsRaw, 'id')
// Relate added groups

View File

@ -3,7 +3,7 @@ title: Local
description: Built-in authentication for Wiki.js
author: requarks.io
logo: https://static.requarks.io/logo/wikijs.svg
color: yellow darken-3
color: primary
website: https://wiki.js.org
isAvailable: true
useForm: true

View File

@ -2,25 +2,11 @@ extends master.pug
block body
#root.is-fullscreen
v-app(dark)
.app-error
v-container
.pt-5
v-layout(row)
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
.app-error
a(href='/')
img(src='/svg/logo-wikijs.svg')
strong Oops, something went wrong...
span= message
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}
if error.stack
pre: code #{error.stack}

View File

@ -32,7 +32,7 @@ html
link(
type='text/css'
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'
link(

View File

@ -36,7 +36,7 @@ html(lang=siteConfig.lang)
link(
type='text/css'
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'
link(

1591
yarn.lock

File diff suppressed because it is too large Load Diff