From e31935501718c672f3576cf56edbf5dab9317f56 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 30 Aug 2020 14:18:22 -0400 Subject: [PATCH] feat: enable/disable TFA per user --- client/components/admin/admin-theme.vue | 52 ++++++------- client/components/admin/admin-users-edit.vue | 82 +++++++++++++++++++- server/controllers/auth.js | 2 +- server/graph/resolvers/user.js | 30 ++++++- server/graph/schemas/user.graphql | 9 +++ server/models/users.js | 2 +- 6 files changed, 144 insertions(+), 33 deletions(-) diff --git a/client/components/admin/admin-theme.vue b/client/components/admin/admin-theme.vue index 1803ab1b..83ec5fac 100644 --- a/client/components/admin/admin-theme.vue +++ b/client/components/admin/admin-theme.vue @@ -70,33 +70,33 @@ ) v-flex(lg6 xs12) - v-card.animated.fadeInUp.wait-p2s - v-toolbar(color='teal', dark, dense, flat) - v-toolbar-title.subtitle-1 {{$t('admin:theme.downloadThemes')}} - v-spacer - v-chip(label, color='white', small).teal--text coming soon - v-data-table( - :headers='headers', - :items='themes', - hide-default-footer, - item-key='value', - :items-per-page='1000' - ) - template(v-slot:item='thm') - td - strong {{thm.item.text}} - td - span {{ thm.item.author }} - td.text-xs-center - v-progress-circular(v-if='thm.item.isDownloading', indeterminate, color='blue', size='20', :width='2') - v-btn(v-else-if='thm.item.isInstalled && thm.item.installDate < thm.item.updatedAt', icon) - v-icon.blue--text mdi-cached - v-btn(v-else-if='thm.item.isInstalled', icon) - v-icon.green--text mdi-check-bold - v-btn(v-else, icon) - v-icon.grey--text mdi-cloud-download + //- v-card.animated.fadeInUp.wait-p2s + //- v-toolbar(color='teal', dark, dense, flat) + //- v-toolbar-title.subtitle-1 {{$t('admin:theme.downloadThemes')}} + //- v-spacer + //- v-chip(label, color='white', small).teal--text coming soon + //- v-data-table( + //- :headers='headers', + //- :items='themes', + //- hide-default-footer, + //- item-key='value', + //- :items-per-page='1000' + //- ) + //- template(v-slot:item='thm') + //- td + //- strong {{thm.item.text}} + //- td + //- span {{ thm.item.author }} + //- td.text-xs-center + //- v-progress-circular(v-if='thm.item.isDownloading', indeterminate, color='blue', size='20', :width='2') + //- v-btn(v-else-if='thm.item.isInstalled && thm.item.installDate < thm.item.updatedAt', icon) + //- v-icon.blue--text mdi-cached + //- v-btn(v-else-if='thm.item.isInstalled', icon) + //- v-icon.green--text mdi-check-bold + //- v-btn(v-else, icon) + //- v-icon.grey--text mdi-cloud-download - v-card.mt-3.animated.fadeInUp.wait-p2s + v-card.animated.fadeInUp.wait-p2s v-toolbar(color='primary', dark, dense, flat) v-toolbar-title.subtitle-1 {{$t(`admin:theme.codeInjection`)}} v-card-text diff --git a/client/components/admin/admin-users-edit.vue b/client/components/admin/admin-users-edit.vue index 99d8e2ef..623c6ac1 100644 --- a/client/components/admin/admin-users-edit.vue +++ b/client/components/admin/admin-users-edit.vue @@ -126,8 +126,6 @@ v-list-item-content v-list-item-title {{$t('admin:users.authProvider')}} v-list-item-subtitle {{ user.providerName }} #[em.caption ({{ user.providerKey }})] - //- v-list-item-action - //- v-img(src='https://static.requarks.io/logo/wikijs.svg', alt='', contain, max-height='32', position='center right') template(v-if='user.providerKey === `local`') v-divider v-list-item @@ -168,6 +166,7 @@ v-btn(icon, color='grey', x-small, v-on='on', disabled) v-icon mdi-email span Send Password Reset Email + template(v-if='user.providerIs2FACapable') v-divider v-list-item v-list-item-avatar(size='32') @@ -179,7 +178,7 @@ v-list-item-action v-tooltip(top) 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', @click='toggle2FA') v-icon mdi-power span {{$t('admin:users.toggle2FA')}} template(v-if='user.providerId') @@ -941,6 +940,82 @@ export default { }) } this.$store.commit(`loadingStop`, 'admin-users-verify') + }, + /** + * Toggle 2FA State + */ + async toggle2FA () { + this.$store.commit(`loadingStart`, 'admin-users-toggle2fa') + if (this.user.tfaIsActive) { + const resp = await this.$apollo.mutate({ + mutation: gql` + mutation ($id: Int!) { + users { + disableTFA(id: $id) { + responseResult { + succeeded + errorCode + slug + message + } + } + } + } + `, + variables: { + id: this.user.id + } + }) + if (_.get(resp, 'data.users.disableTFA.responseResult.succeeded', false)) { + this.$store.commit('showNotification', { + style: 'success', + message: this.$t('admin:users.userTFADisableSuccess'), + icon: 'check' + }) + this.user.tfaIsActive = false + } else { + this.$store.commit('showNotification', { + style: 'red', + message: _.get(resp, 'data.users.disableTFA.responseResult.message', 'An unexpected error occurred.'), + icon: 'warning' + }) + } + } else { + const resp = await this.$apollo.mutate({ + mutation: gql` + mutation ($id: Int!) { + users { + enableTFA(id: $id) { + responseResult { + succeeded + errorCode + slug + message + } + } + } + } + `, + variables: { + id: this.user.id + } + }) + if (_.get(resp, 'data.users.enableTFA.responseResult.succeeded', false)) { + this.$store.commit('showNotification', { + style: 'success', + message: this.$t('admin:users.userTFAEnableSuccess'), + icon: 'check' + }) + this.user.tfaIsActive = true + } else { + this.$store.commit('showNotification', { + style: 'red', + message: _.get(resp, 'data.users.enableTFA.responseResult.message', 'An unexpected error occurred.'), + icon: 'warning' + }) + } + } + this.$store.commit(`loadingStop`, 'admin-users-toggle2fa') } }, apollo: { @@ -955,6 +1030,7 @@ export default { providerKey providerName providerId + providerIs2FACapable location jobTitle timezone diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 12cd4a33..7be1b491 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -71,7 +71,7 @@ router.all('/login/:strategy/callback', async (req, res, next) => { strategy: req.params.strategy }, { req, res }) res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() }) - res.redirect('/') + res.redirect(authResult.redirect) } catch (err) { next(err) } diff --git a/server/graph/resolvers/user.js b/server/graph/resolvers/user.js index f76b6a4a..a94aef8a 100644 --- a/server/graph/resolvers/user.js +++ b/server/graph/resolvers/user.js @@ -23,11 +23,15 @@ module.exports = { .select('id', 'email', 'name', 'providerKey', 'createdAt') }, async single(obj, args, context, info) { - console.info(WIKI.auth.strategies) let usr = await WIKI.models.users.query().findById(args.id) usr.password = '' usr.tfaSecret = '' - usr.providerName = _.get(WIKI.auth.strategies, usr.providerKey).displayName + + const str = _.get(WIKI.auth.strategies, usr.providerKey) + str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey]) + usr.providerName = str.displayName + usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false) + return usr }, async profile (obj, args, context, info) { @@ -140,6 +144,28 @@ module.exports = { return graphHelper.generateError(err) } }, + async enableTFA (obj, args) { + try { + await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id) + + return { + responseResult: graphHelper.generateSuccess('User 2FA enabled successfully') + } + } catch (err) { + return graphHelper.generateError(err) + } + }, + async disableTFA (obj, args) { + try { + await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id) + + return { + responseResult: graphHelper.generateSuccess('User 2FA disabled successfully') + } + } catch (err) { + return graphHelper.generateError(err) + } + }, resetPassword (obj, args) { return false }, diff --git a/server/graph/schemas/user.graphql b/server/graph/schemas/user.graphql index 197138cc..0beef80f 100644 --- a/server/graph/schemas/user.graphql +++ b/server/graph/schemas/user.graphql @@ -78,6 +78,14 @@ type UserMutation { id: Int! ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) + enableTFA( + id: Int! + ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) + + disableTFA( + id: Int! + ): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) + resetPassword( id: Int! ): DefaultResponse @@ -130,6 +138,7 @@ type User { providerKey: String! providerName: String providerId: String + providerIs2FACapable: Boolean isSystem: Boolean! isActive: Boolean! isVerified: Boolean! diff --git a/server/models/users.js b/server/models/users.js index da35e8a6..89c335c7 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -28,7 +28,7 @@ module.exports = class User extends Model { providerId: {type: 'string'}, password: {type: 'string'}, tfaIsActive: {type: 'boolean', default: false}, - tfaSecret: {type: 'string'}, + tfaSecret: {type: ['string', null]}, jobTitle: {type: 'string'}, location: {type: 'string'}, pictureUrl: {type: 'string'},