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;