소스 검색

feat: enable/disable TFA per user

NGPixel 4 년 전
부모
커밋
e319355017

+ 26 - 26
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

+ 79 - 3
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

+ 1 - 1
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)
   }

+ 28 - 2
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
     },

+ 9 - 0
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!

+ 1 - 1
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'},